首页 > 解决方案 > Hibernate 5.2.17 不回滚失败的事务

问题描述

我目前正在将一个大型项目从 Hibernate 5.1 更新到 Hibernate 5.2.17,我遇到了一个我正在努力解决的问题。

我们有一套测试正在使用 H2 内存数据库测试我们的 DAO,但是在更新版本的 Hibernate 上一些测试已经开始失败。

一些测试试图null从持久性上下文中删除一个实体,并期望操作失败并显示IllegalArgumentException. 在新版本的 Hibernate 中,异常仍然按预期抛出,但事务不再回滚并保持活动状态,因此导致后续测试失败,因为已经有一个活动事务。堆栈跟踪包括在下面:

java.lang.AssertionError: Transaction is still active when it should have been rolled back.
    at org.junit.Assert.fail(Assert.java:88)
    at hibernatetest.persistence.HibernateTestDAOTest.testDeleteDetachedEntity(HibernateTestDAOTest.java:50)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.junit.rules.ExternalResource$1.evaluate(ExternalResource.java:48)
    at org.junit.rules.RunRules.evaluate(RunRules.java:20)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206)

在进行调查时,我注意到在尝试删除分离实体时也存在类似的行为差异。我已经能够在一个小的独立项目中重新创建该行为,该项目可以在此处找到。该项目还包括针对 Hibernate 5.0.10 运行的 pom.xml(已注释掉)中的配置,其中测试通过没有问题并且失败的事务被正确回滚。

虽然我无法重新创建删除null实体的错误,但我已经设法使用分离的实体重新创建它,我希望为什么会发生这种情况的答案将有助于指导我为什么它null在真实的代码。

我们在这里做错了什么,或者这是 Hibernate 本身的问题?

