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

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

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

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

在上个十二月,我参加 The Spring Experience 的其中一次会议时,顺便介绍了这个方面的内容,这篇文章也包含了那些一直等待它的源代码。

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

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

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

  • 测试代码:使用 ORM 工具的代码仍然需要测试。为了绝对确定一段数据访问代码(使用 ORM 工具)正确地将记录插入到数据库中,需要验证数据库本身……使用纯 SQL 查询。我个人认为,例如,首先使用 ORM 工具插入一个对象,然后验证行数是否增加是一个非常好的实践。
  • 存储过程:最好使用 JDBC 调用而不是笨重的 API 调用存储过程。我真的不想卷入关于存储过程是否好的 争论 (whether) (stored) (procedures)。如果你对此感兴趣,只需阅读其中一些文章。情况是:我经常遇到使用存储过程的项目,并且希望将使用存储过程的代码与使用 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,会发生什么。在 Spring 管理的环境中,SessionFactory.getCurrentSession() 总是会创建一个新的 Session。如果您希望这个切面能够工作,即使根本没有 SessionFactory,或者还没有创建 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 社区所有即将举行的活动。

查看所有