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.
What are my options?
- Steve Bohlen uses the repository pattern.
- José Romaniello uses repositories in his Chinook Media Manager sample on NHForge.org
- Ayende loved repositories in 2006, but not anymore.
- Fabio Maulo uses DAOs with query objects.
- Tuna Toksoz uses DAOs with FindBy methods
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))