NHibernate cache system. Part 2

In the previous post we saw how the cache system is structured in NHibernate and how it works. We saw that we have different methods to play with the cache (Evict, Clear, Flush …) and they are all associated with the ISession object because the cache of level 1 is associated with the lifecycle of an ISession object.

In this second article we will see how the second level cache works and how it is associated with the ISessionFactory object that is in charge of controlling this cache mechanism.

Second Level cache architecture

How does the second level cache work?

image

First of all, when an entity is cached in the second level cache, the entity is disassembled into a collection of keys/values pair, like a dictionary and persisted in the cache repository. This mechanism is accomplished because most of the second level cache providers are able to persist serialized dictionary collections and because in the same time NHibernate does not force you to make serializable your entities (something that IMHO, should never be done!!).

A second mechanism happens when we cache the result of a query (Linq, HQL, ICriteria) because these results can’t be cached using the first level cache (see previous blog post). After we cache a query result, NHibernate will cache only the unique identifiers of the entities involved in the result of the query.

Third, NHibernate has an internal mechanism that allows him to know and keep track of a timestamp value used to write tables or to work with sessions. How does it work? Well the mechanism is pretty clear, it keeps track of when the last table was written too. A series of mechanism will update this timestamp information and you can find a better explanation of Ayende’s blog: http://ayende.com/blog/3112/nhibernate-and-the-second-level-cache-tips.

Configuration of the second level cache

By default the second level cache is disabled. If you need to use the second level cache you have to let NHibernate know about that. The hibernate.cfg file has a dedicated section of parameters that should be used to enable the second level cache:

<property name="cache.provider_class">
   NHibernate.Cache.HashtableCacheProvider
</property>
<!-- You have to explicitly enable the second level cache ->
<property name="cache.use_second_level_cache">
   true
</property> 

First of all we specify the cache provider we are using, in this case I am using the standard hashtable provider, but I will show you in the next article what are the real providers you should use. Second we say that the cache should be enabled; this part is really important because if you do not specify that the cache is enable, it simply won’t work … Confused smile

Then you may provide to the cache a default expiration in seconds:

<!-- cache will expire in 2 minutes -->
<property name="cache.default_expiration">120</property>

If you want to add additional configuration properties, they will be cache provider specific!

Cache by mapping

One of the possible configuration is to enable the cache at the entity level. This means that we are marking our entity as “cachable”.

<class
   name="CachableProduct“
   table="[CachableProduct]“
   dynamic-insert="true“
   dynamic-update="true">
   <cache usage="read-write"/>

In order to do that we have to introduce a new tag, the <cache> tag. In this tag we can specify different type of “”usage”:

  • Read-write

    It should be used if you plan also to update the data (no with serializable transaction)

  • Read-only

    Simplest and best performing, for read only access

  • Nonstrict-read-write

    If you need to occasionally update the data. You must commit the transaction

  • Transactional

    not documented/implemented yet because no one cache provider allows transactional cache. It is implemented in the Java version because J2EE allow transactional second level cache

Now, if we write a simple test that will create some entities and will try to retrieve them using two different ISession generated by the same ISessionFactory we will get the following behavior:

using (var session = factory.OpenSession())
{
   // create the products
    using (var tx = session.BeginTransaction(IsolationLevel.ReadCommitted))
    {
        Console.WriteLine("*** FIRST SESSION ***");
        var expectedProduct1 = session.Get<CachableProduct>(productId);
        Assert.That(expectedProduct1, Is.Not.Null);
        tx.Commit();
        // retrieve the number of hits we did to the 2nd level cache
        Console.WriteLine("Second level hits {0}", 
           factory.Statistics.SecondLevelCacheHitCount);
    }
}
using (var session = factory.OpenSession())
{
    using (var tx = session.BeginTransaction(IsolationLevel.ReadCommitted))
    {
        Console.WriteLine("*** SECOND SESSION ***");
        var expectedProduct1 = session.Get<CachableProduct>(productId);
        Assert.That(expectedProduct1, Is.Not.Null);
        tx.Commit();
        // retrieve the number of hits we did to the 2nd level cache
        Console.WriteLine("Second level hits {0}", 
           factory.Statistics.SecondLevelCacheHitCount);
    }
}

The result will be the following:

image

As you can see the second session will access the 2nd level cache using a transaction and will not use the database at all. This has been accomplished just by mapping the entity with the <cache> tag and by using the GET<T> method.

Let’s make everything a little bit more complex. Let’s assume for a second that our object is an aggregate root and it is more complex than the previous one. If we want to cache also a collection of child or a parent reference we will need to change our mapping in the following way:

