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:
- Spring @Transaction method call by the method within the same class, does not work?
- Does Spring @Transactional attribute work on a private method?
- Spring Transaction Doesn't Rollback
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.