介绍
既然我已经描述了JPA和Hibernate刷新策略的基础知识 ,我就可以继续阐明Hibernate的AUTO刷新模式的令人惊讶的行为。
并非所有查询都会触发会话刷新
许多人会认为Hibernate 总是在执行任何查询之前先刷新Session。 虽然这可能是一种更直观的方法,并且可能更接近JPA的AUTO FlushModeType ,但是Hibernate尝试对其进行优化。 如果当前执行的查询不会命中未决的SQL INSERT / UPDATE / DELETE语句,则不严格要求刷新。
如参考文档中所述,AUTO刷新策略有时可能在执行查询之前同步当前持久性上下文。 如果框架作者选择将其命名为FlushMode.SOMETIMES,那将更加直观。
JPQL / HQL和SQL
与许多其他ORM解决方案一样,Hibernate提供了一种非常基于SQL-92语法的有限实体查询语言( JPQL / HQL )。
当前数据库方言将实体查询语言转换为SQL,因此它必须在不同的数据库产品中提供相同的功能。 由于大多数数据库系统都是SQL-92投诉,因此实体查询语言是最常见的数据库查询语法的抽象。
虽然您可以在许多用例(选择实体,甚至是投影)中使用实体查询语言,但有时其有限功能与高级查询请求不匹配。 每当我们想要利用某些特定的查询技术时,例如:
- 视窗功能
- 数据透视表
- 常用表表达式
我们别无选择,只能运行本机SQL查询。
Hibernate是一个持久性框架。 Hibernate从未打算取代SQL。 如果某些查询在本机查询中可以更好地表达,那么在数据库可移植性上牺牲应用程序性能是不值得的。
自动冲洗和HQL / JPQL
首先,我们将测试将要执行HQL查询时AUTO刷新模式的行为。 为此,我们定义了以下不相关的实体:
该测试将执行以下操作:
- 一个人将被坚持。
- 选择用户不应触发刷新。
- 查询人员时,AUTO刷新应触发实体状态转换同步(应在执行选择查询之前执行人员INSERT)。
Product product = new Product();
session.persist(product);
assertEquals(0L, session.createQuery("select count(id) from User").uniqueResult());
assertEquals(product.getId(), session.createQuery("select p.id from Product p").uniqueResult());
提供以下SQL输出:
[main]: o.h.e.i.AbstractSaveEventListener - Generated identifier: f76f61e2-f3e3-4ea4-8f44-82e9804ceed0, using strategy: org.hibernate.id.UUIDGenerator
Query:{[select count(user0_.id) as col_0_0_ from user user0_][]}
Query:{[insert into product (color, id) values (?, ?)][12,f76f61e2-f3e3-4ea4-8f44-82e9804ceed0]}
Query:{[select product0_.id as col_0_0_ from product product0_][]}
如您所见,用户选择尚未触发会话刷新。 这是因为Hibernate会根据挂起的表语句检查当前查询空间。 如果当前正在执行的查询与未刷新的表语句不重叠,则可以安全地忽略刷新。
HQL甚至可以在以下情况下检测到产品冲洗:
- 子选择
session.persist(product); assertEquals(0L, session.createQuery("select count(*) " +"from User u " +"where u.favoriteColor in (select distinct(p.color) from Product p)").uniqueResult());
导致正确的冲洗调用:
Query:{[insert into product (color, id) values (?, ?)][Blue,2d9d1b4f-eaee-45f1-a480-120eb66da9e8]} Query:{[select count(*) as col_0_0_ from user user0_ where user0_.favoriteColor in (select distinct product1_.color from product product1_)][]}
- 或theta风格的联接
session.persist(product); assertEquals(0L, session.createQuery("select count(*) " +"from User u, Product p " +"where u.favoriteColor = p.color").uniqueResult());
触发预期的冲洗:
Query:{[insert into product (color, id) values (?, ?)][Blue,4af0b843-da3f-4b38-aa42-1e590db186a9]} Query:{[select count(*) as col_0_0_ from user user0_ cross join product product1_ where user0_.favoriteColor=product1_.color][]}
它起作用的原因是因为实体查询已被解析并转换为SQL查询。 Hibernate无法引用不存在的表,因此它始终知道HQL / JPQL查询将命中的数据库表。
因此,Hibernate仅知道我们在HQL查询中显式引用的那些表。 如果当前待处理的DML语句暗示数据库触发器或数据库级级联,则Hibernate将不会意识到这些。 因此,即使对于HQL,“自动”刷新模式也可能导致一致性问题。
自动刷新和本机SQL查询
当涉及本地SQL查询时,事情变得越来越复杂。 Hibernate无法解析SQL查询,因为它仅支持有限的数据库查询语法。 许多数据库系统提供了超越Hibernate Entity Query功能的专有功能。
使用本机SQL查询查询Person表不会触发刷新,从而导致不一致问题:
Product product = new Product();
session.persist(product);
assertNull(session.createSQLQuery("select id from product").uniqueResult());
DEBUG [main]: o.h.e.i.AbstractSaveEventListener - Generated identifier: 718b84d8-9270-48f3-86ff-0b8da7f9af7c, using strategy: org.hibernate.id.UUIDGenerator
Query:{[select id from product][]}
Query:{[insert into product (color, id) values (?, ?)][12,718b84d8-9270-48f3-86ff-0b8da7f9af7c]}
新保留的产品仅在事务提交期间插入,因为本机SQL查询未触发刷新。 这是主要的一致性问题,许多开发人员很难调试甚至无法预见。 这是始终检查自动生成的SQL语句的另一个原因。
即使对于命名的本机查询,也会观察到相同的行为:
@NamedNativeQueries(@NamedNativeQuery(name = "product_ids", query = "select id from product")
)
assertNull(session.getNamedQuery("product_ids").uniqueResult());
因此,即使SQL查询已预先加载,Hibernate也不会提取关联的查询空间以使其与未决的DML语句匹配。
否决当前的冲洗策略
即使当前会话定义了默认的刷新策略,您也可以始终在查询基础上覆盖它。
查询刷新模式
ALWAYS模式将在执行任何查询(HQL或SQL)之前刷新持久性上下文。 这次,Hibernate没有应用任何优化,所有待处理的实体状态转换都将与当前数据库事务同步。
assertEquals(product.getId(), session.createSQLQuery("select id from product").setFlushMode(FlushMode.ALWAYS).uniqueResult());
指示Hibernate应该同步哪些表
您还可以在当前正在执行的SQL查询上添加同步规则。 然后,Hibernate将知道在执行查询之前需要同步哪些数据库表。 这对于二级缓存也很有用。
assertEquals(product.getId(), session.createSQLQuery("select id from product").addSynchronizedEntityClass(Product.class).uniqueResult());
结论
自动刷新模式非常棘手,并且在查询基础上解决一致性问题是维护人员的噩梦。 如果决定添加数据库触发器,则必须检查所有Hibernate查询,以确保它们最终不会针对过时的数据运行。
我的建议是使用ALWAYS刷新模式,即使Hibernate作者警告我们:
这种策略几乎总是不必要且效率低下的。
不一致是一些偶尔过早冲洗的问题。 当混合DML操作和查询可能会导致不必要的刷新时,这种情况很难缓解。 在会话事务期间,最好在事务开始时(没有待处理的实体状态转换要同步时)和事务结束时(无论如何将刷新当前持久性上下文)执行查询。
应将实体状态转换操作推向事务的结尾,以尝试避免将它们与查询操作交错(因此防止过早的刷新触发器)。
翻译自: https://www.javacodegeeks.com/2014/08/the-dark-side-of-hibernate-auto-flush.html