Using named queries in the data access layer

Named queries encapsulated in query objects are a powerful combination. In this recipe, we will show you how to use named queries with your data access layer.

Getting ready

To complete this recipe, you will need the common service locator from Microsoft patterns and practices. The documentation and source code can be found at http://commonservicelocator.codeplex.com.

Complete the previous recipe: Setting up an NHibernate repository.

Following Fast testing with SQLite in-memory database recipe in Chapter 6, Testing, create a new NHibernate test project named Eg.Core.Data.Impl.Test.

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"/>

How to do it…

  1. In the Eg.Core.Data project, add a folder for the Queries namespace.
  2. Add the following IQuery interfaces:
    public interface IQuery
    {
    }
    
    public interface IQuery<TResult> : IQuery 
    {
      TResult Execute();
    }
  3. Add the following IQueryFactory interface:
    public interface IQueryFactory
    {
      TQuery CreateQuery<TQuery>() where TQuery :IQuery;
    }
  4. Change the IRepository interface to implement the IQueryFactory interface, as shown in the following code:
    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);
    }
  5. In the Eg.Core.Data.Impl project, change the NHibernateRepository constructor and add the _queryFactory field, as shown in the following code:
    private readonly IQueryFactory _queryFactory;
    
    public NHibernateRepository(
      ISessionFactory sessionFactory,
      IQueryFactory queryFactory)
      : base(sessionFactory)
    {
      _queryFactory = queryFactory;
    }
  6. Add the following method to NHibernateRepository:
    public TQuery CreateQuery<TQuery>() where TQuery : IQuery
    {
      return _queryFactory.CreateQuery<TQuery>();
    }
  7. In the Eg.Core.Data.Impl project, add a folder for the Queries namespace.
  8. Install the common service locator using the NuGet Package Manager Console, using the following command:
      Install-Package CommonServiceLocator
    
  9. To the Queries namespace, add this QueryFactory class:
    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>();
      }
    }
  10. Add the following NHibernateQueryBase class:
    public abstract class NHibernateQueryBase<TResult> 
      : NHibernateBase, IQuery<TResult>
    {
      protected NHibernateQueryBase(
        ISessionFactory sessionFactory)
        : base(sessionFactory) { }
    
      public abstract TResult Execute();
    }
  11. Add an empty INamedQuery interface, as shown in the following code:
    public interface INamedQuery
    {
      string QueryName { get; }
    }
  12. Add a NamedQueryBase class, as shown in the following code:
    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; }
      }
    }
  13. In Eg.Core.Data.Impl.Test, add a test fixture named QueryTests inherited from NHibernateFixture.
  14. Add the following test and three helper methods:
    [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 });
    }
  15. For our example query, in the Queries namespace of Eg.Core.Data add the following interface:
    public interface IBookWithISBN : IQuery<Book>
    {
      string ISBN { get; set; }
    }
  16. Add the implementation to the Queries namespace of Eg.Core.Data.Impl using the following code:
    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>();
      }
    }
  17. Finally, add the embedded resource mapping, BookWithISBN.hbm.xml, to Eg.Core.Data.Impl with the following xml code:
    <?xml version="1.0" encoding="utf-8" ?>
    <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
      <query name="BookWithISBN">
        <![CDATA[
        from Book b where b.ISBN = :isbn
        ]]>
      </query>
    </hibernate-mapping>

How it works…

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 or not. They don't care about other necessary technical details, such as the eager loading of children, batching, query caching, and so on. We need something more powerful than simple where clauses. We in other words lose too much to the abstraction.

The query object pattern defines a query object as a group of criteria that can self-organize into 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, technical concerns in respect 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 will 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 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 parameter 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:

var query = bookRepository.CreateQuery<IBookWithISBN>();
query.ISBN = "12345";
var book = query.Execute();

See also

  • Transaction auto-wrapping for the data access layer
  • Setting up an NHibernate repository
  • Using ICriteria in the data access layer
  • Using paged queries in the data access layer
  • Using LINQ specifications in the data access layer
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.45.5