Part 9: NHibernate transactions
In this part, we’re going to wrap our NHibernate transactions and create a factory for them so we can use them in higher layers without referencing NHibernate all the way up.
If you’re new to the series, you can read Part 1, Part 2, Part 3, Part 4, Part 5, Part 6, Part 7, and Part 8 to catch up.
You may have noticed in part 8 that in each DAO method, if we didn’t already have an explicit transaction, I created one around each database interaction. My reason for this is explained in Ayende’s NHibernate Profiler alert “Use of implicit transactions is discouraged.” This works great for simple DB interaction, but what about the more complex scenarios?
This is where we get to talk about this great thing called a business transaction. So once again, I’m going to parade out my experts. Actually, this time it’s only Udi Dahan. There are two key points he’s written about on his blog.
- Partial failures can be good. The programmer in all of us sees that and screams atomicity. Transactions should be all-or-nothing. Anything less is just wrong. Right? In real life, there are instances where we allow, and even prefer partial failures of business transactions. Udi gives us a great example. Would you leave the grocery store empty handed simply because they were out of one item on your list? Probably not. When you’re gathering requirements, be sure to ask questions about the proper way to fail. “Roll it all back” isn’t the only option.
- Realistic Concurrency – The entire post is worth reading, but Udi makes one point I want to touch on specifically. When performing an operation for the user, you should get the current state, validate, and then perform the task all within the business transaction.
Let’s use our college application as an example. We have a user story / use case / requirement / story card / whatever to allow students to register for classes, provided those classes aren’t full. If you’ve ever worked at or attended a college or university where certain classes always have more demand than available seats, you are no doubt aware of how quickly those classes will fill up. In fact, the best sections (best professors and best times) can fill up just minutes after registration is opened. It’s very possible that dozens of potential students could access the section when there is only a few seats left. Since the enrollment in a particular section (the current state) changes so rapidly, you must obtain a lock, refresh your enrollment numbers and make sure there is room (revalidate) before actually enrolling that student. If more than one registration request is received, they should be performed serially.
The process is:
- Open a transaction at the proper isolation level. Consult your nearest DBA, as isolation levels are outside the scope of this series.
- Refresh – Get the current state of the entity
- (Re)Validate – Be sure the business transaction is still valid for the current state
- Execute – Perform the insert / update / delete
- Commit the transaction
Now that we’ve covered business transactions, let’s get set up to use them in our business logic. We shouldn’t have NHibernate types floating around at that level, so we’ll wrap them. Once again, the interfaces go in the Data namespace of the core project and the implementations go in the Data project.
Imports System.Data Namespace Data Public Interface ITransactionFactory Function BeginTransaction() As ITransaction Function BeginTransaction(ByVal IsolationLevel As IsolationLevel) As ITransaction End Interface End Namespace Namespace Data Public Interface ITransaction Inherits IDisposable Sub Commit() Sub Rollback() End Interface End Namespace Imports NHibernate Public Class TransactionFactoryImpl Implements ITransactionFactory Public Sub New(ByVal Session As ISession) m_Session = Session End Sub Protected ReadOnly m_Session As ISession Public Function BeginTransaction() As ITransaction Implements ITransactionFactory.BeginTransaction Return New TransactionWrapper(m_Session.BeginTransaction) End Function Public Function BeginTransaction(ByVal IsolationLevel As System.Data.IsolationLevel) As ITransaction Implements ITransactionFactory.BeginTransaction Return New TransactionWrapper(m_Session.BeginTransaction(IsolationLevel)) End Function End Class Imports NHibernate Public Class TransactionWrapper Implements ITransaction Public Sub New(ByVal Transaction As NHibernate.ITransaction) m_Transaction = Transaction End Sub Protected ReadOnly m_Transaction As NHibernate.ITransaction Public Sub Commit() Implements ITransaction.Commit m_Transaction.Commit() End Sub Public Sub Rollback() Implements ITransaction.Rollback m_Transaction.Rollback() End Sub Private disposedValue As Boolean = False ' To detect redundant calls ' IDisposable Protected Overridable Sub Dispose(ByVal disposing As Boolean) If Not Me.disposedValue Then If disposing Then ' TODO: free other state (managed objects). m_Transaction.Dispose() End If ' TODO: free your own state (unmanaged objects). ' TODO: set large fields to null. End If Me.disposedValue = True End Sub #Region " IDisposable Support " ' This code added by Visual Basic to correctly implement the disposable pattern. Public Sub Dispose() Implements IDisposable.Dispose ' Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above. Dispose(True) GC.SuppressFinalize(Me) End Sub #End Region End Class
using System.Data; namespace NStackExample.Data { public interface ITransactionFactory { ITransaction BeginTransaction(); ITransaction BeginTransaction(IsolationLevel isolationLevel); } } using System; namespace NStackExample.Data { public interface ITransaction : IDisposable { void Commit(); void Rollback(); } } using System.Data; using NHibernate; namespace NStackExample.Data { public class TransactionFactoryImpl : ITransactionFactory { public TransactionFactoryImpl(ISession Session) { m_Session = Session; } protected readonly ISession m_Session; #region ITransactionFactory Members public ITransaction BeginTransaction() { return new TransactionWrapper(m_Session.BeginTransaction()); } public ITransaction BeginTransaction(IsolationLevel isolationLevel) { return new TransactionWrapper(m_Session.BeginTransaction(isolationLevel)); } #endregion } } using System; using NHibernate; namespace NStackExample.Data { public class TransactionWrapper : ITransaction { public TransactionWrapper(NHibernate.ITransaction Transaction) { m_Transaction = Transaction; } protected readonly NHibernate.ITransaction m_Transaction; #region ITransaction Members void ITransaction.Commit() { m_Transaction.Commit(); } void ITransaction.Rollback() { m_Transaction.Rollback(); } private bool disposedValue = false; protected override void Dispose(bool Disposing) { if (!this.disposedValue) { m_Transaction.Dispose(); } this.disposedValue = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion } }
You may be interested to know that the NHibernate ITransaction will perform an implicit rollback when it is disposed, unless an explicit call to Commit or Rollback has already occurred. To implement this behavior, we implement IDisposable in our transaction wrapper and chain our wrapper’s Dispose to NHibernate.ITransaction’s Dispose. This implicit rollback can indicate a missing call to Commit, so it generates an alert in NHibernate Profiler. If you intend to rollback, do it explicitly. Your code will be easier to understand.
That’s it for part 9.
Jason
- Off to mow the lawn.