In my last post I discussed wrapping up the two core objects of nHibernate, the sessionmanagerfactory and the session, in a helper class. At the end of the story I briefly mentioned how to use helper objects in a repository. In this post I am further exploring the actual database actions of the helper and how this helper can be used in real repositories in a "regular" DDD style. Again, don't take this as the way to do things. The web is covered with a lot of ways to get that done and there are several tools available. This is just a minimalistic way which works well for me and does demonstrate some programming ideas. Feel free to comment.
The previous post discussed a way to manage nHibernate sessions. All data operations are performed on such a session. The major operations are described in this interface
public interface INhibernateHelper : IDisposable
{
IQuery Query(string query);
T UniqueResult<T>(IQuery query);
IList<T> ListResult<T>(IQuery query);
IEnumerable<T> EnumerateResult<T>(IQuery query);
void BeginTransAction();
void Commit();
void Save(object dObject);
void Delete(object o);
}
The main reason for describing the functionality as an interface lies in testing; an interface is relatively easy to mock.
The members which return data are defined as generic methods; this gives me the luxury of strongly typed data. At first sight it might be tempting to make the interface itself generic, that is:
public interface INhibernateHelper<T> : IDisposable
{
T UniqueResult<T>(IQuery query);
}
But that will backfire when implementing the interface. To manage a shared sessionfactory I am using a static constructor. As every specific type instance of a generic class fires its own specific static constructor the sessionfactory can no longer be shared over different classes. Having only the methods themselves generic will do.
When it comes to writing to the database it is important to realize the way nHibernate works. Invoking Save or Delete will not immediately persist that data. It takes the session's Flush method to trigger that. When flushing the data your objects will not always be persisted in the same order as the Save and Delete invocations. Things are further complicated because nHibernate will sometimes perform an implicit flush when querying to prevent returning stale data.
On a session you can start a transaction but you can only start one transaction on every session. In my implementation of transactions I'm very forgiving. BeginTransaction can be invoked again and again; the first invocation will start the nHibernate transaction. A Commit is always honored; in case no transaction was started the helper will just flush the data.
The main things when implementing all members is to take care of exceptions. A session cannot recover from an exception. In case you hit one the only solution is to close the session.
internal class NhibernateHelper : INhibernateHelper
{
private static readonly ISessionFactory sessionFactory;
static NhibernateHelper()
{
Configuration cfg = new Configuration();
IDictionary props = new Hashtable();
props.Add("hibernate.dialect", "NHibernate.Dialect.MsSql2000Dialect");
props.Add("hibernate.connection.provider", "NHibernate.Connection.DriverConnectionProvider");
props.Add("hibernate.connection.connection_string", nHibernateConnectionHelper.connectionString);
cfg.SetProperties(props);
cfg.AddAssembly(nHibernateConnectionHelper.domainAssembly);
sessionFactory = cfg.BuildSessionFactory();
}
private static string connectionString
{
get
{
Type typeOfConnectionProvider = typeof (NHibernate.Connection.DriverConnectionProvider);
PropertyInfo ConnectionStringPropertyInfo = typeOfConnectionProvider.GetProperty("ConnectionString",
BindingFlags.Instance | BindingFlags.NonPublic);
return (string) ConnectionStringPropertyInfo.GetValue(sessionFactory.ConnectionProvider, null);
}
}
private readonly ISession currentSession;
internal NhibernateHelper()
{
if (string.IsNullOrEmpty(nHibernateConnectionHelper.connectionString))
throw new Exception("Sessionfactory needs a connectionstring");
if (nHibernateConnectionHelper.connectionString != connectionString)
throw new Exception(string.Format("Sessionfactory connection allready initialized as {0}",connectionString));
currentSession = sessionFactory.OpenSession();
}
private ITransaction tx = null;
public void BeginTransAction()
{
if (tx == null)
try
{
tx = currentSession.BeginTransaction();
}
catch (Exception)
{
currentSession.Close();
throw;
}
}
public void Commit()
{
if (tx == null)
{
try
{
currentSession.Flush();
}
catch(Exception)
{
currentSession.Close();
throw;
}
}
else
{
try
{
tx.Commit();
}
catch (Exception)
{
tx.Rollback();
currentSession.Close();
}
finally
{
tx = null;
}
}
}
public IQuery Query(string query)
{
return currentSession.CreateQuery(query);
}
public void CloseSession()
{
currentSession.Close();
}
public void Save(object dObject)
{
try
{
currentSession.SaveOrUpdate(dObject);
}
catch (Exception)
{
currentSession.Close();
throw;
}
}
public void Delete(object o)
{
try
{
currentSession.Delete(o);
}
catch (Exception)
{
currentSession.Close();
throw;
}
}
public void Dispose()
{
currentSession.Dispose();
}
public T UniqueResult<T>(IQuery query)
{
try
{
return query.UniqueResult<T>();
}
catch (Exception)
{
currentSession.Close();
throw;
}
}
public System.Collections.Generic.IList<T> ListResult<T>(IQuery query)
{
try
{
return query.List<T>();
}
catch (Exception)
{
currentSession.Close();
throw;
}
}
public System.Collections.Generic.IEnumerable<T> EnumerateResult<T>(IQuery query)
{
try
{
return query.Enumerable<T>();
}
catch (Exception)
{
currentSession.Close();
throw;
}
}
}
When hitting an exception all this helper does is clean up after which it rethrows the exception, it's up to its user to take further action.
All database interaction through nHibernate is now stuffed in this helper class. It can be used by a generic repository.
public class Repository<T>
{
private readonly INhibernateHelper hibernate;
private readonly string className;
public Repository(INhibernateHelper hibernate)
{
className = typeof(T).Name;
this.hibernate = hibernate;
}
public IQuery BuildQuery(string queryString)
{
return hibernate.Query(string.Format("from {0} where {1}", className, queryString));
}
public IList<T> ListAll(string orderBy)
{
string queryString = string.Format("from {0} order by {1}", className, orderBy);
IQuery query = hibernate.Query(queryString);
return hibernate.ListResult<T>(query);
}
public IEnumerable<T> EnumerateAll()
{
string queryString = string.Format("from {0}", className);
IQuery query = hibernate.Query(queryString);
return hibernate.EnumerateResult<T>(query);
}
public IEnumerable<T> EnumerateInPeriod(string propertyName, DateTime from, DateTime till)
{
if (from == DateTime.MinValue)
from = new DateTime(1753, 1, 1);
if (till == DateTime.MaxValue)
till = new DateTime(9999, 12, 31);
string queryString = string.Format("from {0} where {1} >= ? and {1} < ?", className, propertyName);
IQuery query = hibernate.Query(queryString);
query.SetDateTime(0, from);
query.SetDateTime(1, till);
return hibernate.EnumerateResult<T>(query);
}
public T FindUnique(string propertyName, string propertyValue)
{
string queryString = string.Format("from {0} where {1}=?", className, propertyName);
IQuery query = hibernate.Query(queryString);
query.SetString(0, propertyValue);
return hibernate.UniqueResult<T>(query);
}
public T FindUnique(string propertyName, long propertyValue)
{
string queryString = string.Format("from {0} where {1}=?", className, propertyName);
IQuery query = hibernate.Query(queryString);
query.SetInt64(0, propertyValue);