【问题标题】:Spring @Transactional春天@Transactional
【发布时间】:2014-03-13 03:30:02
【问题描述】:

我正在尝试为使用 Spring @Transactional 注释的系统构建一个简单的极简 JUnit 测试,但并没有取得多大成功。

我正在为具有唯一约束的列创建两个具有相同值的实例。如果两个实例创建碰巧在不同的事务中,我希望第一个将提交,第二个将抛出异常,导致一行 - 我看到这种情况发生了。如果这两个插入发生在同一个事务中,我希望两者都作为一个原子单元回滚,我没有看到。我确定某处存在配置问题,但我没有太多运气识别它。

对于测试,我有一个 bean (ContextTestHelperImpl / ContextTestHelper),其中包含创建一个或两个实例的方法。每个方法都有一个 Propagation.REQUIRES_NEW 注释:

@Override
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, 
        rollbackFor = {ContextDuplicationException.class})
public Foo createOneFoo(int val) throws ContextDuplicationException {
    try {
        return fooDAO.createFoo(val);
    } catch (Throwable th) {
        throw new ContextDuplicationException(th);
    }
}

@Override
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW, 
        rollbackFor = {ContextDuplicationException.class})
public Foo[] createTwoFoos(int val) throws ContextDuplicationException {
    try {
        return new Foo[] { fooDAO.createFoo(val), fooDAO.createFoo(val) };
    } catch (Throwable th) {
        throw new ContextDuplicationException(th);
    }
}

我有一个 JUnit 测试 (ContextTest),它调用第一个方法两次:

public void dupTestTwoTransactions() {
    contextTestHelper.deleteAllFoo();
    Assert.assertEquals(0, contextTestHelper.getCountOfFoo());

    boolean hadException = false;
    try {
        contextTestHelper.createOneFoo(DUPLICATE_VALUE);
    } catch (ContextDuplicationException th) {
        hadException = true;
        System.out.println("[ContextTest][dupTestTwoTransactions] UNEXPECTED ERROR: " + th);
        th.printStackTrace();
    }
    Assert.assertEquals(hadException, false);
    Assert.assertEquals(1, contextTestHelper.getCountOfFoo());

    try {
      contextTestHelper.createOneFoo(DUPLICATE_VALUE);
    } catch (ContextDuplicationException th) {
        hadException = true;
    }
    Assert.assertEquals(hadException, true);
    Assert.assertEquals(1, contextTestHelper.getCountOfFoo());
}

这按预期工作。第一次调用不会抛出异常;第二个电话确实如此。最后,Foo 表中只有一行。

我在同一个类中有第二个 JUnit 测试,它调用后一个方法一次:

@Test
public void dupTestOneTransaction() {
    contextTestHelper.deleteAllFoo();
    Assert.assertEquals(0, contextTestHelper.getCountOfFoo());

    boolean hadException = false;
    try {
      contextTestHelper.createTwoFoos(DUPLICATE_VALUE);
    } catch (ContextDuplicationException th) {
        hadException = true;
    }
    Assert.assertEquals(hadException, true);
    Assert.assertEquals(0, contextTestHelper.getCountOfFoo());
}

第二个测试在最终断言上失败 - Foo 实例的计数为 1,而我期望为 0。

我在数据源设置方面有一些恶作剧,因为我们尝试使用 JNDI 查找来查找代码在 JBoss 下运行的时间。因此,JUnit 需要在后台设置 JNDI 查找(ContextTest.java):

@BeforeClass
public static void setUpClass() throws NamingException {
    ApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring-test.xml");
    DataSource testDataSource = (DataSource) context.getBean("testDataSource");
    SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
    builder.bind("java:comp/env/jdbc/dataSource", testDataSource);
    builder.activate();
}

这是我的 spring-test.xml 文件,它在 NetBeans 中设置在测试包的默认包中:

<?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-3.0.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.0.xsd">

<context:property-placeholder location="user-specific.properties"/>

<bean id="testDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName"><value>${db.driver.class}</value></property>
    <property name="url"><value>${db.url}</value></property>
    <property name="username"><value>${db.user}</value></property>
    <property name="password"><value>${db.password}</value></property>
</bean>
</beans>

由于第一次测试成功,我显然可以连接到数据库,所以我认为这里没有什么特别的问题。

