Hibernate/Spring - JUnit 因事务性 (REQUIRES_NEW) 而失败
Hibernate/Spring - JUnit fails with Transactional(REQUIRES_NEW)
我们最近决定将一些方法从 @Transactional
更改为 @Transactional(propagation = Propagation.REQUIRES_NEW)
并在applicationContext.xml
中添加了<tx:annotation-driven proxy-target-class="true"/>
运行应用程序一切正常,但我们的测试失败,出现以下异常:
2016-03-15 20:44:02 [main] DEBUG org.hibernate.SQL -
insert
into
utfylling_versjon
(opprettet, utfylling_id, id)
values
(?, ?, ?)
2016-03-15 20:44:02 [main] TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [1] as [TIMESTAMP] - [Tue Mar 15 20:44:02 CET 2016]
2016-03-15 20:44:02 [main] TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [2] as [BIGINT] - [1216]
2016-03-15 20:44:02 [main] TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [3] as [BIGINT] - [1217]
2016-03-15 20:44:02 [main] WARN o.h.e.jdbc.spi.SqlExceptionHelper - SQL Error: 23506, SQLState: 23506
2016-03-15 20:44:02 [main] ERROR o.h.e.jdbc.spi.SqlExceptionHelper - Referential integrity constraint violation: "FK_JA5LSNJNODJIEEC22M3HU3YIS: PUBLIC.UTFYLLING_VERSJON FOREIGN KEY(UTFYLLING_ID) REFERENCES PUBLIC.UTFYLLING(ID) (1216)"; SQL statement:
insert into utfylling_versjon (opprettet, utfylling_id, id) values (?, ?, ?) [23506-191]
2016-03-15 20:44:02 [main] INFO o.h.e.j.b.internal.AbstractBatchImpl - HHH000010: On release of batch it still contained JDBC statements
2016-03-15 20:44:02 [main] INFO o.s.t.c.t.TransactionContext - Rolled back transaction for test context [DefaultTestContext@41289e88 testClass = RisikoServiceTest, testInstance = no.sb1.forsikring.seopp.kjerne.fip.RisikoServiceTest@7e8783b0, testMethod = sjekkAtFaktaBlirSatt@RisikoServiceTest, testException = org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint ["FK_JA5LSNJNODJIEEC22M3HU3YIS: PUBLIC.UTFYLLING_VERSJON FOREIGN KEY(UTFYLLING_ID) REFERENCES PUBLIC.UTFYLLING(ID) (1216)"; SQL statement:
insert into utfylling_versjon (opprettet, utfylling_id, id) values (?, ?, ?) [23506-191]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement, mergedContextConfiguration = [MergedContextConfiguration@d0e4972 testClass = RisikoServiceTest, locations = '{classpath:test-context.xml}', classes = '{}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader', parent = [null]]].
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint ["FK_JA5LSNJNODJIEEC22M3HU3YIS: PUBLIC.UTFYLLING_VERSJON FOREIGN KEY(UTFYLLING_ID) REFERENCES PUBLIC.UTFYLLING(ID) (1216)"; SQL statement:
insert into utfylling_versjon (opprettet, utfylling_id, id) values (?, ?, ?) [23506-191]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:255)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:221)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:521)
更具体
Caused by: org.h2.jdbc.JdbcSQLException: Referential integrity constraint violation: "FK_JA5LSNJNODJIEEC22M3HU3YIS: PUBLIC.UTFYLLING_VERSJON FOREIGN KEY(UTFYLLING_ID) REFERENCES PUBLIC.UTFYLLING(ID) (1216)"; SQL statement:
insert into utfylling_versjon (opprettet, utfylling_id, id) values (?, ?, ?) [23506-191]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:345)
at org.h2.message.DbException.get(DbException.java:179)
为什么它会失败,因为我们 运行 需要新的交易?
如果我将其改回 @Transactional
那么一切正常,但我们想 运行 在新交易中
编辑:
这是部分代码。我创建了 Utfylling。
Utfylling utfylling = someService.createUtfylling();
//Perform some operations
someService.createUtfyllingVersjon(utfylling);
@Transactional
public Utfylling createUtfylling() {
Utfylling utfylling = new Utfylling()
//some setters
entityManager.persist(utfylling);
return utfylling;
}
然后我调用创建 UtfyllingVersjon
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createUtfyllingVersjon(Utfylling utfylling) {
UtfyllingVersjon utfyllingVersjon = new UtfyllingVersjon(utfylling);
entityManager.persist(utfyllingVersjon);
//some more setters
utfylling.getUtfyllingVersjoner().add(utfyllingVersjon);
entityManager.persist(utfyllingVersjon);
entityManager.merge(utfylling);
}
Utfylling 在进入 createUtfyllingVersjon 时是分离的,所以我必须使用合并。
这在 运行 在码头本地编码时有效,但在 运行 宁 JUnit 测试时失败。
这是我的测试-context.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- enable the configuration of transactional behavior based on annotations -->
<tx:annotation-driven proxy-target-class="true"/>
<context:annotation-config />
<context:component-scan base-package="foo.bar"/>
<bean id="dozerMapper" class="org.dozer.DozerBeanMapper" />
<bean id="h2DataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="org.h2.Driver"/>
<property name="url" value="jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceXmlLocation" value="META-INF/persistence-test.xml"/>
<property name="packagesToScan" value="foo.bar" />
<property name="dataSource" ref="h2DataSource"/>
<property name="jpaVendorAdapter" ref="jpaVendorAdapter"/>
<property name="jpaDialect" ref="jpaDialect"/>
<property name="jpaProperties">
<props>
<prop key="hibernate.show_sql">false</prop>
<prop key="hibernate.hbm2ddl.auto">create</prop>
</props>
</property>
</bean>
<bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="database" value="H2"/>
<property name="databasePlatform" value="org.hibernate.dialect.H2Dialect"/>
<property name="generateDdl" value="true"/>
<property name="showSql" value="true"/>
</bean>
<bean id="jpaDialect" class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/>
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager" />
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
<property name="dataSource" ref="h2DataSource"/>
<property name="jpaDialect" ref="jpaDialect"/>
</bean>
</beans>
这里有两个不同的问题:
为什么会失败?
这非常简单:在您的代码中,您实际上是在执行两次插入。当您尝试执行第二个插入时,您收到:
Referential integrity constraint violation
这是合乎逻辑的,因为您刚刚更改了代码以在单独的事务中执行第二个插入。 这个新事务不"see"前一个插入的记录(仅提交,并且事务内插入在任何给定事务中都是可见的),因此外键约束阻止您插入第二行。为什么?因为如果第一个事务因任何原因被回滚,第二个插入可能会导致完整性违规。所以 DB 完全按照它应该的方式行事。为了避免这种情况发生,您需要以某种方式更改您的代码:
- 要么删除 FK 约束
- 不要在单独的事务中执行属于一起的插入(也许只使用 REQUIRE,而不是 REQUIRE_NEW)
为什么测试和主代码的结果不同?
这有点难。我唯一的假设是您的主代码自动提交第一个事务,因此使第一个插入对第二个事务可见。而你的测试让第一个事务挂起(很可能最后会回滚),这样就导致了上面描述的问题。
我的猜测:
Spring @Transactional
默认传播级别是必需的,所需的规范是:"Support a current transaction, create a new one if none exists."
单元测试本身 运行 在一个事务中,createUtfylling 加入现有事务,然后 createUtfyllingVersion 暂停它,打开它自己的事务,它没有看到挂起的更改并触发外键异常。
在应用程序期间 运行你没有封闭事务,createUtfyllingVersion 创建它自己的立即提交的新事务(因此更新对于后续调用是可见的)
我们最近决定将一些方法从 @Transactional
更改为 @Transactional(propagation = Propagation.REQUIRES_NEW)
并在applicationContext.xml
中添加了<tx:annotation-driven proxy-target-class="true"/>
运行应用程序一切正常,但我们的测试失败,出现以下异常:
2016-03-15 20:44:02 [main] DEBUG org.hibernate.SQL -
insert
into
utfylling_versjon
(opprettet, utfylling_id, id)
values
(?, ?, ?)
2016-03-15 20:44:02 [main] TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [1] as [TIMESTAMP] - [Tue Mar 15 20:44:02 CET 2016]
2016-03-15 20:44:02 [main] TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [2] as [BIGINT] - [1216]
2016-03-15 20:44:02 [main] TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [3] as [BIGINT] - [1217]
2016-03-15 20:44:02 [main] WARN o.h.e.jdbc.spi.SqlExceptionHelper - SQL Error: 23506, SQLState: 23506
2016-03-15 20:44:02 [main] ERROR o.h.e.jdbc.spi.SqlExceptionHelper - Referential integrity constraint violation: "FK_JA5LSNJNODJIEEC22M3HU3YIS: PUBLIC.UTFYLLING_VERSJON FOREIGN KEY(UTFYLLING_ID) REFERENCES PUBLIC.UTFYLLING(ID) (1216)"; SQL statement:
insert into utfylling_versjon (opprettet, utfylling_id, id) values (?, ?, ?) [23506-191]
2016-03-15 20:44:02 [main] INFO o.h.e.j.b.internal.AbstractBatchImpl - HHH000010: On release of batch it still contained JDBC statements
2016-03-15 20:44:02 [main] INFO o.s.t.c.t.TransactionContext - Rolled back transaction for test context [DefaultTestContext@41289e88 testClass = RisikoServiceTest, testInstance = no.sb1.forsikring.seopp.kjerne.fip.RisikoServiceTest@7e8783b0, testMethod = sjekkAtFaktaBlirSatt@RisikoServiceTest, testException = org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint ["FK_JA5LSNJNODJIEEC22M3HU3YIS: PUBLIC.UTFYLLING_VERSJON FOREIGN KEY(UTFYLLING_ID) REFERENCES PUBLIC.UTFYLLING(ID) (1216)"; SQL statement:
insert into utfylling_versjon (opprettet, utfylling_id, id) values (?, ?, ?) [23506-191]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement, mergedContextConfiguration = [MergedContextConfiguration@d0e4972 testClass = RisikoServiceTest, locations = '{classpath:test-context.xml}', classes = '{}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader', parent = [null]]].
org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint ["FK_JA5LSNJNODJIEEC22M3HU3YIS: PUBLIC.UTFYLLING_VERSJON FOREIGN KEY(UTFYLLING_ID) REFERENCES PUBLIC.UTFYLLING(ID) (1216)"; SQL statement:
insert into utfylling_versjon (opprettet, utfylling_id, id) values (?, ?, ?) [23506-191]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:255)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:221)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:521)
更具体
Caused by: org.h2.jdbc.JdbcSQLException: Referential integrity constraint violation: "FK_JA5LSNJNODJIEEC22M3HU3YIS: PUBLIC.UTFYLLING_VERSJON FOREIGN KEY(UTFYLLING_ID) REFERENCES PUBLIC.UTFYLLING(ID) (1216)"; SQL statement:
insert into utfylling_versjon (opprettet, utfylling_id, id) values (?, ?, ?) [23506-191]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:345)
at org.h2.message.DbException.get(DbException.java:179)
为什么它会失败,因为我们 运行 需要新的交易?
如果我将其改回 @Transactional
那么一切正常,但我们想 运行 在新交易中
编辑:
这是部分代码。我创建了 Utfylling。
Utfylling utfylling = someService.createUtfylling();
//Perform some operations
someService.createUtfyllingVersjon(utfylling);
@Transactional
public Utfylling createUtfylling() {
Utfylling utfylling = new Utfylling()
//some setters
entityManager.persist(utfylling);
return utfylling;
}
然后我调用创建 UtfyllingVersjon
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createUtfyllingVersjon(Utfylling utfylling) {
UtfyllingVersjon utfyllingVersjon = new UtfyllingVersjon(utfylling);
entityManager.persist(utfyllingVersjon);
//some more setters
utfylling.getUtfyllingVersjoner().add(utfyllingVersjon);
entityManager.persist(utfyllingVersjon);
entityManager.merge(utfylling);
}
Utfylling 在进入 createUtfyllingVersjon 时是分离的,所以我必须使用合并。 这在 运行 在码头本地编码时有效,但在 运行 宁 JUnit 测试时失败。
这是我的测试-context.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- enable the configuration of transactional behavior based on annotations -->
<tx:annotation-driven proxy-target-class="true"/>
<context:annotation-config />
<context:component-scan base-package="foo.bar"/>
<bean id="dozerMapper" class="org.dozer.DozerBeanMapper" />
<bean id="h2DataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="org.h2.Driver"/>
<property name="url" value="jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceXmlLocation" value="META-INF/persistence-test.xml"/>
<property name="packagesToScan" value="foo.bar" />
<property name="dataSource" ref="h2DataSource"/>
<property name="jpaVendorAdapter" ref="jpaVendorAdapter"/>
<property name="jpaDialect" ref="jpaDialect"/>
<property name="jpaProperties">
<props>
<prop key="hibernate.show_sql">false</prop>
<prop key="hibernate.hbm2ddl.auto">create</prop>
</props>
</property>
</bean>
<bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="database" value="H2"/>
<property name="databasePlatform" value="org.hibernate.dialect.H2Dialect"/>
<property name="generateDdl" value="true"/>
<property name="showSql" value="true"/>
</bean>
<bean id="jpaDialect" class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/>
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager" />
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
<property name="dataSource" ref="h2DataSource"/>
<property name="jpaDialect" ref="jpaDialect"/>
</bean>
</beans>
这里有两个不同的问题:
为什么会失败?
这非常简单:在您的代码中,您实际上是在执行两次插入。当您尝试执行第二个插入时,您收到:
Referential integrity constraint violation
这是合乎逻辑的,因为您刚刚更改了代码以在单独的事务中执行第二个插入。 这个新事务不"see"前一个插入的记录(仅提交,并且事务内插入在任何给定事务中都是可见的),因此外键约束阻止您插入第二行。为什么?因为如果第一个事务因任何原因被回滚,第二个插入可能会导致完整性违规。所以 DB 完全按照它应该的方式行事。为了避免这种情况发生,您需要以某种方式更改您的代码:
- 要么删除 FK 约束
- 不要在单独的事务中执行属于一起的插入(也许只使用 REQUIRE,而不是 REQUIRE_NEW)
为什么测试和主代码的结果不同?
这有点难。我唯一的假设是您的主代码自动提交第一个事务,因此使第一个插入对第二个事务可见。而你的测试让第一个事务挂起(很可能最后会回滚),这样就导致了上面描述的问题。
我的猜测:
Spring @Transactional
默认传播级别是必需的,所需的规范是:"Support a current transaction, create a new one if none exists."
单元测试本身 运行 在一个事务中,createUtfylling 加入现有事务,然后 createUtfyllingVersion 暂停它,打开它自己的事务,它没有看到挂起的更改并触发外键异常。
在应用程序期间 运行你没有封闭事务,createUtfyllingVersion 创建它自己的立即提交的新事务(因此更新对于后续调用是可见的)