My situation is not exactly the same as yours, but you might find my solution beneficial, as I've faced the same problem as you and did not want to explicitly call TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
many times throughout my whole codebase.
I'm using a Result<>
class with a isOK()
method; upon returning, the transaction should be rolled back if isOK() == false
. This allows me to do some processing on the data I fetch, and only later return if something is off. It might not be the cleanest solution architecture-wise, but it spares me multiple passes on the data and thus quite some performance time. The Result<>
is a wrapper, so I can still return things from my methods if everything goes well.
I used AOP to solve this problem as follows:
First, I created the following annotation, which implements @Transactional:
package com.foo.bar;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Transactional(rollbackFor = Exception.class)
public @interface TransactionalWithResultRollback {}
then, a corresponding advice class:
@Slf4j
@Aspect
@Component
@ConditionalOnExpression("${aspect.enabled:true}")
public class TransactionAdvice {
@Around("@annotation(com.foo.bar.TransactionalWithResultRollback)")
public Object rollbackOnFailedResult(final ProceedingJoinPoint point) throws Throwable {
Object proceed = point.proceed();
if (proceed instanceof Result<?>result && !result.isOk()) {
try {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
} catch (Exception e) {
log.debug("Exception rolling back transaction on result {}:", result);
}
}
return proceed;
}
}
Finally, instead of annotating methods with @Transactional
, I now use @TransactionalWithResultRollback
:
@TransactionalWithResultRollback
public Result<Void> persist(final Entity entity) {...}
I can thus avoid throwing Exceptions unless absolutely necessary.