JDBC 操作之前,请刷新 Hibernate Session(包含 TSE 示例代码)

工程 | Alef Arendsen | 2008年1月4日 | ...

在同一个事务中混合使用对象关系映射器 (ORM) 代码和不使用 ORM 的代码,可能会导致数据在应该可用时在底层数据库中不可用。由于我时不时会遇到这种情况,我认为如果我写下解决这个问题的方法,对大家都会有所帮助。

简而言之:我将在本文其余部分介绍一个方面,它会触发底层持久性机制(JPA、Hibernate、TopLink)将任何脏数据发送到数据库。

顺便说一下,我在去年 12 月的 The Spring Experience 会议上介绍了这个方面,这篇博文也包含了源代码,供那些等待它的人使用。

混合使用 ORM 引擎和直接 JDBC 的必要性

在许多企业应用程序中,使用对象关系映射引擎来管理(有时是复杂的)领域模型的存储和检索。我认为我不必争论的是,在需要持久化高度互联的领域模型的情况下,ORM 工具可能会提高生产力,更不用说比直接使用 JDBC 更高效了。

但这并不意味着可以完全摒弃在应用程序中编写显式 SQL。在许多情况下,仍然需要编写偶尔的 SQL 查询来满足应用程序中的某些需求。我通常会看到人们仍然手动编写 SQL 查询并在 Java 代码中执行它们的原因如下:

  • 代码测试:使用 ORM 工具的代码仍然需要进行测试。为了绝对确定一段数据访问代码(使用 ORM 工具)是否正确地将记录插入到数据库中,需要验证数据库本身……使用直接的 SQL 查询。例如,我个人认为一个很好的做法是首先使用 ORM 工具插入一个对象,然后验证行数是否增加了。
  • 存储过程:最好使用 JDBC 调用来调用存储过程,而不是通过笨拙的 API。我真的不想参与 关于 是否 应该使用 存储过程 的讨论。如果你对此感兴趣,只需阅读其中一些文章即可。情况是:我经常遇到使用存储过程的项目,并且希望将执行此操作的代码与使用 ORM 引擎的代码混合使用。例如,首先插入几个新对象,然后需要对新插入的记录和已有的记录执行聚合。
  • 涉及大量相似对象的运算。例如,当需要将一百万个订单的取消标志从 true 设置为 false 时,可能会出现这种情况。我个人可能不想为此使用 ORM 引擎(有时即使 ORM 引擎有干净的 DML 来帮我完成这项工作)。

混合 ORM 操作与直接 SQL 的问题

在应用程序中混合使用 ORM 引擎执行的操作和使用直接 SQL 执行的操作有一个很大的问题。要理解这一点,首先让我们看一下下面的伪代码片段(假设数据库为空):
start transaction

create part with name Bolt
associate with ORM engine (i.e. save using entity manager)

update part set stock = 15 where name='Bolt'

end transaction

更新语句将失败,尽管我们确实将该部件与实体管理器关联(换句话说:要求实体管理器为我们持久化它)。但是,由于您将其与实体管理器关联,实体管理器不会立即将记录插入到数据库中。这称为写后缓存——几乎所有 ORM 引擎都实现了这一点。实体管理器中的脏状态(例如我们新创建的部件实例)不会立即(使用 SQL 语句)发送到数据库,而通常只有在事务结束时才会发送。

正如您现在可能已经想到的那样,一般来说,这种写后缓存的概念会在某些时间点导致严重的问题,因为您期望数据在数据库中可用,但实际上(尚未)可用!

正确解决问题的方法

这个问题有几种解决方案。一种(非常无知)的解决方案是简单地说:让我们稍微修改一下伪代码,使其包含两个事务。
start transaction
create part with name Bolt
associate with ORM engine (i.e. save using entity manager)
end transaction

start transaction
update part set stock = 15 where name='Bolt'
end transaction

出于显而易见的原因,这不是正确的解决方案。通过这种方式解决问题会导致两个单独的事务。如果想法是将这两个操作作为一个原子操作执行,那么现在情况不再如此了。

这里的正确解决方案是让 ORM 引擎在 SQL 查询执行之前将其更改保存到数据库中。幸运的是,例如 JPAHibernate 都提供了执行此操作的方法。强制 ORM 引擎将其更改保存到数据库中称为刷新。考虑到这一点,我们可以修改伪代码使其工作。

start transaction

create part with name Bolt
associate with ORM engine (i.e. save using entity manager)

*** flush

update part set stock = 15 where name='Bolt'

end transaction

在正确的地方解决问题

既然我们已经解决了这个问题,让我们将这段代码放在上下文中。我之前曾使用 CarPlant 示例 来说明某些事情,我现在将再次这样做。以下序列图显示 CarPartsInventory 首先使用 Hibernate Session 插入一个部件,然后使用 Spring JdbcTemplate(在底层使用直接 JDBC 连接)更新库存。所有这些都在一个事务中运行。hib-flush1.png