这里是applicationContext.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:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
       http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.2.xsd"
        default-autowire="byName" >

    <context:annotation-config />
    <context:component-scan base-package="com.xyzzy" />
    <tx:annotation-driven transaction-manager="transactionManager" />

    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="location">
           <value>classpath:user-specific.properties</value>
        </property>
    </bean>

    <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/dataSource"/>

    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <bean id="fooDAO" class="com.xyzzy.FooDAOImpl" />

    <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
        <property name="dataSource"><ref local="dataSource"/></property>
        <property name="packagesToScan" value="com.xyzzy" />
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">${db.dialect}</prop>
                <prop key="hibernate.show_sql">${db.show_sql}</prop>
                <prop key="hibernate.hbm2ddl.auto">${db.hbm2ddl.auto}</prop>
            </props>
        </property>
    </bean>

    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="databasePlatform" value="${db.dialect}"/>
                <property name="generateDdl" value="true"/>
                <property name="showSql" value="true"/>

            </bean>
        </property>
        <property name="packagesToScan" value="com.xyzzy" />
    </bean> 
</beans>

所有非单独测试类(Foo、FooDAO、FooDAOImpl)都在包 com.xyzzy 中,所有测试类(ContextTest、ContextTestHelper、ContextTestHelperImpl、ContextDuplicationException)都在 com.xyzzy 中。测试。

Foo、FooDAO 或 FooDAOImpl 上没有 @Transactional 注释。 ContextTestHelperImpl 是唯一指定事务边界的。

关于如何解决此问题以使其正常运行的任何建议? (我选择的dataSource类还是transactionManager有什么问题吗?我在applicationContext.xml中的某些设置是否不一致或多余?)

更新:

DAO实现类:

@Repository("FooDAO")
public class FooDAOImpl implements FooDAO {
    private HibernateTemplate hibernateTemplate;

    @Autowired
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.hibernateTemplate = new HibernateTemplate(sessionFactory);
    }

    @Override
    public Foo createFoo(int val) {
        Foo foo = new Foo();
        foo.setVal(val);
        saveFoo(foo);
        return foo;
    }

    void saveFoo(Foo foo) {
        hibernateTemplate.save(foo);
    }

    [... and many similar methods ...]

我也用 Propagation.REQUIRED 尝试过(在某个地方读到,如果没有与线程关联的事务,它将在早期导致异常),但这不会改变它的行为。

【问题讨论】:

    标签: spring junit transactions


    【解决方案1】:

    我相信你在一个班级有 2 次测试。您假设这些测试是按顺序运行的。您期望准确的记录计数为 1 或 0。测试是并行运行的,因此与您的预期结果相冲突。

    更改逻辑,以便每个测试使用它自己的唯一值(用于插入)。而不是基于全选(无条件)比较记录计数。从带有条件的数据库中选择(添加 where 子句)。

    总而言之,测试运行器正在并行运行测试。您需要考虑并隔离测试用例。在每个测试用例中使用唯一值。

    希望对您有所帮助。

    【讨论】:

    • 感谢您的回复,但我很确定这不是问题所在。当我打印出每个方法的入口/出口时,我看到测试是按顺序运行的。 (正如你所建议的,我将第二个测试更改为使用不同的唯一值,但它仍然以同样的方式失败。)
    • 感谢您的回复。我会挑战以下前提: return new Foo[] { fooDAO.createFoo(val), fooDAO.createFoo(val) }; 在同一个事务中执行 2 次插入。那不会发生在你的DAO中吗?喜欢: entityManager.persist(s); entityManager.persist(s);.我会通过调试器运行代码来测试前提。数据库只会拒绝第二次插入。因此,您的前提仅是在单个事务中发生的 2 个插入。因此,我将验证单个事务是否执行两个插入。祝你好运。
    • 对不起 :) 错字。我的评论应该是:“所以你的前提完全依赖于单个事务中发生的 2 个插入。”。前提是事务管理器,而不是数据库,回滚初始插入。
    • 我确实假设插入发生在同一个事务中,但这不是 Transactional 注释应该做的吗?如果我将事务更改为只读,它会立即失败,因此似乎 Transactional 注释被捕获并产生了一些影响。我认为使用 Spring 管理关闭了 Hibernate 自动提交。
    • 感谢您提出有趣的挑战。我在质疑 Transactional 注释的放置位置(不是是否,抱歉)。它不应该去实体经理提交/坚持的地方吗?我将通过用 java 源代码替换注入的事务来进行测试。这样您就可以保证单笔交易(仅作为证明概念)。
    猜你喜欢
    • 2018-08-05
    • 2015-09-18
    • 2011-09-23
    • 2015-10-06
    • 2013-11-12
    • 2020-02-02
    • 2021-12-02
    • 1970-01-01
    • 2011-11-28
    相关资源
    最近更新 更多