In this article by Alexander Zaytsev, author of NHibernate 4.0 Cookbook, we will cover the following topics:
(For more resources related to this topic, see here.)
There are two styles of data access layer common in today's applications. Repositories and Data Access Objects. In reality, the distinction between these two have become quite blurred, but in theory, it's something like this:
The first recipe shows the beginnings of a typical data access object. The remaining recipes show how to set up a repository-based data access layer with NHibernate's various APIs.
In this recipe, we'll show you how we can set up the data access layer to wrap all data access in NHibernate transactions automatically.
public class DataAccessObject<T, TId>
where T : Entity<TId>
{
private readonly ISessionFactory _sessionFactory;
private ISession session
{
get
{
return _sessionFactory.GetCurrentSession();
}
}
public DataAccessObject(ISessionFactory sessionFactory)
{
_sessionFactory = sessionFactory;
}
public T Get(TId id)
{
return WithinTransaction(() => session.Get<T>(id));
}
public T Load(TId id)
{
return WithinTransaction(() => session.Load<T>(id));
}
public void Save(T entity)
{
WithinTransaction(() => session.SaveOrUpdate(entity));
}
public void Delete(T entity)
{
WithinTransaction(() => session.Delete(entity));
}
private TResult WithinTransaction<TResult>(Func<TResult> func)
{
if (!session.Transaction.IsActive)
{
// Wrap in transaction
TResult result;
using (var tx = session.BeginTransaction())
{
result = func.Invoke();
tx.Commit();
}
return result;
}
// Don't wrap;
return func.Invoke();
}
private void WithinTransaction(Action action)
{
WithinTransaction<bool>(() =>
{
action.Invoke();
return false;
});
}
}
public class DataAccessObject<T>
: DataAccessObject<T, Guid>
where T : Entity
{
}
NHibernate requires that all data access occurs inside an NHibernate transaction.
Remember, the ambient transaction created by TransactionScope is not a substitute for an NHibernate transaction
This recipe, however, shows a more explicit approach. To ensure that at least all our data access layer calls are wrapped in transactions, we create a private WithinTransaction method that accepts a delegate, consisting of some data access methods, such as session.Save or session.Get. This WithinTransaction method first checks if the session has an active transaction. If it does, the delegate is invoked immediately. If it doesn't, a new NHibernate transaction is created, the delegate is invoked, and finally the transaction is committed. If the data access method throws an exception, the transaction will be rolled back automatically as the exception bubbles up to the using block.
This transactional auto-wrapping can also be set up using SessionWrapper from the unofficial NHibernate AddIns project at https://bitbucket.org/fabiomaulo/unhaddins. This class wraps a standard NHibernate session. By default, it will throw an exception when the session is used without an NHibernate transaction. However, it can be configured to check for and create a transaction automatically, much in the same way I've shown you here.
Many developers prefer the repository pattern over data access objects. In this recipe, we'll show you how to set up the repository pattern with NHibernate.
How to do it...
public interface IRepository<T>: IEnumerable<T>
where T : Entity
{
void Add(T item);
bool Contains(T item);
int Count { get; }
bool Remove(T item);
}
protected readonly ISessionFactory _sessionFactory;
protected virtual ISession session
{
get
{
return _sessionFactory.GetCurrentSession();
}
}
public NHibernateBase(ISessionFactory sessionFactory)
{
_sessionFactory = sessionFactory;
}
protected virtual TResult WithinTransaction<TResult>(
Func<TResult> func)
{
if (!session.Transaction.IsActive)
{
// Wrap in transaction
TResult result;
using (var tx = session.BeginTransaction())
{
result = func.Invoke();
tx.Commit();
}
return result;
}
// Don't wrap;
return func.Invoke();
}
protected virtual void WithinTransaction(Action action)
{
WithinTransaction<bool>(() =>
{
action.Invoke();
return false;
});
}
public class NHibernateRepository<T> :
NHibernateBase,
IRepository<T> where T : Entity
{
public NHibernateRepository(
ISessionFactory sessionFactory)
: base(sessionFactory)
{
}
public void Add(T item)
{
WithinTransaction(() => session.Save(item));
}
public bool Contains(T item)
{
if (item.Id == default(Guid))
return false;
return WithinTransaction(() =>
session.Get<T>(item.Id)) != null;
}
public int Count
{
get
{
return WithinTransaction(() =>
session.Query<T>().Count());
}
}
public bool Remove(T item)
{
WithinTransaction(() => session.Delete(item));
return true;
}
public IEnumerator<T> GetEnumerator()
{
return WithinTransaction(() => session.Query<T>()
.Take(1000).GetEnumerator());
}
IEnumerator IEnumerable.GetEnumerator()
{
return WithinTransaction(() => GetEnumerator());
}
}
The repository pattern, as explained in http://martinfowler.com/eaaCatalog/repository.html, has two key features:
In this recipe, we are concerned only with the first feature, behaving as an in-memory collection. The remaining recipes in this article will build on this base, and show various methods for satisfying the second point.
Because our repository should act like an in-memory collection, it makes sense that our IRepository<T> interface should resemble ICollection<T>.
Our NHibernateBase class provides both contextual session management and the automatic transaction wrapping explained in the previous recipe.
NHibernateRepository simply implements the members of IRepository<T>.
The Repository pattern reduces data access to its absolute simplest form, but this simplification comes with a price. We lose much of the power of NHibernate behind an abstraction layer. Our application must either do without even basic session methods like Merge, Refresh, and Load, or allow them to leak through the abstraction.
Named Queries encapsulated in query objects is a powerful combination. In this recipe, we'll show you how to use Named Queries with your data access layer.
To complete this recipe you will need Common Service Locator from Microsoft Patterns & Practices. The documentation and source code could be found at http://commonservicelocator.codeplex.com.
Complete the previous recipe Setting up an NHibernate repository.
Include the Eg.Core.Data.Impl assembly as an additional mapping assembly in your test project's App.Config with the following xml:
<mapping assembly="Eg.Core.Data.Impl"/>
public interface IQuery
{
}
public interface IQuery<TResult> : IQuery
{
TResult Execute();
}
public interface IQueryFactory
{
TQuery CreateQuery<TQuery>() where TQuery :IQuery;
}
public interface IRepository<T>
: IEnumerable<T>, IQueryFactory
where T : Entity
{
void Add(T item);
bool Contains(T item);
int Count { get; }
bool Remove(T item);
}
private readonly IQueryFactory _queryFactory;
public NHibernateRepository(
ISessionFactory sessionFactory,
IQueryFactory queryFactory)
: base(sessionFactory)
{
_queryFactory = queryFactory;
}
public TQuery CreateQuery<TQuery>() where TQuery : IQuery
{
return _queryFactory.CreateQuery<TQuery>();
}
Install-Package CommonServiceLocator
public class QueryFactory : IQueryFactory
{
private readonly IServiceLocator _serviceLocator;
public QueryFactory(IServiceLocator serviceLocator)
{
_serviceLocator = serviceLocator;
}
public TQuery CreateQuery<TQuery>() where TQuery : IQuery
{
return _serviceLocator.GetInstance<TQuery>();
}
}
public abstract class NHibernateQueryBase<TResult>
: NHibernateBase, IQuery<TResult>
{
protected NHibernateQueryBase(
ISessionFactory sessionFactory)
: base(sessionFactory) { }
public abstract TResult Execute();
}
public interface INamedQuery
{
string QueryName { get; }
}
public abstract class NamedQueryBase<TResult>
: NHibernateQueryBase<TResult>, INamedQuery
{
protected NamedQueryBase(ISessionFactory sessionFactory)
: base(sessionFactory) { }
public override TResult Execute()
{
var nhQuery = GetNamedQuery();
return Transact(() => Execute(nhQuery));
}
protected abstract TResult Execute(IQuery query);
protected virtual IQuery GetNamedQuery()
{
var nhQuery = session.GetNamedQuery(QueryName);
SetParameters(nhQuery);
return nhQuery;
}
protected abstract void SetParameters(IQuery nhQuery);
public virtual string QueryName
{
get { return GetType().Name; }
}
}
[Test]
public void NamedQueryCheck()
{
var errors = new StringBuilder();
var queryObjectTypes = GetNamedQueryObjectTypes();
var mappedQueries = GetNamedQueryNames();
foreach (var queryType in queryObjectTypes)
{
var query = GetQuery(queryType);
if (!mappedQueries.Contains(query.QueryName))
{
errors.AppendFormat(
"Query object {0} references non-existent " +
"named query {1}.",
queryType, query.QueryName);
errors.AppendLine();
}
}
if (errors.Length != 0)
Assert.Fail(errors.ToString());
}
private IEnumerable<Type> GetNamedQueryObjectTypes()
{
var namedQueryType = typeof(INamedQuery);
var queryImplAssembly = typeof(BookWithISBN).Assembly;
var types = from t in queryImplAssembly.GetTypes()
where namedQueryType.IsAssignableFrom(t)
&& t.IsClass
&& !t.IsAbstract
select t;
return types;
}
private IEnumerable<string> GetNamedQueryNames()
{
var nhCfg = NHConfigurator.Configuration;
var mappedQueries = nhCfg.NamedQueries.Keys
.Union(nhCfg.NamedSQLQueries.Keys);
return mappedQueries;
}
private INamedQuery GetQuery(Type queryType)
{
return (INamedQuery) Activator.CreateInstance(
queryType,
new object[] { SessionFactory });
}
public interface IBookWithISBN : IQuery<Book>
{
string ISBN { get; set; }
}
public class BookWithISBN :
NamedQueryBase<Book>, IBookWithISBN
{
public BookWithISBN(ISessionFactory sessionFactory)
: base(sessionFactory) { }
public string ISBN { get; set; }
protected override void SetParameters(
NHibernate.IQuery nhQuery)
{
nhQuery.SetParameter("isbn", ISBN);
}
protected override Book Execute(NHibernate.IQuery query)
{
return query.UniqueResult<Book>();
}
}
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping >
<query name="BookWithISBN">
<![CDATA[
from Book b where b.ISBN = :isbn
]]>
</query>
</hibernate-mapping>
As we learned in the previous recipe, according to the repository pattern, the repository is responsible for fulfilling queries, based on the specifications submitted to it. These specifications are limiting. They only concern themselves with whether a particular item matches the given criteria. They don't care for other necessary technical details, such as eager loading of children, batching, query caching, and so on. We need something more powerful than simple where clauses. We lose too much to the abstraction.
The query object pattern defines a query object as a group of criteria that can self-organize in to a SQL query. The query object is not responsible for the execution of this SQL. This is handled elsewhere, by some generic query runner, perhaps inside the repository. While a query object can better express the different technical requirements, such as eager loading, batching, and query caching, a generic query runner can't easily implement those concerns for every possible query, especially across the half-dozen query APIs provided by NHibernate.
These details about the execution are specific to each query, and should be handled by the query object. This enhanced query object pattern, as Fabio Maulo has named it, not only self-organizes into SQL but also executes the query, returning the results. In this way, the technical concerns of a query's execution are defined and cared for with the query itself, rather than spreading into some highly complex, generic query runner.
According to the abstraction we've built, the repository represents the collection of entities that we are querying. Since the two are already logically linked, if we allow the repository to build the query objects, we can add some context to our code. For example, suppose we have an application service that runs product queries. When we inject dependencies, we could specify IQueryFactory directly. This doesn't give us much information beyond "This service runs queries." If, however, we inject IRepository<Product>, we have a much better idea about what data the service is using.
The IQuery interface is simply a marker interface for our query objects. Besides advertising the purpose of our query objects, it allows us to easily identify them with reflection.
The IQuery<TResult> interface is implemented by each query object. It specifies only the return type and a single method to execute the query.
The IQueryFactory interface defines a service to create query objects. For the purpose of explanation, the implementation of this service, QueryFactory, is a simple service locator. IQueryFactory is used internally by the repository to instantiate query objects.
The NamedQueryBase class handles most of the plumbing for query objects, based on named HQL and SQL queries. As a convention, the name of the query is the name of the query object type. That is, the underlying named query for BookWithISBN is also named BookWithISBN. Each individual query object must simply implement SetParameters and Execute(NHibernate.IQuery query), which usually consists of a simple call to query.List<SomeEntity>() or query.UniqueResult<SomeEntity>().
The INamedQuery interface both identifies the query objects based on Named Queries, and provides access to the query name. The NamedQueryCheck test uses this to verify that each INamedQuery query object has a matching named query.
Each query has an interface. This interface is used to request the query object from the repository. It also defines any parameters used in the query. In this example, IBookWithISBN has a single string parameter, ISBN. The implementation of this query object sets the :isbn parameter on the internal NHibernate query, executes it, and returns the matching Book object.
Finally, we also create a mapping containing the named query BookWithISBN, which is loaded into the configuration with the rest of our mappings.
The code used in the query object setup would look like the following code:
var query = bookRepository.CreateQuery<IBookWithISBN>();
query.ISBN = "12345";
var book = query.Execute();
In this article we learned how to transact Auto-wrapping for the data access layer, setting up an NHibernate repository and how to use Named Queries in the data access layer
Further resources on this subject: