返回

Hibernate 6 CriteriaQuery 获取 SQL 语句终极指南

java

Hibernate 6 中从 Criteria API 获取 SQL 的方法

碰到难题了!在 Hibernate 6 中,想从 CriteriaQuery 里拿出 SQL 字符串,发现以前那些老办法都歇菜了。Query.getQueryString() 不好使了,SQLExtractor.from() 也是白搭,Stack Overflow 上那些针对 Hibernate 5 及更早版本的招数也都过时了。光靠日志记录 SQL 语句满足不了我的需求,我得把它们存到字符串里做进一步处理。这可咋整? 别慌,下面就来捋一捋。

一、 问题根源:Hibernate 内部机制的变动

Hibernate 6 对内部 API 做了不少调整,尤其是查询相关的部分。Query.getQueryString() 这个曾经方便的帮手,现在对 CriteriaQuery 不再返回生成的 SQL 了。主要原因是 Hibernate 6 更加强调类型安全和编译时检查,而 getQueryString() 的设计在这方面有些力不从心。 另外其内部直接粗暴使用反射也是有安全性问题的。

二、 解决方案:

既然老路走不通,那就得另辟蹊径。下面介绍几种在 Hibernate 6 中获取 CriteriaQuery 对应 SQL 的可行方法。

1. JdbcServicesgetSqlStatementLogger (适用于仅需查看 SQL 的情况)

虽然不能直接获取到 SQL 字符串,但如果你只是想看看生成的 SQL 长啥样,那么可以用 JdbcServices 里的 getSqlStatementLogger。 这是一种查看Hibernate语句的方式,但不适合复杂场景,但可以提供给你参考。

原理: Hibernate 内部通过 JdbcServices 来管理与 JDBC 相关的服务,其中就包括 SQL 语句的日志记录。我们可以通过它来获取到将要执行的 SQL。

代码示例:

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.jpa.spi.HibernateEntityManagerImplementor;

import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import java.sql.PreparedStatement;

// 假设你已经有一个 EntityManager 对象
EntityManager entityManager = ...;

// 1. 获得Session (从 EntityManager 获取)
Session session = entityManager.unwrap(Session.class);

// 2. 创建 CriteriaQuery (此处以简单的查询为例)
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<YourEntity> criteriaQuery = cb.createQuery(YourEntity.class);
Root<YourEntity> root = criteriaQuery.from(YourEntity.class);
criteriaQuery.select(root);

// 3. 准备 CriteriaQuery (编译和准备查询)
org.hibernate.query.Query<YourEntity> jpaQuery =  (org.hibernate.query.Query<YourEntity>)session.createQuery(criteriaQuery) ;

// 4. 获取 SessionFactoryImplementor
SessionFactoryImplementor sessionFactoryImplementor =  (SessionFactoryImplementor)session.getSessionFactory();

// 5. 访问JdbcServices,从中获取 SqlStatementLogger
String sql = sessionFactoryImplementor.getJdbcServices().getSqlStatementLogger().getSqlStatementParser().getSqlStatement(jpaQuery.getHibernateQl());
System.out.println("Generated SQL: " + sql);

注意事项:

  • 这种方法取到的是Hibernate Query Language (HQL)转换后的产物, 如果SQL方言较为特殊, 可能不一定准确, 但参考价值依旧很高.
  • 获取的仅仅是根据 HQL 推测出的 sql 语句。
  • 不适合拿到语句之后再去做更改并执行的场景。

2. QueryImplementorHQLConverter (进阶玩法,操作更底层)

对于需要获取并操作 SQL 字符串的需求,这条路更有戏。它能让你拿到相对“原始”的 SQL,可定制性更强。

原理: QueryImplementor 是 Hibernate 内部表示查询的对象,HQLConverter 负责 HQL 到 SQL 的转换。 我们可以通过获取QueryImplementor, 利用Hibernate提供的转换能力获取SQL。

代码示例:

import org.hibernate.Session;
import org.hibernate.query.Query;
import org.hibernate.query.spi.QueryImplementor;
import org.hibernate.query.sql.internal.SQLQueryParser;
import org.hibernate.engine.spi.SessionFactoryImplementor;

import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
// 假设已有一个 EntityManager 对象
EntityManager entityManager = ...;

// 1. 获取Session (从 EntityManager 获取)
Session session = entityManager.unwrap(Session.class);

// 2. 创建你的 CriteriaQuery
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<YourEntity> cq = cb.createQuery(YourEntity.class);
Root<YourEntity> root = cq.from(YourEntity.class);
cq.select(root); // 一个简单的查询作为示例

// 3. 获取 QueryImplementor
QueryImplementor<YourEntity> query = (QueryImplementor<YourEntity>) session.createQuery(cq);

// 4. 使用 HQLConverter 获取 SQLString. 需要SessionFactory
SessionFactoryImplementor sessionFactory = (SessionFactoryImplementor) session.getSessionFactory();

// 使用SQLQueryParser和SessionFactory获取SQL
String parsedSql = SQLQueryParser.parse(query.getQueryString(), sessionFactory).getSqlString();

System.out.println("SQL: " + parsedSql);

解释:

  • 我们先把CriteriaQuery 转换成了Hibernate的Query 对象.
  • QueryImplementor接口是Query对象的内部表示。
  • SQLQueryParser 类可以把 HQL 查询转化成 SQL。

进阶使用技巧:

通过这种方式,你可以获取到SQL的AST(抽象语法树),从而对SQL进行更细粒度的修改。你可以通过实现org.hibernate.sql.ast.spi.SqlAstWalker 接口去遍历并且更改AST.

3. 自定义 StatementInspector (最灵活,但有一定侵入性)

这是最灵活的方法,允许你在SQL执行前拦截并获取它,甚至可以修改它!

原理: Hibernate 允许我们通过 StatementInspector 接口来“审查”即将执行的 SQL 语句。

如何实现:

  1. 创建自定义的 StatementInspector

    import org.hibernate.resource.jdbc.spi.StatementInspector;
    
    public class MyStatementInspector implements StatementInspector {
    
        private String lastSql = "";
    
        @Override
        public String inspect(String sql) {
            this.lastSql = sql;
              //可以在这里对SQL语句进行修改或者打印,但通常我们仅获取SQL
            return sql;
        }
    
        public String getLastSql() {
            return lastSql;
        }
    }
    
  2. 配置 Hibernate 使用你的 StatementInspector

    你可以通过多种方式配置,这里给出两种常见的方式:

    • 通过 hibernate.cfg.xml

      <property name="hibernate.session_factory.statement_inspector">
          your.package.MyStatementInspector
      </property>
      
    • 通过 Java 代码配置(例如在 Spring Boot 中):

      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.hibernate.jpa.boot.spi.EntityManagerFactoryBuilder;
      import javax.sql.DataSource;
       @Configuration
      public class HibernateConfig {
      
        @Autowired
        private DataSource dataSource;
         @Bean
        public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder) {
          MyStatementInspector inspector = new MyStatementInspector();
          return builder
              .dataSource(dataSource)
              .packages("your.entity.package") // 替换为你的实体类所在的包
               .properties(Collections.singletonMap("hibernate.session_factory.statement_inspector",inspector))
              .build();
        }
      // ...
      }
      
  3. 使用:
    在执行CriteriaQuery后, 访问MyStatementInspector实例来获取上一次的SQL。

    MyStatementInspector inspector = new MyStatementInspector();
   //按上述方法配置好inspector.
  //...执行了 Criteria Query之后
  String sql = inspector.getLastSql();
  System.out.println(sql);

安全建议:

  • 当你在inspect 方法中修改 SQL时,要格外小心. 错误的SQL修改会导致严重问题.
  • 通常, 使用StatementInspector 的目的是获取而非修改SQL.
  • 确保自定义inspector是线程安全的.

优点:

  • 可以拿到真正要执行的SQL,包括参数占位符(如 ?)。
  • 非常灵活,你甚至可以在这里修改 SQL。

缺点:

  • 有一定侵入性, 因为要修改Hibernate配置。
  • 需要小心处理线程安全, 因为inspect方法可能在多线程环境下被调用。

4. 借助第三方工具 (p6spy)

有些第三方库(例如 p6spy)可以用来拦截 JDBC 操作,从而获取到 Hibernate 生成的 SQL。 虽然不是 Hibernate 原生的方法,但在某些情况下用起来很方便。

原理: p6spy 通过代理 JDBC 驱动来工作,拦截所有 JDBC 调用,包括 SQL 语句的执行。

使用步骤(以 p6spy 为例):

  1. 添加 p6spy 依赖:

    <dependency>
        <groupId>p6spy</groupId>
        <artifactId>p6spy</artifactId>
        <version>3.9.1</version>  </dependency>
    
  2. 配置 p6spy:

    通常需要在 classpath 下创建一个名为 spy.properties 的文件,并配置 p6spy 的行为。例如:

    driverlist=com.mysql.cj.jdbc.Driver # 你的数据库驱动
    # 设置日志文件
    logfile=spy.log
    #使用日志系统输出
    #loggers=com.p6spy.engine.logging.P6SpyLogger
     realdriver=com.mysql.cj.jdbc.Driver
    # 开启 SQL 语句记录
    appender=com.p6spy.engine.spy.appender.FileLogger
    
    
  3. 修改数据库连接 URL:

    将你的数据库连接 URL 前缀从 jdbc:mysql 改为 jdbc:p6spy:mysql。 例如:

    jdbc:p6spy:mysql://localhost:3306/your_database
    

4.运行程序
所有生成的sql将记录在你配置的日志文件中(示例中是spy.log)。

提示

  • 此类方法的优势是通用,不限于Hibernate, 任何通过JDBC访问数据库的操作都能监控.
  • 对于不同的工具,配置方式不一样,要参考工具文档。

总结

搞定 Hibernate 6 里的 CriteriaQuery 获取 SQL,关键在于理解 Hibernate 内部机制的变化。 上面这几招,总有一款适合你:

  • 只想看 SQL: JdbcServices 够用了。
  • 要拿到 SQL 字符串,还得做修改: QueryImplementorHQLConverter
  • 需要最大的灵活性:StatementInspector
  • 不想改 Hibernate 配置: 试试 p6spy 这样的第三方工具。

根据你的实际需求,选一个最合适的方案吧!