Tuesday, November 13, 2007

Spring Transactions

Spring offers the capability to manage DB transactions outside of the application container. We recently came across this need with Hibernate. Since we are using Java 5 we went with annotations. After some fumbling around trying to understand what was happening, we got something working. Then we tried to push it further.

We were trying to solve the situation where you have two transactions in a wrapper method. The two transactions should be independent. An exception in the second should not cause a roll back in the first. (This was a hypothetical situation I think. It was brought up by another programmer). So here's what we had:

class Service {
public wrapperMethod() {
txMethodA();
txMethodB();
}
@Transactional
public txMethodA(){ ... do some db inserts. commit. }
@Transactional
public txMethodB(){ ... do some db inserts. throw exception. commit. }
}
class Action {
public execute() {
service.wrapperMethod();
}
}

As a test we would sometimes throw an exception between the two methods inside the wrapper. We were also using Spring's OpenSessionInView filter.

What we immediately found it this situation was that because of the OSV filter setting readOnly to true that we couldn't use txMethodA to insert with out a transaction on the wrapperMethod. So we put a transaction on the wrapperMethod. This led to the exception rolling all inserts back. So it seemed that the transactional annotation was being ignored.
We then tried changing the propagation on txMethodA and txMethodB to REQUIRES_NEW to try to create a new transaction for each method. This too was ignored. Anything we tried would not result in the commit of one and and the rollback of the other.

Well, after some reading (duh) from the Spring docs, namely chapter 9 it dawned on me.

This maybe obvious to some people, but I run up against this once in a while and it doesn't seem intuitive to me so I forget how it works all the time. Take this example:

class A{
public doSomething() {
doSomethingElse();
}

public doSomethingElse() {
println "Class A";
}
}

class B extends A {
public doSomethingElse() {
println "Class B";
}
}

B b = new B();
b.doSomethingElse(); //-> Class B

Turns out it actually works like I expect, i.e. prints "Class B". I was thinking that this would print "Class A". This was disheartening because I thought I figured out what was happening.

Turns out I was close but not quite there. I read the section on proxies which is what Spring creates for each transactional class, even if the transaction is only on one method. It is the proxy class/object that gets wired in to the action, not the actual session. This is important be cause, as the page says, self references in the session object will not go through the proxy.

I guess the proxy can be though of as using the decorator pattern, but doesn't add anything to it. So what was happening?

Our original call to the wrapper was through the proxy. This would set up transactions if we had them on the wrapper, but we didn't. That is why we got the readOnly error. We were still using the OSIV transaction. Once we added a transaction to the wrapper class and called through the proxy the transaction was started for the wrapper but then the self referenced calls to txMethodA and txMethodB where only in the service object. Not throught the proxy. The pojo service object knows nothing about transactions so that's why nothing happened and it seemed like it was being ignored.

This is just something to keep in mind not only when using spring transactions but when using any Spring AOP advised code. It's also a sign I need to read more...

No comments: