NHibernate cache system. Part 1

In this series of articles I will try to explain you how NHibernate cache system works and how it should be used in order to get the best performance/configuration from this product.

NHibernate Cache architecture

NHibernate has an internal cache architecture that I will define absolutely well done. On an architectural point of view, it is designed for the enterprise and it is 100% configurable. Consider that it allows you to create also your custom cache provider!

The following picture show the cache architecture overview of NHibernate (actually the version I am talking about is the 3.2 GA).

image

The cache system is composed by two levels, the cache of level 1 that usually it is configured by default if you are working with the ISession object, and the cache of level 2 that by default is disabled.

The cache of level 1 is provided by the ISession data context and it is maintained by the lifecycle of the ISession object, this means that as soon as you destroy (dispose) an ISession object, also the cache of level 1 will be destroyed and all the corresponding objects will be detached from the ISession. This cache system works on a per transaction basis and it is designed to reduce the number of database calls during the lifecycle of an ISession. As an example, you should use this cache if you have the need to access and modify an object in a transaction, multiple times.

The cache of level 2 is provided by the ISessionFactory component and it is shared across all the session created using the same factory. Its lifecycle correspond to the lifecycle of the session factory and it provides a more powerful but also dangerous set of features. It allows you to keep objects in cache across multiple transactions and sessions; the objects are available everywhere and not only on the client that is using a specific ISession

Cache Level 1 mechanism

As soon as you start to create a new ISession (not an IStatelessSession!!) NHibernate starts to holds in memory, using a specific mechanism, all the objects that are involved with the current session. The methods used by NHibernate to load the data into the cache are two: Get<T>(id) and Load<T>(id). This means that if you try to load one or more entities using: LinQ, HQL, ICriteria … NHibernate will not put them into the cache of level 1.

Another way to put an object into the lvl1 cache is to use persistence methods like Save, Delete, Update and SaveOrUpdate.

image

As you can see from the previous picture, the ISession object is able to contains two different categories of entities, the one that we define “loaded” using Get or Load and the one that we define “dirty”, which means that they were somehow modified and associated with a session.

Load and Get, what’s the difference?

A major confusion I personally noticed while working with NHibernate is the not correct usage of the two methods Get and Load so let’s see for a moment how they work and when they should or should not be used.

Get

Load

 

Fetch method

Retrieve the entire entity in one SELECT statement and puts the entity in the cache.

Retrieve only the ID of the entity and returns a non fetched proxy instance of the entity. As soon as you “hit” a property, the entity is loaded.

How it loads

It verifies if the entity is in the cache, otherwise it tries to execute a SELECT

It verifies if the entity is in the cache, otherwise it tries to execute a SELECT

Not Available

If the entity does not exist, it returns NULL

If the entity does not exist, it THROW an exception

 

I personally prefer Get because it returns NULL instead of throwing a nasty exception, but this is a personal choice; while some of you may prefer to use Load because you want to avoid a database call until is really needed.

Below I wrote a couple of very simple tests to show you how the Get and Load methods work across the same ISession.

Get<T>()

   1:  [Test]
   2:  [Category("Database")]
   3:  public void UsingGetThePersonIsFullyCached()
   4:  {
   5:      using (var session = factory.OpenSession())
   6:      {
   7:          using (var tx = session.BeginTransaction(IsolationLevel.ReadCommitted))
   8:          {
   9:              var persons = MockFactory.MakePersons();
  10:              persons.ForEach(p => session.Save(p));
  11:              tx.Commit();
  12:              session.Clear();
  13:              Console.WriteLine("*** FIRST SELECT ***");
  14:              var expectedPerson1 = session.Get<Person>(persons[0].PersonId);
  15:              Assert.That(expectedPerson1, Is.Not.Null);
  16:              Console.WriteLine("*** SECOND SELECT ***");
  17:              var expectedPerson2 = session.Get<Person>(persons[0].PersonId);
  18:              Assert.That(expectedPerson2, Is.Not.Null);
  19:              Assert.That(expectedPerson2.FirstName, Is.Not.EqualTo(string.Empty));
  20:          }
  21:      }
  22:  }

In this test I have created a list of Persons in one transaction and then I cleared the session in order to be sure that nothing was left in the cache. Then I loaded one of the Person entities using the Get<T> method and then I load it again using the same method call in order to verify that the SELECT statement was issued only once.

image

As you can see, NHibernate is loading the entire entity from the database in the first call, and in the second one is simply loading it again from the level 1 cache. You should notice here that NHibernate is loading the entire entity in the first Get<T> call.

Load<T>()

[Test]
[Category("Database")]
public void UsingLoadThePersonIsPartiallyCached()
{
    using (var session = factory.OpenSession())
    {
        using (var tx = session.BeginTransaction(IsolationLevel.ReadCommitted))
        {
            var persons = MockFactory.MakePersons();
            persons.ForEach(p => session.Save(p));
            tx.Commit();
            session.Clear();
            Console.WriteLine("*** FIRST SELECT ***");
            var expectedPerson1 = session.Load<Person>(persons[0].PersonId);
            Assert.That(expectedPerson1, Is.Not.Null);
            Console.WriteLine("*** SECOND SELECT ***");
            var expectedPerson2 = session.Load<Person>(persons[0].PersonId);
            Assert.That(expectedPerson2, Is.Not.Null);
            Assert.That(expectedPerson2.FirstName, Is.Not.EqualTo(string.Empty));
        }
    }
}

In this second test I am executing the same exact steps of the previous one, but this time I am using the Load<T> method and the result is completely different! Look at the SQL log below:

image

Now NHibernate is not loading the entity from the database at all, it is loading it only in the second call, when I try to hit one of the Person properties. If you debug this code you will notice that NHibernate issues the database call at the line Assert.That(expectedPerson2.FirstName, Is.Not.EqualTo(string.Empty)); and not before!

Session maintenance

If you are working with the ISession object in a Client application or if you are keeping it alive in a web application using some strange behaviors like keeping it saved inside the HttpContext you will realize, soon or later, that sometimes the cache of level 1 needs to be cleared.

Now, despite the fact that these methods (based on my personal experience) should never be used, because it means that you are wrongly implementing your data layer, and despite the fact that the behavior of these methods may result in something unexpected, NHibernate provides three different methods to clear the cache of level 1 content.

 

Session.Clear

 

Session.Evict

 

Session.Flush

 
             
 

Removed all the existing objects from the ISession without syncing them with the database

Remove a specific object from the ISession without syncing it with the database

Remove all the existing objects from the session by syncing them with the database

I will probably write more about these three methods in some future post but if you need to investigate more about them, I would suggest you to read carefully the NHibernate docs available here: http://www.nhforge.org/doc/nh/en/index.html

In the next article we talk about the level 2 cache.