如果我们将伪代码直接转换为 Java 代码,我们必须添加 flush() 调用,这时会出现一个难题:我们将 flush() 调用放在哪里:我们将其作为 addPart() 调用的部分(在我们已将部件与 Session 关联之后)还是将其作为 updateStock() 调用的部分(在发出 UPDATE 语句之前)。

无论你如何看待它,两者都是不好的。

  • 将其作为 addPart() 调用的部分实际上破坏了写后缓存的整个概念。写入后插入部件,我们立即强制 Hibernate 刷新会话,因此如果需要在同一事务中插入多个部件,它将无法再进行优化。
  • 从前面的论点来看,将其作为 updateStock() 调用的部分更好,但是如果需要执行其他 SQL 语句,我们是否也需要在那里添加 flush() 调用呢?
hib-flush2.png

总而言之,我们有三个需求(添加部件、更新部件和刷新会话),而我们只能在两个地方添加代码来满足需求。这就是面向方面编程发挥作用的地方。面向方面的编程技术实际上提供了我们可以添加代码以满足此需求的另一个地方。换句话说,它允许我们在各自独立的模块中解决每个需求。

在三个不同的模块中实现三个需求

让我们在单独的模块中处理每个需求。幸运的是,前两个需求非常简单。

插入新部件


private SessionFactory sessionFactory;

public void insertPart(Part p) {
	sessionFactory.getCurrentSession().save(p);
}

使用 Hibernate SessionFactory,我们获得一个会话。该会话用于保存新部件。

更新部件库存


private SimpleJdbcTemplate jdbcTemplate;

public void updateStock(Part p, int stock) {
	jdbcTemplate.update("update stock set stock = stock + ? where number=?", 
		stock, p.getNumber());
}

同步会话 一般来说,可以说每当即将进行 JDBC 操作时,如果会话处于脏状态,则首先刷新会话。我们可以将其重新表述为在调用 JDBC 操作之前,如果 Hibernate 会话处于脏状态,则刷新它。此短语中有两个重要的元素。最后部分指定了我们想要做什么。第一部分回答了我们在哪里何时想要执行刷新行为的问题。

  • 何时:之前
  • 何处:调用 JDBC 操作
  • 做什么:刷新脏的 Hibernate 会话

如果了解 AspectJ 语言,则很容易将其转换为 AspectJ。即使您不想使用 AspectJ,也可以通过使用 Spring AOP 来实现此行为。


public aspect HibernateStateSynchronizer {

	private SessionFactory sessionFactory;
	
	public void setSessionFactory(SessionFactory sessionFactory() {
		this.sessionFactory = sessionFactory;
	}

	pointcut jdbcOperation() : 
		call(* org.springframework.jdbc.core.simple.SimpleJdbcTemplate.*(..));
		
	before() jdbcOperation() {
		Session session = sessionFactory.getCurrentSession();
		if (session.isDirty()) {
			session.flush();
		}
	}
}

这个方面将实现所需的行为;每当即将进行 JDBC 操作时,它都会刷新 Hibernate 会话。

变体

在查看此方面时,需要记住一些事项。

首先,您希望应用此行为的位置可能会有所不同。上面的示例将行为应用于 SimpleJdbcTemplate 上的所有方法调用。这可能对您的口味来说太过了。可以轻松修改切点以将行为应用于由特定注释注释的方法(考虑:execution(@JdbcOperation *(..)))。

其次,您可能想知道如果没有可用的 Hibernate Session 会发生什么。SessionFactory.getCurrentSession() 始终在 Spring 托管的环境中创建一个新的 Session。如果希望此方面能够工作,即使根本没有 SessionFactory,或者尚未创建 Session(并且您不希望创建 Session),也应该修改该方面以使用 Spring 的 SessionFactoryUtils 类。此类具有允许您请求 Session 并且如果没有 Session 则不会返回 Session 的方法。

源代码

随此条目提供的源代码使用 AspectJ 实现 HibernateStateSynchronizer 方面。但是,修改此方面以使其与 Spring AOP 一起工作将非常简单。

HibernateCarPartsInventoryTests 测试用例说明了该行为。启用该方面时,testAddPart() 方法会成功。禁用该方面时(例如,从构建路径中排除它,或注释 before() 建议),测试将失败,因为计数语句每次执行时都会得到相同数量的记录(换句话说,在查询执行时,该部件不存在于数据库中)。

在当前设置中,before 建议已注释掉,因此测试将失败。请注意,此项目的 pom.xml 文件包含 Maven AspectJ 插件。可能有一些关于版本冲突的警告(由插件使用与项目本身不同的 AspectJ 版本引起),但是尽管存在这些警告,它仍然应该可以工作。

源代码:carplant.zip

获取 Spring 电子报

通过 Spring 电子报保持联系

订阅

领先一步

VMware 提供培训和认证,助您快速提升技能。

了解更多

获取支持

Tanzu Spring 通过单一订阅提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看全部