Part 8: DAOs, Repositories, or Query Objects

Part 8 is about abstracting NHibernate. Catch up by reading Part 1, Part 2, Part 3, Part 4, Part 5, Part 6, and Part 7.

Warning: This post will contain an extraordinary number of links. They will lead you to the opinions of very smart people™. Click them. Read them. Learn something new.
There is no one best practice. I know. I googled for it. It seems there are just as many patterns as there are anti-patterns. In fact, these days we’re not even clear which is which. There are differing opinions all over the place.

What are my options?

Repositories

Data Access Objects:

Query objects:

  • In early 2009, Ayende posted that he no longer likes repositories, and has switched to query objects which expose raw NHibernate  ICriteria.
  • Udi Dahan also prefers query objects

    One note about all of this: Repositories and DAOs can both be used with query objects or simple FindBy methods. Query objects can also be used on their own.

    What’s the score?

    The experts don’t agree, so use whatever you think will work best for your application and your team. By the way, if you’re not following all of these people on twitter, go follow them now.

    If you’re looking for a good NHibernate repository sample, check out Your First NHibernate based application by Gabriel Schenker on the NHForge wiki, or José’s Chinook WPF app.

    In this sample application, we’re going to use Data Access Objects. The pattern is simple and well known. This application is small and we won’t have many queries, so we’ll use DAOs with FindBy methods. In a large project, such as an ERP, I would use query objects.

    Splitting the CRUD

    CRUD stands for create, read/retrieve, update, and delete/destroy. which correspond to SQL INSERT, SELECT, UPDATE, and DELETE respectively.

    Suppose we’re tracking down an issue in our system where the customer’s middle name was being erased from the database. You start with the most likely locations such as the round trip through the customer update view. No luck. You’ll have to dig in deeper.

    We’re using constructor dependency injection throughout our application. Our DAO is defined by the interface IDAO<T>. If you saw some object with a dependency of IDAO<Customer>, you would assume that it performs some database action on customer, so it would be a candidate for deeper investigation. Of course, without diving in to the code, you wouldn’t know what it actually does to customer.

    As it turns out 95% of the uses of IDAO<Customer> only display customer data. They don’t actually change anything. You just wasted a LOT of time digging through code that couldn’t possibly cause your bug.

    Now suppose you had split your IDAO interface to allow more fine-grained dependencies. Instead of IDAO<T>, you now have ICreate<T>, IRead<T>, IUpdate<T>, and IDelete<T>. When searching for a bug like the one I described, you only need to search through classes with dependencies on IUpdate<Customer> and possibly ICreate<Customer>.

    We’re tracking which entity instances are transient (new, not saved) and which ones are already persisted (saved to the database) by the ID property. If the ID is equal to Guid.Empty, the instance is transient. If the ID has any other value, it’s persistent. Since we know that handy bit of information, we don’t really need separate interfaces for create and update operations. We can combine them in to one called ISave<T>. We now have IRead<T>, ISave<T>, and IDelete<T>.

    Even though we’ve split our interface up by operation, we’re still only going to have one DAO implementation. In the Ninject module, we’ll bind each of our three interfaces to the DAO implementation.

    Every entity has the same basic CUD, but what about entity-specific queries? In these cases, we’ll create entity-specific interfaces such as IReadCustomer. This means you could have up to four IoC bindings for each entity.

    Splitting the CRUD operations in to separate interfaces has one added benefit. In our case, we don’t want to allow certain (most) entities to be deleted. In these cases, your entity-specific DAO shouldn’t implement IDelete. For this reason, we won’t implement deletes in our generic base DAO.

    Show me some code already!

    We put our interfaces in the data namespace of the core project and our implementations in the data project.

    Namespace Data
    
        Public Interface IRead(Of TEntity As Entity)
    
            Function GetByID(ByVal ID As Guid) As TEntity
    
        End Interface
    
    End Namespace
    
    Namespace Data
    
        Public Interface ISave(Of TEntity As Entity)
    
            Function Save(ByVal Entity As TEntity) As TEntity
    
        End Interface
    
    End Namespace
    
    Namespace Data
    
        Public Interface IDelete(Of TEntity As Entity)
    
            Sub Delete(ByVal Entity As TEntity)
    
        End Interface
    
    End Namespace
    
    Namespace Data
    
        Public Interface IReadStudent
            Inherits IRead(Of Student)
    
            Function FindByStudentID(ByVal StudentID As String) As Student
            Function FindByName(ByVal LikeFirstName As String, ByVal LikeLastName As String) As IEnumerable(Of Student)
    
        End Interface
    
    End Namespace
    
    Imports NHibernate
    
    Public Class GenericDAOImpl(Of TEntity As Entity)
        Implements IRead(Of TEntity)
        Implements ISave(Of TEntity)
    
        Public Sub New(ByVal Session As ISession)
            m_session = Session
        End Sub
    
        Protected ReadOnly m_Session As ISession
    
        Public Function GetByID(ByVal ID As System.Guid) As TEntity Implements IRead(Of TEntity).GetByID
            If m_Session.Transaction Is Nothing Then
                Dim RetVal As TEntity
                Using Tran = m_Session.BeginTransaction
                    RetVal = m_Session.Get(Of TEntity)(ID)
                    Tran.Commit()
                    Return RetVal
                End Using
            Else
                Return m_Session.Get(Of TEntity)(ID)
            End If
        End Function
    
        Public Function Save(ByVal Entity As TEntity) As TEntity Implements ISave(Of TEntity).Save
            If m_Session.Transaction Is Nothing Then
                Using Tran = m_Session.BeginTransaction
                    m_Session.SaveOrUpdate(Entity)
                    Tran.Commit()
                End Using
            Else
                m_Session.SaveOrUpdate(Entity)
            End If
            Return Entity
        End Function
    
    End Class
    
    Imports NHibernate
    Imports NHibernate.Criterion
    
    Public Class StudentDaoImpl
        Inherits GenericDAOImpl(Of Student)
        Implements IReadStudent
    
        Public Sub New(ByVal Session As ISession)
            MyBase.New(Session)
        End Sub
    
        Public Function FindByName(ByVal LikeFirstName As String, ByVal LikeLastName As String) As System.Collections.Generic.IEnumerable(Of Student) Implements IReadStudent.FindByName
            Dim crit As ICriteria = m_Session.CreateCriteria(Of Student) _
                .Add(Expression.Like("FirstName", LikeFirstName)) _
                .Add(Expression.Like("LastName", LikeLastName)) _
                .SetMaxResults(101)
            If m_Session.Transaction Is Nothing Then
                Using Tran = m_Session.BeginTransaction()
                    Dim RetVal = crit.List.Cast(Of Student)()
                    Tran.Commit()
                    Return RetVal
                End Using
            Else
                Return crit.List.Cast(Of Student)()
            End If
        End Function
    
        Public Function FindByStudentID(ByVal StudentID As String) As Student Implements IReadStudent.FindByStudentID
            Dim Crit = m_Session.CreateCriteria(Of Student) _
                .Add(Expression.Eq("StudentID", StudentID))
            If m_Session.Transaction Is Nothing Then
                Using Tran = m_Session.BeginTransaction
                    Dim RetVal = Crit.UniqueResult(Of Student)()
                    Tran.Commit()
                    Return RetVal
                End Using
            Else
                Return Crit.UniqueResult(Of Student)()
            End If
        End Function
    
    End Class
    using System;
    
    namespace NStackExample.Data
    {
        public interface IRead<TEntity> where TEntity : Entity 
        {
    
            TEntity GetById(Guid ID);
    
        }
    }
    
    namespace NStackExample.Data 
    {
        public interface ISave<TEntity> where TEntity : Entity 
        {
    
            TEntity Save(TEntity entity);
    
        }
    }
    
    namespace NStackExample.Data
    {
        public interface IDelete<TEntity> where TEntity:Entity 
        {
    
            void Delete(TEntity entity);
    
        }
    }
    
    using System.Collections.Generic;
    
    namespace NStackExample.Data
    {
        public interface IReadStudent : IRead<Student>
        {
    
            Student FindByStudentID(string StudentID);
            IEnumerable<Student> FindByName(string LikeFirstName, string LikeLastName);
    
        }
    }
    
    using NHibernate;
    
    namespace NStackExample.Data
    {
    
        public class GenericDAOImpl<TEntity> : IRead<TEntity>, ISave<TEntity> where TEntity : Entity
        {
    
            public GenericDAOImpl(ISession Session)
            {
                m_Session = Session;
            }
    
            protected readonly ISession m_Session;
    
            public TEntity GetByID(System.Guid ID)
            {
                if (m_Session.Transaction == null)
                {
                    TEntity retval;
                    using (var Tran = m_Session.BeginTransaction())
                    {
                        retval = m_Session.Get<TEntity>(ID);
                        Tran.Commit();
                        return retval;
                    }
                }
                else
                {
                    return m_Session.Get<TEntity>(ID);
                }
            }
    
            public TEntity Save(TEntity Entity)
            {
                if (m_Session.Transaction == null)
                {
                    using (var Tran = m_Session.BeginTransaction())
                    {
                        m_Session.SaveOrUpdate(Entity);
                        Tran.Commit();
                    }
                }
                else
                {
                    m_Session.SaveOrUpdate(Entity);
                }
                return Entity;
            }
    
        }
    
    }
    
    using NHibernate;
    using NHibernate.Criterion;
    using System.Collections.Generic;
    using System.Linq;
    
    namespace NStackExample.Data
    {
        public class StudentDAOImpl : GenericDAOImpl<Student>, IReadStudent
        {
    
            public StudentDAOImpl(ISession Session) : base(Session) { }
    
            public System.Collections.Generic.IEnumerable<Student> FindByName(string LikeFirstName, string LikeLastName)
            {
                ICriteria crit = m_Session.CreateCriteria<Student>()
                    .Add(Expression.Like("FirstName", LikeFirstName))
                    .Add(Expression.Like("LastName", LikeLastName))
                    .SetMaxResults(101);
                if (m_Session.Transaction == null)
                {
                    using (var Tran = m_Session.BeginTransaction())
                    {
                        var RetVal = crit.List().Cast<Student>();
                        Tran.Commit();
                        return RetVal;
                    }
                }
                else
                {
                    return crit.List().Cast<Student>();
                }
            }
    
            public Student FindByStudentID(string StudentID)
            {
                var Crit = m_Session.CreateCriteria<Student>()
                    .Add(Expression.Eq("StudentID", StudentID));
                if (m_Session.Transaction == null)
                {
                    using (var Tran = m_Session.BeginTransaction())
                    {
                        var RetVal = Crit.UniqueResult<Student>();
                        Tran.Commit();
                        return RetVal;
                    }
                }
                else
                {
                    return Crit.UniqueResult<Student>();
                }
            }
    
    
        }
    }

    Other changes

    I’ve cleaned out the course and student DAO junk from part 7. These were just used to illustrate session-per-request.

    The fluent mapping classes have been moved in to a mapping folder.

    That’s it for part 8. Don’t forget to write your tests for the queries.

    Jason
    - IBlog.Post(Part8) operation completed. Executing IWatchTV.Watch(Timespan.FromHours(1))

  • blog comments powered by Disqus