簡體   English   中英

@Transactional 服務方法回滾休眠更改

[英]@Transactional service methods rollback hibernate changes

我在 @Service 類中有一個方法,它在兩個不同的 @Service 類中調用兩個不同的方法。 這兩種不同的方法在數據庫中保存兩個實體(通過休眠),它們都可能拋出一些異常。 我希望如果拋出異常,獨立於哪個 @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)重新創建所有表。

現在拋出的所有 RuntimeExceptions 使事務回滾其中所做的所有更改。

警告:我注意到,如果拋出不是 RuntimeException 子類的異常,則不會應用回滾,並且所有已完成的更改都保留在數據庫中。

您要實現的目標應該是開箱即用的。 檢查您的彈簧配置。

確保您創建了TransactionManager bean 並確保您在某些 spring @Configuration上放置了@EnableTransactionManagement注釋。 該注解負責注冊必要的 Spring 組件,這些組件為注解驅動的事務管理提供支持,例如TransactionInterceptor和基於代理或基於 AspectJ 的建議,它們在調用@Transactional方法時將攔截器編織到調用堆棧中。

請參閱鏈接的文檔。

如果您使用的是spring-boot ,如果您在類路徑上有PlatformTransactionManager類,它應該會自動為您添加此注釋。

另外,請注意檢查異常不會觸發事務的回滾 只有運行時異常和錯誤才會觸發回滾。 當然,您可以使用rollbackFornoRollbackFor注釋參數配置此行為。

編輯

當您澄清您正在使用 spring-boot 時,答案是:一切都應該在沒有任何配置的情況下工作。

這是 spring-boot 版本2.1.3.RELEASE最小 100% 工作示例(但應該適用於任何版本的 ofc):

依賴項:

    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());
        };
    }
}

如果您運行 main 方法,您應該看到在 DB 中沒有找到任何書籍或用戶。 事務回滾。 如果您從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 3.1 開始,如果您在類路徑上使用 spring-data-* 或 spring-tx 依賴項,則默認情況下將啟用事務管理。

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

但是檢查 Springs 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)應該可以工作

您在這里嘗試實現的事情是不可能的,因為一旦您在執行后退出該方法; 由於您有 @Transactional 注釋,因此無法還原更改。

或者,您可以將自動提交設置為 false,並在 A 類的方法 A 中編寫一個 try catch 塊。如果沒有異常,則提交 DB 事務,否則不提交。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM