领先一步
VMware 提供培训和认证,助您加速进步。
了解更多在同一个事务中混合使用对象关系映射器(Object-Relational Mapper)的代码和不使用它的代码,可能会导致数据在底层数据库中未能及时可用的问题。由于这种情况我时常遇到,我认为如果我写下解决此问题的方法,对大家都会有所帮助。
简而言之:我将在本文的其余部分中介绍一个方面,它触发底层持久化机制(JPA、Hibernate、TopLink)将任何脏数据发送到数据库。
在上个十二月,我参加 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
这里的更新语句将失败,尽管我们_确实_将部件与实体管理器关联起来(换句话说:要求实体管理器为我们持久化它)。然而,实体管理器不会因为您将其与实体管理器关联而立即将记录插入数据库。这被称为“写回”——几乎所有 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 查询执行之前将其更改保存到数据库中_。幸运的是,JPA 和 Hibernate 都提供了实现此功能的方法。强制 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

如果我们将伪代码直接转换为 Java 代码,我们必须添加 flush() 调用,这时就出现了一个棘手的问题:我们将 flush() 调用放在哪里:是将其作为 addPart() 调用的一部分(在我们将部件与 Session 关联后),还是将其作为 updateStock() 调用的一部分(在发出 UPDATE 语句之前)?
无论你怎么看,两者都是不好的
结论是我们有三个需求(添加部件、更新部件和刷新会话),但只有两个地方可以添加代码来解决需求。这就是面向切面编程发挥作用的地方。面向切面编程技术本质上提供了一个额外的、可以添加代码来解决这个需求的地方。换句话说,它允许我们在各自独立的模块中解决每个需求。
插入新部件
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 会话是脏的,请刷新它_。这句话中有两个重要元素。后半部分指明了我们_想_做什么。前半部分回答了我们_在_哪里以及_何时_执行刷新行为的问题。
如果了解 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。
HibernateCarPartsInventoryTests 测试用例演示了该行为。当切面启用时,testAddPart() 方法成功。当切面禁用时(例如,通过将其从构建路径中排除,或注释掉 before() 建议),测试将失败,因为每次执行时计数语句的记录数量都相同(换句话说,在查询执行时,部件不在数据库中)。
在当前设置中,before 建议被注释掉了,所以测试将**失败**。请注意,此项目的 pom.xml 文件包含 Maven AspectJ 插件。可能会有一些关于版本冲突的警告(由插件使用与项目本身不同的 AspectJ 版本引起),但尽管有这些警告,它仍然应该有效。
源代码:carplant.zip