Spring Boot + Spring Data JPA + 事务不正常工作

11

我使用1.2.0版本的spring-boot-starter-data-jpa创建了一个Spring Boot应用程序,并且我正在使用MySQL。我已经在application.properties文件中正确配置了我的MySQL属性。

我有一个简单的JPA实体、Spring Data JPA仓库和服务如下所示:

@Entity
class Person
{
    @Id @GeneratedValue(strategy=GenerationType.AUTO)
    private Integer id;
    private String name;
    //setters & getters
}

@Repository
public interface PersonRepository extends JpaRepository<Person, Integer>{

}


import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
class PersonService
{
    @Autowired PersonRepository personRepository;

    @Transactional
    void save(List<Person> persons){
        for (Person person : persons) {         
            if("xxx".equals(person.getName())){
                throw new RuntimeException("boooom!!!");
            }
            personRepository.save(person);          
        }
    }
}

我有以下的JUnit测试:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {

    @Autowired
    PersonService personService;

    @Test
    public void test_logging() {
        List<Person> persons = new ArrayList<Person>();
        persons.add(new Person(null,"abcd"));
        persons.add(new Person(null,"xyz"));
        persons.add(new Person(null,"xxx"));
        persons.add(new Person(null,"pqr"));

        personService.save(persons);
    }

}
期望不向“PERSON”表中插入记录,因为在插入第三个人对象时会抛出异常。但是事务没有回滚,前两个记录已被插入并提交。 然后我想用JPA EntityManager快速尝试一下。
@PersistenceContext
private EntityManager em;

em.save(person);

然后我遇到了javax.persistence.TransactionRequiredException: No transactional EntityManager available 异常。

在谷歌上搜索一段时间后,我遇到了与此相同主题的 JIRA 线程 https://jira.spring.io/browse/SPR-11923

然后我将 Spring Boot 版本更新为1.1.2,以获取比4.0.6旧的 Spring 版本。

然后 em.save(person) 如预期工作,事务也正常工作(意味着在发生 RuntimeException 时回滚所有的 db 插入)。

但是,即使使用 Spring Data JPA 1.6.0 版本中的 personRepository.save(person) 替换 em.persist(person),在 Spring 4.0.5 + Spring Data JPA 版本下事务仍然不起作用。

看来 Spring Data JPA 存储库正在提交事务。

我漏掉了什么?如何使 Service 层的 @Transactional 注释起作用?

PS:

Maven pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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>com.sivalabs</groupId>
    <artifactId>springboot-data-jpa</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>springboot-data-jpa</name>
    <description>Spring Boot Hello World</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.0.RELEASE</version>
        <relativePath />
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>com.sivalabs.springboot.Application</start-class>
        <java.version>1.7</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <dependencies>
    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>
</project>

Application.java

@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

请发布您的应用程序类和Maven pom。您使用的Spring Boot版本应该使用Spring 4.1.x而不是旧版本,如果列表中有其他版本,则可能存在一些奇怪的依赖项。 - M. Deinum
4
把你的 PersonService 类和你调用的方法都改为 public - M. Deinum
非常感谢Deinum。将它们公开确实解决了问题。我花了几个小时拔光了我的头发 :-( - K. Siva Prasad Reddy
明白了。将其设置为公共方法会在事务上下文中运行,这样我就不会收到那个异常了。 - K. Siva Prasad Reddy
@M.Deinum:将类和方法设为public对我也起作用了。你应该把它写成答案! - CSJ
显示剩余2条评论
3个回答

4
将Transactional注解更改为@Transactional(rollbackFor=Exception.class)。

2
这是Spring的默认行为,无需显式指定。 - K. Siva Prasad Reddy
我遇到了同样的问题。所以我保留了我的异常类,这解决了问题。 - Abhinay
如果是已检查异常,我们需要明确地指定。如果是未检查异常,例如RuntimeException或其任何子类,则Spring会自动回滚事务。 - K. Siva Prasad Reddy
1
在我使用Spring自动配置的EntityManagerFactory进行开发时,将@Transactional添加到存储库是不必要的,但是现在我已经改为创建自己的配置,这个注释是必需的 #SpringVoodoo - Adam

2

来自@m-deinum的评论:

将您的PersonService和您调用的方法都设为public

这对于几个用户似乎起效了。同样的内容也在此答案中提到,引用了手册上的说法:

使用代理时,应仅将@Transactional注释应用于具有公共可见性的方法。 如果您使用@Transactional注解保护、私有或包可见性的方法,则不会引发错误, 但是注释的方法不会显示配置的事务设置。如果需要注释非公共方法,请考虑使用AspectJ(参见下文)。


1
我已经测试了SpringBoot v1.2.0和v1.5.2,一切都按预期工作,您不需要使用实体管理器或@Transactional(rollbackFor=Exception.class)。
我无法看出您哪个部分配置错误,在第一次浏览时似乎一切正常,因此我将发布所有我的配置作为参考,其中包括更新的注释和H2嵌入式内存数据库。

application.properties

# Embedded memory database
spring.jpa.hibernate.ddl-auto=create
spring.datasource.url=jdbc:h2:~/test;AUTO_SERVER=TRUE

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>com.demo</groupId>
    <artifactId>jpaDemo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.2.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
    </dependencies>
</project>

Person.java

@Entity
public class Person
{
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private Integer id;
    private String name;

    public Person() {}
    public Person(String name) {this.name = name;}
    public Integer getId() {return id;}
    public void setId(Integer id) {this.id = id;}
    public String getName() {return name;}
    public void setName(String name) {this.name = name;}
}

PersonRepository.java

@Repository
public interface PersonRepository extends JpaRepository<Person, Integer> {}

PersonService.java

@Service
public class PersonService
{
    @Autowired
    PersonRepository personRepository;

    @Transactional
    public void saveTransactional(List<Person> persons){
        savePersons(persons);
    }

    public void saveNonTransactional(List<Person> persons){
        savePersons(persons);
    }

    private void savePersons(List<Person> persons){
        for (Person person : persons) {
            if("xxx".equals(person.getName())){
                throw new RuntimeException("boooom!!!");
            }
            personRepository.save(person);
        }
    }
}

Application.java

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

PersonServiceTest.java

@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonServiceTest {

    @Autowired
    PersonService personService;

    @Autowired
    PersonRepository personRepository;

    @Test
    public void person_table_size_1() {
        List<Person> persons = getPersons();
        try {personService.saveNonTransactional(persons);}
        catch (RuntimeException e) {}

        List<Person> personsOnDB = personRepository.findAll();
        assertEquals(1, personsOnDB.size());
    }

    @Test
    public void person_table_size_0() {
        List<Person> persons = getPersons();
        try {personService.saveTransactional(persons);}
        catch (RuntimeException e) {}

        List<Person> personsOnDB = personRepository.findAll();
        assertEquals(0, personsOnDB.size());
    }

    public List<Person> getPersons(){
        List<Person> persons = new ArrayList() {{
            add(new Person("aaa"));
            add(new Person("xxx"));
            add(new Person("sss"));
        }};

        return persons;
    }
}

每次测试都会清除并重新初始化数据库,以便我们始终根据已知状态进行验证。


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