Spring + Hibernate + JUnit(中)

spring 提供的 @Transactional 支持设定 transaction 的类型和 isolation level,这分别意味着,当前这个方法如何使用 transaction,这个 transaction 与其他的 transaction 的关系如何。我们通过 propagation 设定前者:

  • MANDATORY,必须存在一个 transaction,否则抛出异常(说明这个只能发生在 transaction 中间和后面)
  • NESTED 嵌套的 transaction,这样这部分出现 rollback 的时候只是这个内层的 rollback
  • NEVER 一定不在一个 transaction 当中,否则抛出异常
  • NOT_SUPPORTED 将存在的 transaction 关闭
  • REQUIRED(默认),要求存在一个 transaction 否则创建一个,如果有则不做事情
  • REQUIRES_NEW,要求存在一个 transaction,如果已经存在了则结束前面的,创建新的
  • SUPPORTS,有或者没有 transaction 都可以

而 isolation 可以为如下级别:

  • DEFAULT 使用数据库默认的,这个比较 tricky,每个数据库是不一样的
  • READ_UNCOMMITTED,允许做 dirty read
  • READ_COMMITTED,不允许做 dirty read,但允许做 non-repeatable read
  • REPEATBLE_READ,不能做 non-repeatable read 但允许做 phantom read
  • SERIALIZABLE,不允许做 phantom read

要看懂这个就必须理解清楚如下几个概念:

  • dirty read,可以读别的 transaction 尚未 commit 的数据
  • non-repeatable read,可以读别的 transaction 已经 commit 的数据(因此 repeatable read 是说不能读别的 transaction 已经 commit 的数据)
  • phantom read,当前 transaction 读取数据后,别的 transaction 可以进行 insert、update 能使得该数据过期的操作

那么前面一部分提到的策略:另开一个 session 要能真正的 work 或者我们使用 REQUIRES_NEW 保证第二次读是在新的 transaction 里面进行,或者要弄成 READ_COMMITTED。后者似乎有点小问题,因为不是所有的数据库都支持这个操作,比如 MySQL 在不打开 replication 的时候是支持的,但是打开之后就是不支持的了。

从前面的认识我们可以开始一些探索:

  • spring 与 JUnit 的结合始于 SpringJUnit4ClassRunner,我们从那里开始,很明显这是实现 JUnit 某个类,其中发现似乎测试相关的上下文交给了 TestContextManager
  • TestContextManager 会注册一系列的 TestExecutionListener,这些 listener 在不同的时期会做一些不同的事情,我们知道通过 annotation 我们获得的一定是 TransactionalTestExecutionListener
  • 这个实现里面很快就能定位到 PlatformTransactionManager,它是实际创建 transaction 的对象,我们的应用中使用了 HibernateTransactionManager
  • 在 hibernate 对应的 transaction manager 里面我们可以看到通过 HibernateTransactionObject(其实是对 SessionHolder 的简单封装)的 setSession 将一个新创建的 Session 存放起来

问题是我们不可能获得这个 HibernateTransactionObject,因此我们不能够直接拿到对应的 SessionHolder。几经阅读这部分代码发现另有 TransactionSynchronizationManager 提供了 static method getResource 可以直接将为 session factory 管理的资源(即 SessionHolder)拿到,因此一种曲线救国的策略自然是写个简单的类,提供一个 renewCurrentSession 方法,将当前作废的 Session 关闭并重新申请一个 Session 放在 SessionHolder 里面。这样捕获了 ConstraintViolationException 之后我们直接调用这个 renewCurrentSession 后面的事情就能继续下去了。

回到前面的问题,每个 @Test 结束是不是会 rollback?答案是肯定的:

  • 在 TransactionalTestExecutionLister 中通过 TransactionContext 类将 PlatformTransactionManager 进行了简单的封装,其中 beforeTestMethod 中创建了这个对象后调用 startNewTransaction 获得了 transaction,而在 afterTestMethod 中根据 isRollback 判断最后是否 rollback
  • 不同的 @Test 是否会共享 Session?答案当然是不会,在 HibernateTransactionManager 里面的 doBegin 我们可以看到通过 openSession 创建的 Session 加入 SessionHolder 之后尝试进行各种事情,一旦发生异常,默认行为是 rollback 并且关闭这个 Session;在 doCleanupAfterCompletion 之后也有关闭 Session(or deferred)的行为。

有了这些概念,我们相信可以为经典的 exception recovery 问题提供几种解决策略。

有用的链接

——————
Then went Esau unto Ishmael, and took unto the wives which he had Mahalath the daughter of Ishmael Abraham’s son, the sister of Nebajoth, to be his wife.

Advertisements
Spring + Hibernate + JUnit(中)

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s