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.

  1. 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.
  2. 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:

  1. Open a transaction at the proper isolation level. Consult your nearest DBA, as isolation levels are outside the scope of this series.
  2. Refresh – Get the current state of the entity
  3. (Re)Validate – Be sure the business transaction is still valid for the current state
  4. Execute – Perform the insert / update / delete
  5. 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.

blog comments powered by Disqus