When to commit NHibernate transactions in ASP.NET MVC 2 application?
First, some background: I'm new to ASP.NET MVC 2 and NHibernate. I'm starting my first application and I want to use NHibernate, because I come from JSP + Struts 1 + Hibernate web applications.
No one seems to be talking about this, so I guess it must be pretty obvious. Still I scratch my head because I can't find a solution that accomplish the following things:
1) I want to use the "session per request" strategy. So, everytime a user makes a request, he gets an Nhibernate session, starts a transaction, and when the request is over, the transaction commits, and the NHibernate session closes (and returns to the pool if there is one). This guarantees that my transactions are atomic.
2) When a database exception occurs (PK violation, unique violation, whatever) I want to capture that exception, rollback my transaction and give the user a explicit message: if it was PK violation, then that message, and the same with all integrity errors.
So, what is my problem? I come from the Java World, where I used a Filter to open the session, start the transaction, process the request, then commit the transaction and close the session. This works, except when an DB exception occurs, and by the time you are in the filter there's no way to change the destination page because the response is already committed.
So the user sees the success page when in reality the transaction was rollbacked. To avoid this I have to write a lot of data integrity checks in Java in order to prevent all integrity exceptions, because I could not handle them correctly. This is bad because I'm doing the work instead of leaving it to the database (or maybe I'm wrong and I always have to write all this data integrity code in my app?).
So I've found the IHttpModule interface which I'm guessing is pretty much the same concept as a javax.servlet.Filter (correct me if I'm wrong), so I'm guessing I could have the same problem again.
Where should I put my commits in order to make sure that my transactions are atomic, and when they throw exceptions I can capture them and change my destination page and give the user a comprehensive message?
So far the only possible solution I've come up with is to keep my IHttpModule to start and close the transaction, and put the commit calls in the last line of my controllers methods, thus being able to capture exceptions there and then return an appropiate view with the message. Now I would hav开发者_JAVA技巧e to copy those commit and exception handling lines into all my controller methods that require commits. And there is the separation of concerns issue, that my controllers have to know about DB, which I don't like at all.
Is there a better way?
If you're using ASP.NET MVC, you could use an ActionFilter to achieve the same effect.
Something like (this is hacked together from difference pieces of my architecture):
public class TransactionalAttribute : ActionFilterAttribute, IAuthorizationFilter, IExceptionFilter
{
ITransaction transaction = NullTransaction.Instance;
public IsolationLevel IsolationLevel { get; set; }
public TransactionalAttribute()
{
IsolationLevel = IsolationLevel.ReadCommitted;
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
try
{
transaction.Commit();
transaction = NullTransaction.Instance;
}
catch (Exception exception)
{
Log.For(this).FatalFormat("Problem trying to commit transaction {0}", exception);
}
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (transaction == NullTransaction.Instance) transaction = UnitOfWork.Current.BeginTransaction(IsolationLevel);
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (filterContext.Result != null) return;
transaction.Commit();
transaction = NullTransaction.Instance;
}
public void OnAuthorization(AuthorizationContext filterContext)
{
transaction = UnitOfWork.Current.BeginTransaction(IsolationLevel);
}
public void OnException(ExceptionContext filterContext)
{
try
{
transaction.Rollback();
transaction = NullTransaction.Instance;
}
catch (Exception exception)
{
Log.For(this).FatalFormat("Problem trying to rollback transaction {0}", exception);
}
}
private class NullTransaction : ITransaction
{
public static ITransaction Instance { get { return Singleton<NullTransaction>.Instance; } }
public void Dispose()
{
}
public void Commit()
{
}
public void Rollback()
{
}
}
}
Well after thinking about it and discussed it with coworkers I've come up with a solution that meets almost all my requirements.
I implemented the solution with my Java projects and it worked great. I'll just pust the idea so everybody can use it within any framework.
The solution consist in putting the commit call in the last line of the controller method, inside a try-catch block. If a constraint exception occurs you can get the name of the violated constraint. With the name you can tell the user exactly what went wrong. I used a properties file to store the message to show to the user wich constraint was violated. The keys of the properties file are the constraints names and the values are the constraint violation messages.
Yo can refactor the commit-handle_exception-find_constraint_message to a method, that's what I did.
For now it solves my problem of writing code to check database integrity and I believe it's pretty elegant with the constraint violation messages in a properties file. Now, I still don't like the idea that my controllers need to call the commit, but that's way better than writing integrity checks that the database already does.
I will continue to use a filter just like David Kemp said, just that the filter will only open the (n)hibernate session and the transaction, and then, at the end of the request, close the session.
Comments are more than welcome. Thanks.
精彩评论