The latest issue that I’ve encountered while working with Squeryl in a Lift-based web application, is that not all transactions are being committed to the database. This post is a quick discussion of the symptoms that I was seeing and a note on how to avoid the issue.
The Setup
As noted before, the setup that I am using is quite generic and based on the example configurations on the Squeryl-Record wiki page. The only portion which is relevant to this article is how transactions are handled. For now, I’m using a simple transaction-per-request strategy, implemented as follows:
S.addAround(new LoanWrapper {
override def apply[T](f: => T): T = inTransaction { f }
})
Although this code fragment appears in most of the examples on the web, it has at least one significant flaw.
The Symptoms
The flaw with the above code fragment is that it does not handle Lift’s flow control exceptions properly. In retrospect, the behavior is very clear and easy to spot (but was not quite so obvious at the time): The transactions failed to commit to the database whenever the request which caused the database updates resulted in an HTTP redirect response. The mechanics for why this happens are equally clear in retrospect.
Squeryl’s inTransaction
method assumes that the transaction should be aborted
if the code that it is executing throws an exception and it will rollback the
transaction when this happens. However, Lift implements partial or
short-circuited responses (e.g. redirects) by throwing a
LiftFlowOfControlException
. Therefore, whenever a response is redirected
after database changes are made, those changes will be rolled back.
The Solution
The solution that I am using is reasonably simple. Replace the above code fragment with:
S.addAround(new LoanWrapper {
override def apply[T](f: => T): T = {
val resultOrExcept = inTransaction {
try {
Right(f)
} catch {
case e: LiftFlowOfControlException => Left(e)
}
}
resultOrExcept match {
case Right(result) => result
case Left(except) => throw except
}
}
})
This way whenever a LiftFlowOfControlException
is thrown it will be returned
through inTransaction and the transaction will commit while any other exception
propagates out as before causing transaction rollback. Then, if an exception
was returned, it is re-thrown from the LoanWrapper
to continue propagating
up to the Lift internals.
With this in place, transactions should commit and Lift’s control flow exceptions should continue to work as expected. At least, I hope so…
Article Changes
2012-09-07
- Fixed the code using
Either
to follow the standard convention thatLeft
is failure andRight
is success (as documented in the scala.util.Either scaladoc).