Spring/JTA/JPA单元测试:回滚不起作用

5

我正在尝试使用Spring测试一个EJB3实体。

EJB本身并没有使用Spring,我希望尽量减少生产JPA配置的重复(例如不复制persistence.xml)。

我的单元测试似乎工作正常,但是即使我的单元测试应该是事务性的,在各个测试方法之间数据仍然被持久化...

这是我的实体:

package sample;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Ejb3Entity {

    public Ejb3Entity(String data) {
        super();
        this.data = data;
    }
    private Long id;
    private String data;

    @Id
    @GeneratedValue
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }

    public String getData() {
        return data;
    }
    public void setData(String data) {
        this.data = data;
    }

}

我的单元测试:

package sample;

import static org.junit.Assert.*;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"/appContext.xml"})
@Transactional
public class Ejb3EntityTest {

    @PersistenceContext
    EntityManager em;

    @Before
    public void setUp() throws Exception {
        Ejb3Entity one = new Ejb3Entity("Test data");
        em.persist(one);
    }

    @Test
    public void test1() throws Exception {

        Long count = (Long) em.createQuery("select count(*) from Ejb3Entity").getSingleResult();
        assertEquals(Long.valueOf(1l), count);
    }

    @Test
    public void test2() throws Exception {

        Long count = (Long) em.createQuery("select count(*) from Ejb3Entity").getSingleResult();
        assertEquals(Long.valueOf(1l), count);
    }

}

并且我的 appContext.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-2.5.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">

    <bean id="jotm" class="org.springframework.transaction.jta.JotmFactoryBean" />

    <bean id="transactionManager"
        class="org.springframework.transaction.jta.JtaTransactionManager">
        <property name="userTransaction" ref="jotm" />
        <property name="allowCustomIsolationLevels" value="true" />
    </bean>

    <bean id="dataSource" class="org.enhydra.jdbc.standard.StandardXADataSource">
        <property name="driverName" value="org.h2.Driver" />
        <property name="url" value="jdbc:h2:mem:unittest;DB_CLOSE_DELAY=-1" />
        <property name="user" value="" />
        <property name="password" value="" />
        <property name="transactionManager" ref="jotm" />
    </bean>

    <bean id="emf"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="persistenceUnitPostProcessors">
            <bean class="sample.JtaDataSourcePersistenceUnitPostProcessor">
                <property name="jtaDataSource" ref="dataSource" />
            </bean>
        </property>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="showSql" value="false" />
                <property name="generateDdl" value="true" />
                <property name="database" value="H2" />
                <property name="databasePlatform" value="org.hibernate.dialect.H2Dialect" />
            </bean>
        </property>
        <property name="jpaPropertyMap">
            <map>
                <entry key="hibernate.transaction.manager_lookup_class"
                    value="org.hibernate.transaction.JOTMTransactionManagerLookup" />
                <entry key="hibernate.transaction.auto_close_session" value="false" />
                <entry key="hibernate.current_session_context_class" value="jta" />
            </map>
        </property>

    </bean>


</beans>

当我运行测试时,test2失败了,因为它发现了两个实体,而我只期望一个(因为第一个应该已经回滚了...)
我尝试了很多不同的配置,这个配置似乎是我能得到的最全面的...我没有其他想法。你呢?

为什么您认为第一个实体应该已经被回滚? - janko
因为我正在使用@Transactional注解,它使得每次测试运行都使用自己的事务,并且由Spring自动回滚。 - Michel
4个回答

2
我使用Bitronix代替JOTM,成功使它工作。Bitronix提供了一个LrcXADataSource,允许非XA数据库参与JTA事务。
我认为问题是H2不符合XA标准,而enhydra的StandardXADataSource也无法通过魔法解决这个问题(我最后使用了HSQLDB,但这与问题无关)。
以下是我的Spring上下文配置,可以正常工作:
<?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-2.5.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">

    <context:annotation-config />
    <tx:annotation-driven transaction-manager="transactionManager" />

    <!--  Bitronix Transaction Manager embedded configuration -->
    <bean id="btmConfig" factory-method="getConfiguration"
        class="bitronix.tm.TransactionManagerServices">
        <property name="serverId" value="spring-btm" />
        <property name="journal" value="null" />
    </bean>

    <!-- create BTM transaction manager -->
    <bean id="BitronixTransactionManager" factory-method="getTransactionManager"
        class="bitronix.tm.TransactionManagerServices" depends-on="btmConfig,dataSource"
        destroy-method="shutdown" />

    <bean id="transactionManager"
        class="org.springframework.transaction.jta.JtaTransactionManager">
        <property name="transactionManager" ref="BitronixTransactionManager" />
        <property name="userTransaction" ref="BitronixTransactionManager" />
        <property name="allowCustomIsolationLevels" value="true" />
    </bean>


    <!-- DataSource definition -->

    <bean id="dataSource" class="bitronix.tm.resource.jdbc.PoolingDataSource"
        init-method="init" destroy-method="close">
        <property name="className" value="bitronix.tm.resource.jdbc.lrc.LrcXADataSource" />
        <property name="uniqueName" value="unittestdb" />
        <property name="minPoolSize" value="1" />
        <property name="maxPoolSize" value="3" />
        <property name="allowLocalTransactions" value="true" />
        <property name="driverProperties">
            <props>
                <prop key="driverClassName">org.hsqldb.jdbcDriver</prop>
                <prop key="url">jdbc:hsqldb:mem:unittestdb</prop>
                <prop key="user">sa</prop>
                <prop key="password"></prop>
            </props>
        </property>
    </bean>

    <!-- Entity Manager Factory -->
    <bean id="emf"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="showSql" value="true" />
                <property name="generateDdl" value="true" />
                <property name="database" value="HSQL" />
            </bean>
        </property>
        <property name="jpaPropertyMap">
            <map>
                <entry key="hibernate.transaction.manager_lookup_class"
                    value="org.hibernate.transaction.BTMTransactionManagerLookup" />
                <entry key="hibernate.transaction.auto_close_session" value="false" />
                <entry key="hibernate.current_session_context_class" value="jta" />
            </map>
        </property>

    </bean>

发布不到一周,我就遇到了同样的问题。我也无法正确地让JOTM回滚;它总是说正在回滚,但事务的更改仍会影响数据库。BTM可以很好地完成这项工作,无论是使用MySQL还是H2后端。奇怪。 - Henning

2
当我尝试集成JOTM和Hibernate时,最终不得不编写我的ConnectionProvider实现。以下是它目前的样子:http://pastebin.com/f78c66e9c
然后,在Hibernate属性中指定您的实现作为连接提供程序,事务就会神奇地开始工作。
问题在于默认的连接提供程序在数据源上调用getConnection()。在您自己的实现中,您调用getXAConnection().getConnection()。这就是区别所在。

抱歉,我最终使用了BTM而不是JOTM,并且我没有机会做你建议的事情。 - Michel

1

编辑:(抱歉,当我写这段话时,似乎我只有一半清醒。当然,你是对的,默认情况下应该回滚所有内容。)

您可以检查事务管理器实际执行的操作,例如通过为其启用调试输出。

假设使用log4j:

log4j.logger.org.springframework.transaction=DEBUG

事务管理器会提供非常好的日志输出,包括已创建和加入的事务,以及提交和回滚的信息。这将有助于您找出设置中存在的问题。

0
在Spring文档中提到的那样,在 @Transactional 注释之后,添加 @Rollback 注释(来自 org.springframework.test.annotation)。
@Rollback is a test annotation that is used to indicate whether a test-
managed transaction should be rolled back after the test method has 
completed. 
Consult the class-level Javadoc for 
org.springframework.test.context.transaction.TransactionalTest-
ExecutionListener for an explanation of test-managed transactions. 

When declared as a class-level annotation, @Rollback defines the default 
rollback semantics for all test methods within the test class hierarchy. When 
declared as a method-level annotation, @Rollback defines rollback semantics 
for the specific test method, potentially overriding class-level default 
commit or rollback semantics. 

As of Spring Framework 4.2, @Commit can be used as direct replacement for 
@Rollback(false). 

Warning: Declaring @Commit and @Rollback on the same test method or on the 
same test class is unsupported and may lead to unpredictable results. 

This annotation may be used as a meta-annotation to create custom composed 
annotations. Consult the source code for @Commit for a concrete example.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接