Hibernate 6 CriteriaQuery 获取 SQL 语句终极指南
2025-03-19 19:56:38
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. JdbcServices
的 getSqlStatementLogger
(适用于仅需查看 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. QueryImplementor
和 HQLConverter
(进阶玩法,操作更底层)
对于需要获取并操作 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 语句。
如何实现:
-
创建自定义的
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; } }
-
配置 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(); } // ... }
-
-
使用:
在执行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 为例):
-
添加 p6spy 依赖:
<dependency> <groupId>p6spy</groupId> <artifactId>p6spy</artifactId> <version>3.9.1</version> </dependency>
-
配置 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
-
修改数据库连接 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 字符串,还得做修改:
QueryImplementor
和HQLConverter
- 需要最大的灵活性: 上
StatementInspector
! - 不想改 Hibernate 配置: 试试 p6spy 这样的第三方工具。
根据你的实际需求,选一个最合适的方案吧!