领先一步
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
更新语句将失败,尽管我们确实将该部件与实体管理器关联(换句话说:要求实体管理器为我们持久化它)。但是,由于您将其与实体管理器关联,实体管理器不会立即将记录插入到数据库中。这称为写后缓存——几乎所有 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 会发生什么。SessionFactory.getCurrentSession() 始终在 Spring 托管的环境中创建一个新的 Session。如果希望此方面能够工作,即使根本没有 SessionFactory,或者尚未创建 Session(并且您不希望创建 Session),也应该修改该方面以使用 Spring 的 SessionFactoryUtils 类。此类具有允许您请求 Session 并且如果没有 Session 则不会返回 Session 的方法。
HibernateCarPartsInventoryTests 测试用例说明了该行为。启用该方面时,testAddPart() 方法会成功。禁用该方面时(例如,从构建路径中排除它,或注释 before() 建议),测试将失败,因为计数语句每次执行时都会得到相同数量的记录(换句话说,在查询执行时,该部件不存在于数据库中)。
在当前设置中,before 建议已注释掉,因此测试将失败。请注意,此项目的 pom.xml 文件包含 Maven AspectJ 插件。可能有一些关于版本冲突的警告(由插件使用与项目本身不同的 AspectJ 版本引起),但是尽管存在这些警告,它仍然应该可以工作。
源代码:carplant.zip