<!-- inside the product mapping file -->
<bag name="Attributes" cascade="all" inverse ="true">
  <cache usage="read-write"/>
  <key column="ProductId" />
  <one-to-many class="CacheAttribute"/>
</bag> 
<!-- inside theCacheAttribute file -->
<class
    name="CacheAttribute"
    table="[CacheAttribute]"
    dynamic-insert="true"
    dynamic-update="true">
  <cache usage="read-write"/>
  <!-- omit -->
  <many-to-one 
     class="CachableProduct" 
     name="Product" cascade="all">   
    <column name="ProductId" />
  </many-to-one>
</class>

Now we can execute the following test (I am omitting some parts for saving space, I hope you don’t mind …)

using (var session = factory.OpenSession())
{
    using (var tx = session.BeginTransaction(IsolationLevel.ReadCommitted))
    {
        Console.WriteLine("*** FIRST SESSION ***");
        var expectedProduct1 = session.Get<CachableProduct>(productId);
        Assert.That(expectedProduct1, Is.Not.Null);
        Assert.That(expectedProduct1.Attributes, Has.Count.GreaterThan(0));
        tx.Commit();
        Console.WriteLine("Second level hits {0}", 
           factory.Statistics.SecondLevelCacheHitCount);
    }
 
}
using (var session = factory.OpenSession())
{
    using (var tx = session.BeginTransaction(IsolationLevel.ReadCommitted))
    {
        Console.WriteLine("*** SECOND SESSION ***");
        var expectedProduct1 = session.Get<CachableProduct>(productId);
        Assert.That(expectedProduct1, Is.Not.Null);
        Assert.That(expectedProduct1.Attributes, Has.Count.GreaterThan(0));
        tx.Commit();
        Console.WriteLine("Second level hits {0}", 
           factory.Statistics.SecondLevelCacheHitCount);
    }
}

And this is the result from the profiled SQL:

image

In this case the second ISession is calling the cache 4 times in order to resolve all the objects (2 products x 2 categories).

Cache a query result

Another way to cache our result is by creating a cachable query that is slightly different than creating a cachable object.

Important note:

In order to cache a query we need to set the query as “cachable” and then set the corresponding entity as “cachable” too. Otherwise NHB will cache the ID of the entity but then it will always fetch the entity and cache only the query result.

To write a cachable query we need to implement an IQuery object in the following way:

using (var session = factory.OpenSession()) {
     using (var tx = session.BeginTransaction(IsolationLevel.ReadCommitted))
     {
        var result = session
             .CreateQuery("from CachableProduct p where p.Name = :name")
             .SetString("name", "PC")
             .SetCacheable(true)
             .List<CachableProduct>();
        tx.Commit();
     }
 }

Now, let’s try to write a unit test for this:

using (var session = factory.OpenSession())
{
    using (var tx = session.BeginTransaction(IsolationLevel.ReadCommitted))
    {
        Console.WriteLine("*** FIRST SESSION ***");
        var result = session
            .CreateQuery("from CachableProduct p where p.Name = :name")
            .SetString("name", "PC")
            .SetCacheable(true)
            .List<CachableProduct>();
        Console.WriteLine("Cached queries {0}", 
             factory.Statistics.QueryCacheHitCount);
        Console.WriteLine("Second level hits {0}", 
             factory.Statistics.SecondLevelCacheHitCount);
        tx.Commit();
    }
}
using (var session = factory.OpenSession())
{
    using (var tx = session.BeginTransaction(IsolationLevel.ReadCommitted))
    {
        Console.WriteLine("*** SECOND SESSION ***");
        var result = session
            .CreateQuery("from CachableProduct p where p.Name = :name")
            .SetString("name", "PC")
            .SetCacheable(true)
            .List<CachableProduct>();
        Console.WriteLine("Cached queries {0}", 
             factory.Statistics.QueryCacheHitCount);
        Console.WriteLine("Second level hits {0}", 
             factory.Statistics.SecondLevelCacheHitCount);
        tx.Commit();
    }
}

And this is the expected result:

image

In this case the cache is telling us that the second session has 1 query result cached and that we called it once.

Final advice

As you saw using the 1st and 2nd level cache is a pretty straightforward process but it requires time and understanding of NHibernate cache mechanism. Below are some final advice that you should keep in consideration when working with the 2nd level cache:

  • 2nd Level Cache is never aware of external database changes!
  • Default cache system is hashtable, you must use a different one
  • Wrong implementation of the 2nd level cache may result in a non expected performance degrade (i.e. hashtable doc)
  • First level cache is shared across same ISession, second level is shared across same ISessionFactory

In the next article we will see what are the available cache providers.