Home > Blockchain >  Why transactional does not rollback when RuntimeException occur?
Why transactional does not rollback when RuntimeException occur?

Time:03-27

I want to test a non-transactional method in the service layer when inner methods are transactional separately. I want to know what will happen if some method throws an exception. Does it properly roll back or not? I also tried rollbackFor but it did not help. Any suggestions?

  • Spring v-2.5.1

app.properties

spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=root

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto=create-drop

server.error.include-message=always
server.error.include-binding-errors=always

Controller

@PostMapping("/test")
    public void test(){
        service.test();
    }

Service

@Service
public class UserService {
  @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;


    public void test() {
        User user1 = new User();
        String encodedPassword = passwordEncoder.encode("123");
        user1.setUsername("username");
        user1.setPassword(encodedPassword);
        user1.setFirst_name("name1");
        user1.setLast_name("family1");
        save(user1);
        User update = update(user1.getUsername());
        throw new RuntimeException();// I expect Spring rollback all data for save and update methods
//but it seems data has been committed before an exception occur.
    }

 @Transactional
    public void save(User user) {
        if (userExists(user.getUsername()))
            throw new ApiRequestException("Username has already taken!");
        else {
            User user1 = new User();
            String encodedPassword = passwordEncoder.encode(user.getPassword());
            user1.setUsername(user.getUsername());
            user1.setPassword(encodedPassword);
            user1.setFirst_name(user.getFirst_name());
            user1.setLast_name(user.getLast_name());
            user1.setCreate_date(user.getCreate_date());
            user1.setModified_date(user.getModified_date());
            user1.setCompany(user.getCompany());
            user1.setAddresses(user.getAddresses());
            userRepository.save(user1);
          //  throw new RuntimeException(); Similarly I expect rollback here.
        }
    }

    @Transactional
    public User update(String username) {
        User userFromDb = userRepository.findByUsername(username);
        userFromDb.setFirst_name("new username");
        userFromDb.setLast_name("new lastname");
        return userRepository.save(userFromDb);
    }

}

CodePudding user response:

Due to way spring proxying works by default, you can not call a "proxied" method from within the instance.

Consider that when you put @Transactional annotation on a method, Spring makes a proxy of that class and the proxy is where the transaction begin/commit/rollback gets handled. The proxy calls the actual class instance. Spring hides this from you.

But given that, if you have a method on the class instance (test()), that calls another method on itself (this.save()), that call doesn't goes through the proxy, and so there is no "@Transactional" proxy. There is nothing to do the rollback when the RuntimeException occurs.

There are ways to change the how Spring does the proxying which would allow this to work, but it has changed over the years. There are various alternatives. One way is to create create separate classes. Perhaps UserServiceHelper. The UserServiceHelper contains @Transactional methods that get called by UserService. This same issue occurs when different @Transactional isolations and propagations are needed.

Related answers and info:

Your example code is not very clear as to what you are trying to do. Often, @Transactional would be put on the service class and apply to all public methods (like test()).

CodePudding user response:

To begin with, I'm not sure if you understand what a transaction represents. If there's a unit of work that contains several functionalities to be executed, and you either want them to completely fail or completely pass - that's when you use a transaction.

So in the case of test() method - why should underlying mechanism (in this case Hibernate) care about rolling back something that happened within save() and update() when the test() itself isn't a transaction?

Sorry for asking more than answering, but as far as I can see - you need to annotate test() with the annotation, and not the individual methods. Or you can annotate them all.

  • Related