Reputation: 797
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:
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
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.
Upvotes: 6
Views: 8510
Reputation: 1198
Since spring 3.1 if you're using spring-data-* or spring-tx dependencies on the classpath, then transaction management will be enabled by default.
https://www.baeldung.com/transaction-configuration-with-jpa-and-spring
But checking Springs Transactional annotation we can see that you'll need to inform the parameter rollbackFor if exception thown isn't a extension of RuntimeException.
/**
* 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 {};
A simple @Transactional(rollbackFor = Exception.class) should work
Upvotes: 0
Reputation: 5420
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 @Configuration
s. 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.
Upvotes: 6
Reputation: 1
The thing that you are trying to achieve here is not possible, as once you come out of the method after executing it; changes can not be reverted as you have @Transactional annotation.
Alternatively you could set auto commit false, and write a try catch block in methodA of class A. And if there is no exception commit the DB transaction, or else don't.
Upvotes: -2