@ Транзакционные сервисные методы отката изменений гибернации - PullRequest
1 голос
/ 18 мая 2019

У меня есть метод внутри класса @Service, который вызывает два разных метода в двух разных классах @Service. Эти два разных метода сохраняют две сущности в базе данных (через 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>{
}

Мне бы хотелось, чтобы этот метод A класса ClassA, после исключения из метода 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 делают транзакцию для отката всех изменений, выполненных в ней.

ALERT: я заметил, что если выдается исключение, которое не является подклассом RuntimeException, откат не применяется, и все уже сделанные изменения остаются в базе данных.

Ответы [ 2 ]

4 голосов
/ 19 мая 2019

То, что вы пытаетесь достичь, должно работать из коробки. Проверьте конфигурацию пружины.

Убедитесь, что вы создали TransactionManager боб и убедитесь, что вы поместили аннотацию @EnableTransactionManagement на некоторые из ваших пружин @Configuration s. Эта аннотация отвечает за регистрацию необходимых компонентов Spring, которые обеспечивают управление транзакциями на основе аннотаций, таких как TransactionInterceptor и рекомендации на основе прокси или AspectJ, которые вплетают перехватчик в стек вызовов при вызове методов @Transactional.

См. Связанную документацию.

Если вы используете spring-boot, он должен автоматически добавить эту аннотацию для вас, если у вас есть класс PlatformTransactionManager на пути к классам.

Также обратите внимание, что отмеченные исключения не вызывают откат транзакции . Только исключения и ошибки времени выполнения вызывают откат. Разумеется, вы можете настроить это поведение с помощью параметров аннотаций rollbackFor и noRollbackFor.

Редактировать

Поскольку вы пояснили, что используете spring-boot, ответ таков: все должно работать без какой-либо конфигурации.

Вот минимальный 100% рабочий пример для весенней загрузки 2.1.3.RELEASE (но должен работать с любой версией 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);
    }
}

Main:

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

Если вы запустите основной метод, вы должны увидеть, что в БД нет книг или пользователей. Сделка откатывается. Если вы удалите строку throw new RuntimeException("User not saved") из UserService, оба объекта будут сохранены в порядке.

Также вы должны увидеть журналы пакета org.springframework.transaction на уровне TRACE, где, например, вы увидите:

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 методы (UserService.saveUser вызывается из CompositeService.saveUserAndBook и оба метода @Transactional), но все, что они делают, это определяют правила для будущих действий (в момент синхронизация транзакций). Фактический откат будет выполнен только один раз, при выходе из метода @Transactional.

0 голосов
/ 18 мая 2019

То, чего вы пытаетесь достичь, невозможно, как только вы выйдете из метода после его выполнения; изменения не могут быть отменены, так как у вас есть аннотация @Transactional.

В качестве альтернативы вы можете установить автоматическую фиксацию false и написать блок try catch в методе A класса A. И если нет исключений, совершите транзакцию БД, или же нет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...