在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

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

正如你可能已经想到的,一般来说,写后(write-behind)这个概念可能会在某些时候导致严重问题,当你期望数据在数据库中可用时,它却还没有到位!

以正确的方式解决问题

这个问题有几种解决方案。一种(非常无知)的解决方案是简单地说:让我们稍微修改一下伪代码,使其包含两个事务。
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引擎将其更改保存到数据库称为刷新(flushing)。考虑到这一点,我们可以修改伪代码使其工作:

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()调用的一部分,实际上破坏了写后(write-behind)的整个概念。在插入一个零件后立即强制Hibernate刷新session,这样在同一个事务中需要插入多个零件的情况下,它就无法再进行优化了。
  • 从前面的论点来看,将其作为updateStock()调用的一部分更好,但是如果还有额外的SQL语句需要执行,我们是否也需要在那里添加flush()调用呢?
hib-flush2.png

总结一下,我们有三个需求(添加零件、更新零件和刷新session),但只有两个可以添加代码的地方来满足需求。这就是面向切面编程(aspect-orientation)的用武之地。面向切面编程技术提供了一个额外的可以添加代码的位置来解决这个需求。换句话说,它允许我们在各自独立的模块中解决每个需求。

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

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

插入新零件


private SessionFactory sessionFactory;

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

使用Hibernate SessionFactory获取一个session。这个session用于保存新零件。

更新零件库存


private SimpleJdbcTemplate jdbcTemplate;

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

同步session 一般来说,我们可以说,每当即将进行JDBC操作时,如果session是脏的,就先刷新session。我们可以将其重新表述为在调用JDBC操作之前,如果Hibernate session是脏的,就刷新它。这句话中有两个重要元素。后半部分说明了我们想做什么。前半部分回答了我们想在哪里何时执行刷新行为的问题。

  • 何时:之前
  • 何地:调用JDBC操作时
  • 何事:刷新脏的Hibernate session

如果了解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 session。

变体/变种

在审查这个切面时,有几点需要记住。

首先,你希望应用此行为的地方可能会有所不同。上面的示例将此行为应用于SimpleJdbcTemplate上的所有方法调用。这可能不适合你的偏好。可以轻松修改切入点(pointcut),将此行为应用于由特定注解标注的方法(例如:execution(@JdbcOperation *(..)))。

其次,你可能会想,如果没有可用的Hibernate Session会发生什么。在Spring管理的环境中,SessionFactory.getCurrentSession()总是创建一个新的Session。如果你希望这个切面即使在完全没有SessionFactory的情况下,或者即使尚未创建Session(并且你也不希望创建一个)也能工作,你应该修改切面以使用Spring的SessionFactoryUtils类。这个类提供了方法,允许你请求一个Session,并且在没有可用Session时不会返回。

源代码

本文附带的源代码使用AspectJ实现了HibernateStateSynchronizer切面。然而,修改这个切面以使其与Spring AOP一起工作也相当直接。

HibernateCarPartsInventoryTests测试用例演示了此行为。当切面启用时,testAddPart()方法成功。当切面禁用时(例如,通过将其从构建路径中排除,或通过注释掉before()通知),测试将失败,因为count语句每次执行时都返回相同数量的记录(换句话说,在查询执行时,数据库中不存在该零件)。

在当前的设置中,before通知被注释掉了,所以测试将会失败。请注意,这个项目的pom.xml文件包含了Maven AspectJ插件。可能会有一些关于版本冲突的警告(由于插件使用的AspectJ版本与项目本身不同),但尽管存在这些警告,它仍然应该能正常工作。

源代码:carplant.zip

获取Spring新闻通讯

订阅Spring新闻通讯保持联系

订阅

获得领先

VMware提供培训和认证,助你快速前进。

了解更多

获取支持

Tanzu Spring在一个简单的订阅中提供对OpenJDK™、Spring和Apache Tomcat®的支持和二进制文件。

了解更多

即将举行的活动

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

查看全部