Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

@Transactional service methods rollback hibernate changes

I have a method inside a @Service class which calls two different methods in two different @Service classes. These two different methods save two entities inside the database (through hibernate) and they both may throw some exceptions. I would like that if an exception is thrown, independently from which @Service method, all the changes are rolled back. So all the entities created inside the database are deleted.

//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>{
}

I would like that methodA of ClassA, once an exception has been thrown from either methodB or methodC, it rollbacks all the changes inside the database. But it doesn't do that. All the changes remains after the exception... What am I missing? What should I add in order to make it work as I want? I'm using Spring Boot 2.0.6! I haven't configured anything in particular to make the transactions work!


EDIT 1

This is my main class if it can help:

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

When an exception is thrown this is what I see in the console:

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

It seems that each time it calls a method it creates a new transaction! Is without rolling back anything after RuntimeException occurs!


EDIT 2

This is the pom.xml dependencies file:

<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>

This is the application.properties file:

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

SOLUTION

Thanks to @M.Deinum I found the solution!

I was using a wrong database engine (MyISAM), which does not support transaction! So I changed the table engine type with "InnoDB" which supports transactions. What I did is this:

  1. I added this property inside application.properties file, inorder to tell to JPA which was the engine type it should use to "manipulate" the tables:

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

  1. I dropped all the existing tables (with the wrong engine type) inside my DB and I let JPA to recreate all of them with the right engine (InnoDB).

Now all the RuntimeExceptions thrown make the transaction to rollback all the changes done within it.

ALERT: I noticed that if an exception which is not a subclass of RuntimeException is thrown, no rollback is applied and all the changes already done remain inside the database.

like image 756
Stefano Sambruna Avatar asked May 18 '19 18:05

Stefano Sambruna


Video Answer


1 Answers

What you are trying to achieve should work out of the box. Check your spring configuration.

Make sure you created TransactionManager bean and make sure you placed @EnableTransactionManagement annotation on some of your spring @Configurations. This annotation are responsible for registering the necessary Spring components that power annotation-driven transaction management, such as the TransactionInterceptor and the proxy- or AspectJ-based advice that weave the interceptor into the call stack when @Transactional methods are invoked.

See the linked documentation.

If you are using spring-boot it should automatically add this annotation for you if you have PlatformTransactionManager class on classpath.

Also, please note that checked exceptions does not trigger a rollback of the transaction. Only runtime exceptions and errors trigger a rollback. You can, of course, configure this behavior with the rollbackFor and noRollbackFor annotation parameters.

Edit

As you clarified that you are using spring-boot, the answer is: all should work without any configuration.

Here is minimal 100% working example for spring-boot version 2.1.3.RELEASE (but should work with any version ofc):

Dependencies:

    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

User entity:

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

Book entity:

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

User repository:

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

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

Book repository:

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

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

User service:

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

Book service:

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

Composite service:

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

If you run the main method you should see that no books or users found in DB. The transaction is rolled back. If you remove the throw new RuntimeException("User not saved") line from UserService, both entities will be saved fine.

Also you should see the logs of org.springframework.transaction package on TRACE level, where for instance you will see:

Getting transaction for [demo.jpa.CompositeService.saveUserAndBook]

And then after exception is thrown:

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

Here No relevant rollback rule found: applying default rules means that rules defined by DefaultTransactionAttribute will be applied to determine if transaction should be rolled back. And these rules are:

Rolls back on runtime, but not checked, exceptions by default.

RuntimeException is runtime exception, so the transaction will be rolled back.

The line Clearing transaction synchronization is where rollback is actually applied. You will see some other Applying rules to determine whether transaction should rollback messages because @Transactional methods are nested here (UserService.saveUser called from CompositeService.saveUserAndBook and both methods are @Transactional), but all they do is determine rules for future actions (at the point of transaction synchronization). The actual rollback will be done only once, at the outermost @Transactional method exit.

like image 135
Ruslan Stelmachenko Avatar answered Oct 17 '22 01:10

Ruslan Stelmachenko