Reputation: 25770
In my Spring Boot project I have implemented following service method:
@Transactional
public boolean validateBoard(Board board) {
boolean result = false;
if (inProgress(board)) {
if (!canPlayWithCurrentBoard(board)) {
update(board, new Date(), Board.AFK);
throw new InvalidStateException(ErrorMessage.BOARD_TIMEOUT_REACHED);
}
if (!canSelectCards(board)) {
update(board, new Date(), Board.COMPLETED);
throw new InvalidStateException(ErrorMessage.ALL_BOARD_CARDS_ALREADY_SELECTED);
}
result = true;
}
return result;
}
Inside this method I use another service method which is called update
:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Board update(Board board, Date finishedDate, Integer status) {
board.setStatus(status);
board.setFinishedDate(finishedDate);
return boardRepository.save(board);
}
I need to commit changes to database in update
method independently of the owner transaction which is started in validateBoard
method. Right now any changes is rolling back in case of any exception.
Even with @Transactional(propagation = Propagation.REQUIRES_NEW)
it doesn't work.
How to correctly do this with Spring and allow nested transactions ?
Upvotes: 46
Views: 85009
Reputation: 220
You could create a new service (CustomTransactionalService) that will run your code in a new transaction :
@Service
public class CustomTransactionalService {
@Transactional(propagation= Propagation.REQUIRES_NEW)
public <U> U runInNewTransaction(final Supplier<U> supplier) {
return supplier.get();
}
@Transactional(propagation= Propagation.REQUIRES_NEW)
public void runInNewTransaction(final Runnable runnable) {
runnable.run();
}
}
And then :
@Service
public class YourService {
@Autowired
private CustomTransactionalService customTransactionalService;
@Transactional
public boolean validateBoard(Board board) {
// ...
}
public Board update(Board board, Date finishedDate, Integer status) {
this.customTransactionalService.runInNewTransaction(() -> {
// ...
});
}
}
Upvotes: 1
Reputation: 640
Using "self" inject pattern you can resolve this issue.
sample code like below:
@Service @Transactional
public class YourService {
//... your member
@Autowired
private YourService self; //inject proxy as an instance member variable ;
@Transactional(propagation= Propagation.REQUIRES_NEW)
public void methodFoo() {
//...
}
public void methodBar() {
//call self.methodFoo() rather than this.methodFoo()
self.methodFoo();
}
}
The point is using "self" rather than "this".
Upvotes: 9
Reputation: 1640
This documentation covers your problem - https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/data-access.html#transaction-declarative-annotations
In proxy mode (which is the default), only external method calls coming in through the proxy are intercepted. This means that self-invocation, in effect, a method within the target object calling another method of the target object, will not lead to an actual transaction at runtime even if the invoked method is marked with @Transactional. Also, the proxy must be fully initialized to provide the expected behaviour so you should not rely on this feature in your initialization code, i.e. @PostConstruct.
However, there is an option to switch to AspectJ mode
Upvotes: 49
Reputation: 211
The basic thumb rule in terms of nested Transactions is that they are completely dependent on the underlying database, i.e. support for Nested Transactions and their handling is database dependent and varies with it. In some databases, changes made by the nested transaction are not seen by the 'host' transaction until the nested transaction is committed. This can be achieved using Transaction isolation in @Transactional (isolation = "")
You need to identify the place in your code from where an exception is thrown, i.e. from the parent method: "validateBoard" or from the child method: "update".
Your code snippet shows that you are explicitly throwing the exceptions.
YOU MUST KNOW::
In its default configuration, Spring Framework’s transaction infrastructure code only marks a transaction for rollback in the case of runtime, unchecked exceptions; that is when the thrown exception is an instance or subclass of RuntimeException.
But @Transactional never rolls back a transaction for any checked exception.
Thus, Spring allows you to define
Try annotating your child method: update with @Transactional(no-rollback-for="ExceptionName") or your parent method.
Upvotes: 6
Reputation: 5407
Your problem is a method's call from another method inside the same proxy.It's self-invocation. In your case, you can easily fix it without moving a method inside another service (why do you need to create another service just for moving some method from one service to another just for avoid self-invocation?), just to call the second method not directly from current class, but from spring container. In this case you call proxy second method with transaction not with self-invocatio.
This principle is useful for any proxy-object when you need self-invocation, not only a transactional proxy.
@Service
class SomeService ..... {
-->> @Autorired
-->> private ApplicationContext context;
-->> //or with implementing ApplicationContextAware
@Transactional(any propagation , it's not important in this case)
public boolean methodOne(SomeObject object) {
.......
-->> here you get a proxy from context and call a method from this proxy
-->>context.getBean(SomeService.class).
methodTwo(object);
......
}
@Transactional(any propagation , it's not important in this case)public boolean
methodTwo(SomeObject object) {
.......
}
}
when you do call context.getBean(SomeService.class).methodTwo(object);
container returns proxy object and on this proxy you can call methodTwo(...)
with transaction.
Upvotes: 6
Reputation: 9102
Your transaction annotation in update
method will not be regarded by Spring transaction infrastructure if called from some method of same class. For more understanding on how Spring transaction infrastructure works please refer to this.
Upvotes: 2