代码还包括在下面:

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>HibernateTest</groupId>
    <artifactId>HibernateTest</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <maven.compiler.version>3.7.0</maven.compiler.version>

        <!-- Uncomment this property to run as Hibernate 5.0.10 -->
        <!-- <hibernate.core.version>5.0.10.Final</hibernate.core.version> -->
        <!-- Uncomment this property to run as Hibernate 5.2.17 -->
        <hibernate.core.version>5.2.17.Final</hibernate.core.version>
        <junit.version>4.12</junit.version>
        <h2.version>1.4.197</h2.version>
        <javaee.api.version>7.0</javaee.api.version>

    </properties>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven.compiler.version}</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>${hibernate.core.version}</version>
        </dependency>
        <!-- Uncomment these dependencies to run using Hibernate 5.0.10 -->
        <!-- 
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-java8</artifactId>
            <version>${hibernate.core.version}</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>${hibernate.core.version}</version>
            <scope>test</scope>
        </dependency>
        -->
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>${javaee.api.version}</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>${h2.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

HibernateTest.java(实体类):

package hibernatetest.persistence;

import java.util.UUID;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import org.hibernate.annotations.Type;

@Entity
@Table(name = "hibernate_test")
public class HibernateTest {

    @Id
    @Column(name = "id")
    @Type(type = "uuid-char")
    private UUID id;

    public HibernateTest(final UUID id) {
        this.id = id;
    }
}

HibernateTestDAO.java

package hibernatetest.persistence;

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

public class HibernateTestDAO {

    @PersistenceContext(unitName = "hibernate-test")
    private EntityManager entityManager;

    public void delete(final HibernateTest entity) {
        entityManager.remove(entity);
    }
}

EntityManagerRule.java(为测试提供实体管理器的 JUnit Rule):

package hibernatetest.persistence;

import java.lang.reflect.Field;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import org.junit.rules.ExternalResource;

public class EntityManagerRule extends ExternalResource {

    private EntityManagerFactory emFactory;

    private EntityManager em;

    @Override
    protected void before() {
        emFactory = Persistence.createEntityManagerFactory("hibernate-test");
        em = emFactory.createEntityManager();
    }

    @Override
    protected void after() {
        if (em != null) {
            em.close();
        }
        if (emFactory != null) {
            emFactory.close();
        }
    }

    public HibernateTestDAO initDAO() {
        final HibernateTestDAO dao = new HibernateTestDAO();

        try {
            injectEntityManager(dao);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return dao;
    }

    public EntityManager getEntityManager() {
        return em;
    }

    public void persist(final Object entity) {
        final EntityTransaction transaction = em.getTransaction();
        transaction.begin();
        try {
            em.persist(entity);
        } catch (Exception e) {
            transaction.rollback();
            throw e;
        }
        transaction.commit();
    }

    private void injectEntityManager(final HibernateTestDAO dao) throws Exception {
        final Field emField = dao.getClass().getDeclaredField("entityManager");
        emField.setAccessible(true);
        emField.set(dao, em);
    }
}

HibernateTestDAOTest.java

package hibernatetest.persistence;

import static org.junit.Assert.fail;

import java.util.UUID;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

public class HibernateTestDAOTest {

    @Rule
    public EntityManagerRule rule = new EntityManagerRule();

    private HibernateTestDAO dao;

    @Before
    public void setup() {
        dao = rule.initDAO();
    }

    @Test
    public void testDeleteNullEntity() {
        HibernateTest entity = null;
        try {
            dao.delete(entity);
        } catch (IllegalArgumentException e) {
            if (rule.getEntityManager().getTransaction().isActive()) {
                fail("Transaction is still active when it should have been rolled back.");
            }
        }
    }

    @Test
    public void testDeleteDetachedEntity() {
        HibernateTest entity = new HibernateTest(UUID.randomUUID());
        rule.persist(entity);
        rule.getEntityManager().detach(entity);
        try {
            dao.delete(entity);
        } catch (IllegalArgumentException e) {
            if (rule.getEntityManager().getTransaction().isActive()) {
                fail("Transaction is still active when it should have been rolled back.");
            }
        }
    }
}

persistence.xml来自src/test/resources/META-INF

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
    version="2.0">
    <persistence-unit name="hibernate-test" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <class>hibernatetest.persistence.HibernateTest</class>

        <properties>
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="javax.persistence.jdbc.url"
                value="jdbc:h2:mem:test;INIT=create schema if not exists test\;runscript from 'classpath:/populate.sql';DB_CLOSE_DELAY=-1;"/>
            <property name="javax.persistence.validation.mode" value="none"/>
        </properties>
    </persistence-unit>
</persistence>

populate.sql来自src/test/resources

CREATE TABLE IF NOT EXISTS hibernate_test (
  id       UUID NOT NULL
);

标签: javahibernate

解决方案


规范中没有任何内容说明当调用EntityManager#remove失败时持久性提供者应该回滚现有事务,这是没有意义的。

如果您查看 Hibernate 测试套件中的所有示例,您会注意到以下行为:

EntityManager entityManager = getOrCreateEntityManager();
try {
  entityManager.getTransaction().begin();
  // do something
  entityManager.getTransaction().commit();
}
catch ( Exception e ) {
  if ( entityManager != null && entityManager.getTransaction.isActive() ) {
    entityManager.getTransaction().rollback();
  }
  throw e;
}
finally {
  if ( entityManager != null ) {
    entityManager.close();
  }
}

如果您的测试以前工作并且不再以相同的方式工作,我不确定我是否一定会说这是一个错误,因为您在上面提供的代码不符合我在此处显示的正确处理回滚用户代码,除非你有 spring 或其他一些你没有说明的框架。

但是,如果您觉得 5.1 和 5.2 之间存在回归,欢迎您打开 JIRA 并将其与您的可重现测试用例一起报告,我们可以进行进一步调查。

要记住的一个关键点是 5.2.x 引入了 JPA 工件的合并hibernate-entitymanagerhibernate-core因此这里可能会出现回归,但这种可能性极小。


推荐阅读