Force a transactional rollback without encountering an exception?
Asked Answered
R

9

57

I have a method that does a bunch of things; amongst them doing a number of inserts and updates.

It's declared thusly:

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false)
public int saveAll() {
 //do stuff;
}

It works exactly as it is supposed to and I have no problems with it. There are situations however when I want to force the rollback in spite of there not being an exception... at the moment, I'm forcing an exception when I encounter the right conditions, but it's ugly and I don't like it.

Can I actively call the rollback somehow?

The exception calls it... I'm thinking maybe I can too.

Renounce answered 7/5, 2009 at 0:11 Comment(1)
check static.springsource.org/spring/docs/2.0.x/reference/… section 9.5.3Kimon
N
36

In Spring Transactions, you use TransactionStatus.setRollbackOnly().

The problem you have here is that you're using @Transactional to demarcate your transactions. This has the benefit of being non-invasive, but it also means that if you want to manually interact with the transaction context, you can't.

If you want tight control of your transaction status, you have to use programmatic transactions rather than declarative annotations. This means using Spring's TransactionTemplate, or use its PlatformTransactionManager directly. See section 9.6 of the Spring reference manual.

With TransactionTemplate, you provide a callback object which implements TransactionCallback, and the code in this callback has access to the TransactionStatus objects.

It's not as nice as @Transactional, but you get closer control of your tx status.

Nappie answered 7/5, 2009 at 15:36 Comment(1)
Would using TransactionInterceptor.currentTransactionStatus().setRollbackOnly() be a way of manually interacting with the transaction in the middle of a method that uses @Transactional ? Mainly for replacing the setRollbackOnly EJB methods. Would be interested if you have a comment on that. Thanks!Lacrimatory
A
36

This works for me:

TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
Abreaction answered 6/5, 2014 at 18:27 Comment(0)
U
19

We don't use EJB, but simple Spring and we have chosen AOP approach. We've implemented new annotation @TransactionalWithRollback and used AOP to wrap those annotated methods with "around" advice. To implement the advice we use mentioned TransactionTemplate. This means a little work at the beginning, but as a result we can just annotate a method with @TransactionalWithRollback like we use @Transactional in other cases. The main code looks clean and simple.

//
// Service class - looks nice
//
class MyServiceImpl implements MyService {
    @TransactionalWithRollback
    public int serviceMethod {
        // DO "read only" WORK
    }
}

//
// Annotation definition
//
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface TransactionalWithRollback {
}

//
// the around advice implementation
//
public class TransactionalWithRollbackInterceptor {
    private TransactionTemplate txTemplate;
    @Autowired private void setTransactionManager(PlatformTransactionManager txMan) {
        txTemplate = new TransactionTemplate(txMan);
    }

    public Object doInTransactionWithRollback(final ProceedingJoinPoint pjp) throws Throwable {
        return txTemplate.execute(new TransactionCallback<Object>() {
            @Override public Object doInTransaction(TransactionStatus status) {
                status.setRollbackOnly();
                try {
                    return pjp.proceed();
                } catch(RuntimeException e) {
                    throw e;
                } catch (Throwable e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }
}

//
// snippet from applicationContext.xml:
//
<bean id="txWithRollbackInterceptor" class="net.gmc.planner.aop.TransactionalWithRollbackInterceptor" />

<aop:config>
    <aop:aspect id="txWithRollbackAspect" ref="txWithRollbackInterceptor">
        <aop:pointcut 
            id="servicesWithTxWithRollbackAnnotation" 
            expression="execution( * org.projectx..*.*(..) ) and @annotation(org.projectx.aop.TransactionalWithRollback)"/>
        <aop:around method="doInTransactionWithRollback" pointcut-ref="servicesWithTxWithRollbackAnnotation"/>
    </aop:aspect>
</aop:config>
Ulrikeulster answered 12/3, 2013 at 10:41 Comment(3)
+1 for taking the time to answer a question that I asked 4 years ago and which already has a multiple upvoted accepted answer! :)Renounce
I like things to be complete :-)Ulrikeulster
For this case, you should probably use @Transactional(readOnly=true)Regurgitate
O
17

Call setRollbackOnly() on the SessionContext if you're in an EJB.

You can inject SessionContext like so:

public MyClass {
    @Resource
    private SessionContext sessionContext;

    @Transactional(propagation = Propagation.REQUIRED, 
                   isolation = Isolation.DEFAULT, 
                   readOnly = false)
    public int saveAll(){
        //do stuff;
        if(oops == true) {
             sessionContext.setRollbackOnly();
             return;
        }
    }

setRollbackOnly() is a member of EJBContext. SessionContext extends EJBContext: http://java.sun.com/j2ee/1.4/docs/api/javax/ejb/SessionContext.html Note it's only available in session EJBs.

@Resource is a standard Java EE annotation, so you should probably check your setup in Eclipse. Here's an example of how to inject the SessionContext using @Resource.

I suspect that this is probably not your solution, since it seems like you may not be working with EJBs -- explaining why Eclipse is not finding @Resource.

If that's the case, then you will need to interact with the transaction directly -- see transaction template.

Orthopedic answered 7/5, 2009 at 0:18 Comment(1)
Eclipse doesn't recognize @Resource as an annotation, and setRollbackOnly() is not a member of SessionContext (when it imports from org.springframework.orm.hibernate3.SpringSessionContext). So... I'm closer, but not close enough :)Renounce
M
5

You should have spring inject the transactional manager. Then you can just call the rollback method on it.

Mustard answered 7/5, 2009 at 0:41 Comment(2)
that does seem like the right way to go... I'm investigating SavepointManager now.Renounce
this is causing an exception to be thrown at the end "Transaction rolled back because it has been marked as rollback-only"Gig
S
2

I have service methods annotated with @Transactional. When the validation fails, and I already have an entity attached to the current unit of work, I use sessionFactory.getCurrentSession().evict(entity) to make sure nothing is written to the database. That way I don't need to throw an exception.

Spanking answered 17/11, 2011 at 22:14 Comment(1)
This might be a compromise solution in a long session.Governance
E
1

At the moment, I'm forcing an exception when I encounter the right conditions, but it's ugly and I don't like it.

Why is it ugly? I would argue otherwise. You have your @Transactional on your public method, with rollbackFor property like so:

@Transactional(rollbackFor = Exception.class)
public void myMethod() throws IllegalStateException {

and then if your method goes south, you throw a standard Java exception, probably:

throw new IllegalStateException("XXX");

This is beautiful, you use an existing standard Java exception using only a 1-liner, the framework does the rollback and that's it.

Effectual answered 4/12, 2021 at 14:48 Comment(2)
Exceptions are expensive to throw from a performance standpoint.Dit
True, the framework has to create the exception object involving capturing the stacktrace and look for the handler. However, because it is an exception, the frequency should be rare. The benefit should outweigh the performance costEffectual
D
0

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.

Dit answered 11/6 at 8:28 Comment(0)
T
-3

Throw an exception and use the framework as designed otherwise do not use declarative transaction management and follow skaffman advise above. Keep it simple.

Trehalose answered 22/6, 2009 at 13:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.