@Transactional服务方法回滚Hibernate更改

6
我有一个@服务类中的方法,调用两个不同的@服务类中的方法。这两种不同的方法通过Hibernate在数据库中保存了两个实体,并且它们都可能抛出一些异常。 如果抛出异常,我希望无论是哪个@Service方法,所有更改都会回滚。因此,删除在数据库中创建的所有实体。
//entities
@Entity
public class ObjectB{
   @Id
   private long id;
   ...
}

@Entity
public class ObjectC{
   @Id
   private long id;
   ...
}



//servicies
@Service
@Transactional
public class ClassA{

   @Autowired
   private ClassB classB;

   @Autowired
   private ClassC classC;

   public void methodA(){
      classB.insertB(new ObjectB());
      classC.insertC(new ObjectC());
   }
}

@Service
@Transactional
public class ClassB{

   @Autowired
   private RepositoryB repositoryB;

   public void insertB(ObjectB b){
      repositoryB.save(b);
   }
}

@Service
@Transactional
public class ClassC{

   @Autowired
   private RepositoryC repositoryC;

   public void insertC(ObjectC c){
      repositoryC.save(c);
   }
}


//repositories
@Repository
public interface RepositoryB extends CrudRepository<ObjectB, String>{
}

@Repository
public interface RepositoryC extends CrudRepository<ObjectC, String>{
}

我希望ClassA的methodA在methodB或methodC抛出异常后,可以回滚数据库中所有更改。但实际上它并没有这样做,所有的更改都保留了下来。我错过了什么?我应该添加什么才能让它按照我的要求工作?我正在使用Spring Boot 2.0.6!我没有特别配置事务处理! 编辑1 如果有帮助的话,这是我的主类:
@SpringBootApplication
public class JobWebappApplication extends SpringBootServletInitializer {


    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(JobWebappApplication.class);
    }

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

当出现异常时,我在控制台中看到的内容如下:
Completing transaction for [com.example.ClassB.insertB]
Retrieved value [org.springframework.orm.jpa.EntityManagerHolder@31d4fbf4] for key [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@df9d400] bound to thread [http-nio-8080-exec-7]
Retrieved value [org.springframework.jdbc.datasource.ConnectionHolder@1d1ad46b] for key [HikariDataSource (HikariPool-1)] bound to thread [http-nio-8080-exec-7]
Getting transaction for [com.example.ClassC.insertC]
Completing transaction for [com.example.ClassC.insertC] after exception: java.lang.RuntimeException: runtime exception!
Applying rules to determine whether transaction should rollback on java.lang.RuntimeException: runtime exception!
Winning rollback rule is: null
No relevant rollback rule found: applying default rules
Completing transaction for [com.example.ClassA.methodA] after exception: java.lang.RuntimeException: runtime exception!
Applying rules to determine whether transaction should rollback on java.lang.RuntimeException: runtime exception!
Winning rollback rule is: null
No relevant rollback rule found: applying default rules
Clearing transaction synchronization
Removed value [org.springframework.jdbc.datasource.ConnectionHolder@1d1ad46b] for key [HikariDataSource (HikariPool-1)] from thread [http-nio-8080-exec-7]
Removed value [org.springframework.orm.jpa.EntityManagerHolder@31d4fbf4] for key [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@df9d400] from thread [http-nio-8080-exec-7]
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: runtime exception!] with root cause

看起来每次调用方法都会创建一个新的事务!在RuntimeException发生后没有回滚任何东西!


编辑2

这是pom.xml依赖文件:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.10.RELEASE</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.5</version>
        </dependency>
</dependencies>

这是 application.properties 文件:

spring.datasource.url=jdbc:mysql://localhost:3306/exampleDB?useSSL=false
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.show-sql=true
logging.level.org.springframework.transaction=TRACE
spring.jpa.database=MYSQL
spring.jpa.hibernate.ddl-auto=update
spring.datasource.driver.class=com.mysql.jdbc.Driver  
spring.jpa.properties.hibernate.locationId.new_generator_mappings=false

解决方法

感谢 @M.Deinum,我找到了解决方法!

我使用了错误的数据库引擎(MyISAM),它不支持事务!所以我改变了表格的引擎类型为"InnoDB",它支持事务。我的做法如下:

  1. 我在application.properties文件中添加了此属性,告诉JPA应该使用哪种引擎类型来“操作”表格:

spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect

  1. 我删除了数据库中所有现有的表(使用了错误的引擎类型),并让JPA重新创建这些表格,使用正确的引擎类型(InnoDB)。

现在,所有抛出的RuntimeException都会使事务回滚到其中所做的所有更改。

注意:我发现如果抛出的异常不是RuntimeException的子类,则不会应用回滚,已经完成的所有更改仍将保留在数据库中。


3
请确保 @Transactionalorg.springframework.transaction.annotation.Transactional 而不是 javax.transaction.Transactional - Ken Chan
@KenChan 多谢啦!你说得对!我引入的都错了... 现在我要改过来并且再次测试! - Stefano Sambruna
1
@Stefano。谢谢。如果它工作了,请告诉我。如果它工作,请问我是否可以将我的评论更改为答案并给予投票支持? - Ken Chan
2
移除 spring-test 依赖项(该依赖项已包含在 spring-boot-starter-test 依赖项中)。您正在使用 MYSQL,请确保您正在使用带有 InnoDB 表而不是 MyISAM 表的 MySQL。后者不支持事务。 - M. Deinum
是的,这是预期的行为,这种行为也来自EJB。这也被记录为预期的行为,以及您可以如何修改它。 - M. Deinum
显示剩余6条评论
3个回答

6

您所尝试实现的功能应该开箱即用。请检查您的Spring配置。

确保您已创建TransactionManager bean,并确保您在一些Spring @Configuration上放置了@EnableTransactionManagement注释。此注释负责注册必要的Spring组件,以支持基于注释的事务管理,例如TransactionInterceptor和代理或基于AspectJ的建议,在调用堆栈中编织拦截器以响应调用@Transactional方法。

请参阅链接的文档。

如果您使用的是spring-boot,则如果您的类路径中有PlatformTransactionManager类,则应自动为您添加此注释。

此外,需要注意的是已检查异常不会触发事务回滚。只有运行时异常和错误会触发回滚。当然,您可以使用rollbackFornoRollbackFor注释参数来配置此行为。 编辑 由于您明确表示正在使用spring-boot,答案是:所有内容都应该在没有任何配置的情况下工作。 这是一个最小化的、完全可行的spring-boot示例,版本为2.1.3.RELEASE(但应适用于任何版本): 依赖项:
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    runtimeOnly('com.h2database:h2') // or any other SQL DB supported by Hibernate
    compileOnly('org.projectlombok:lombok') // for getters, setters, toString

用户实体:
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

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

@Entity
@Getter
@Setter
@ToString
public class User {

    @Id
    @GeneratedValue
    private Integer id;

    private String name;
}

书籍实体:

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

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

@Entity
@Getter
@Setter
@ToString
public class Book {

    @Id
    @GeneratedValue
    private Integer id;

    @ManyToOne
    private User author;

    private String title;
}

用户存储库:
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Integer> {
}

图书存储库:

import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, Integer> {
}

用户服务:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Transactional
@Component
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public User saveUser(User user) {
//        return userRepository.save(user);
        userRepository.save(user);
        throw new RuntimeException("User not saved");
    }

    public List<User> findAll() {
        return userRepository.findAll();
    }
}

预订服务:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Transactional
@Component
public class BookService {

    @Autowired
    private BookRepository bookRepository;

    public Book saveBook(Book book) {
        return bookRepository.save(book);
    }

    public List<Book> findAll() {
        return bookRepository.findAll();
    }
}

组合服务:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@Component
public class CompositeService {

    @Autowired
    private UserService userService;

    @Autowired
    private BookService bookService;

    public void saveUserAndBook() {
        User user = new User();
        user.setName("John Smith");
        user = userService.saveUser(user);

        Book book = new Book();
        book.setAuthor(user);
        book.setTitle("Mr Robot");
        bookService.saveBook(book);
    }
}

主要:
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class JpaMain {

    public static void main(String[] args) {
        new SpringApplicationBuilder(JpaMain.class)
                .web(WebApplicationType.NONE)
                .properties("logging.level.org.springframework.transaction=TRACE")
                .run(args);
    }

    @Bean
    public CommandLineRunner run(CompositeService compositeService, UserService userService, BookService bookService) {
        return args -> {
            try {
                compositeService.saveUserAndBook();
            } catch (RuntimeException e) {
                System.err.println("Exception: " + e);
            }

            System.out.println("All users: " + userService.findAll());
            System.out.println("All books: " + bookService.findAll());
        };
    }
}

