获得领先
VMware提供培训和认证,助你快速前进。
了解更多在同一个事务中混合使用对象关系映射器(ORM)的代码与不使用ORM的代码,可能会导致在底层数据库中应该存在的数据却不可用的问题。由于这种情况我偶尔会遇到,我认为如果我把这个问题的解决方案写下来,对大家都会有帮助。
简而言之:本文余下部分将介绍的是一个方面,它会触发底层的持久化机制(JPA、Hibernate、TopLink)将任何脏数据发送到数据库。
顺便说一下,去年12月我在The Spring Experience的一次会议上介绍了这方面的内容,本文也包含了那些等待源码的人所需要的源码。
然而,这并不意味着在应用程序中编写显式SQL可以完全放弃。在许多情况下,为了满足应用程序中的特定需求,仍然需要编写偶尔的SQL查询。我通常看到人们仍然手动编写SQL查询并在Java代码中执行的几个原因,例如:
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查询执行之前将更改保存到数据库。幸运的是,例如JPA和Hibernate都提供了这样做的方法。强制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
如果我们将伪代码直接翻译成Java代码,我们就必须添加flush()调用,这时一个棘手的问题出现了:我们应该把flush()调用放在哪里?是让它成为addPart()调用的一部分(在我们把零件与Session关联后立即执行),还是让它成为updateStock()调用的一部分(在发出UPDATE语句之前执行)?
无论你怎么看,这两种方式都不好
总结一下,我们有三个需求(添加零件、更新零件和刷新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是脏的,就刷新它。这句话中有两个重要元素。后半部分说明了我们想做什么。前半部分回答了我们想在哪里和何时执行刷新行为的问题。
如果了解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时不会返回。
HibernateCarPartsInventoryTests测试用例演示了此行为。当切面启用时,testAddPart()方法成功。当切面禁用时(例如,通过将其从构建路径中排除,或通过注释掉before()通知),测试将失败,因为count语句每次执行时都返回相同数量的记录(换句话说,在查询执行时,数据库中不存在该零件)。
在当前的设置中,before通知被注释掉了,所以测试将会失败。请注意,这个项目的pom.xml文件包含了Maven AspectJ插件。可能会有一些关于版本冲突的警告(由于插件使用的AspectJ版本与项目本身不同),但尽管存在这些警告,它仍然应该能正常工作。
源代码:carplant.zip