如果您运行主方法,您应该看到在数据库中没有找到任何书籍或用户。事务已经回滚了。如果您从UserService中删除throw new RuntimeException("User not saved")这一行代码,则两个实体都将被保存成功。
此外,您应该在TRACE级别上查看org.springframework.transaction包的日志,例如,您将看到:
Getting transaction for [demo.jpa.CompositeService.saveUserAndBook]

然后在抛出异常之后:

Completing transaction for [demo.jpa.CompositeService.saveUserAndBook] after exception: java.lang.RuntimeException: User not saved
Applying rules to determine whether transaction should rollback on java.lang.RuntimeException: User not saved
Winning rollback rule is: null
No relevant rollback rule found: applying default rules
Clearing transaction synchronization

这里的No relevant rollback rule found: applying default rules意味着将应用由DefaultTransactionAttribute定义的规则来确定是否应该回滚事务。这些规则是:

默认情况下,运行时但未经检查的异常会导致回滚。

RuntimeException是运行时异常,因此事务将被回滚。

代码行Clearing transaction synchronization是实际应用回滚的地方。您将看到一些其他的Applying rules to determine whether transaction should rollback消息,因为@Transactional方法在此处嵌套(从CompositeService.saveUserAndBook调用UserService.saveUser,并且两个方法都是@Transactional),但它们所做的所有操作仅确定未来操作的规则(在事务同步点)。实际的回滚只会在最外层的@Transactional方法退出时执行一次。


谢谢您的回答...我已经说明我正在使用Spring Boot!根据您所说,我应该添加什么?只需要PlatformTransactionManager吗?在哪里以及如何添加? - Stefano Sambruna
如果您正在使用 spring-boot,则所有内容都应该可以正常工作,无需任何配置。请确保您已添加了 org.springframework.boot:spring-boot-starter-data-jpa 和您选择的 JDBC 驱动程序(例如 com.h2database:h2)。实际上不需要任何配置。我将更新答案以包括一些最小工作示例。 - Ruslan Stelmachenko
非常感谢,但似乎我正在做完全相同的事情。我更新了我的问题,并添加了控制台在异常抛出后告诉我的内容。 您为什么在业务逻辑类中使用@Component注释而不是@Service? - Stefano Sambruna
你的日志信息表明一切正常。交易应该被回滚。你是如何确定它没有回滚的?你是如何调用ClassA.methodA()方法的?我建议你创建一个新的项目,采用最小化的设置(例如使用我的答案中的代码),确保它能够工作,然后逐步将其修改为你自己的直到它停止工作。我无法猜测你的代码还可能有什么问题,因为它看起来很好。关于@Component@Service - 它们是可以互换的。 - Ruslan Stelmachenko
数据库中保留了所有创建的信息!我正在从控制器调用methodA方法! - Stefano Sambruna

0

自 Spring 3.1 开始,如果您在类路径上使用 spring-data-* 或 spring-tx 依赖项,则事务管理将默认启用。

https://www.baeldung.com/transaction-configuration-with-jpa-and-spring

但是检查Spring的Transactional注释,我们可以看到如果抛出的异常不是RuntimeException的扩展,那么您需要通知参数rollbackFor。

/**
 * Defines zero (0) or more exception {@link Class classes}, which must be
 * subclasses of {@link Throwable}, indicating which exception types must cause
 * a transaction rollback.
 * <p>By default, a transaction will be rolling back on {@link RuntimeException}
 * and {@link Error} but not on checked exceptions (business exceptions). See
 * {@link org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)}
 * for a detailed explanation.
 * <p>This is the preferred way to construct a rollback rule (in contrast to
 * {@link #rollbackForClassName}), matching the exception class and its subclasses.
 * <p>Similar to {@link org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(Class clazz)}.
 * @see #rollbackForClassName
 * @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)
 */
Class<? extends Throwable>[] rollbackFor() default {};

一个简单的@Transactional(rollbackFor = Exception.class)应该可以解决问题


-2
你在这里尝试实现的事情是不可能的,因为一旦你执行完方法并退出后,由于你使用了 @Transactional 注解,所以无法撤销更改。
作为替代方案,你可以将自动提交设置为 false,并在 A 类的 methodA 中编写 try catch 块。如果没有异常,则提交数据库事务,否则不提交。

由于顶层的@Transactional,所有事务方法都包含在单个事务中。 - M. Deinum

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