Caching

We have briefly touched the topic of caching in multiple places earlier. Caching support in NHibernate is more extensive than most of the frameworks that I have come across so far. Following are the three different types of caches available in NHibernate:

  • Session level cache: ISession caches every entity that it loads from the database. This is default behavior and cannot be changed or turned off.
  • Query cache: At individual query level, you can specify if the output of that query needs to be cached. This cache is shared across sessions.
  • Second level cache: This is similar to session level cache but it is shared across all the sessions in a session factory. Due to its shared nature, this cache is most complex of all and comes with settings that you can use to fine tune use of second level cache in your application.

Let's dive into each type of cache in more detail to understand how to enable, disable, and make use of each cache.

Session level cache

We have come across references to session level cache on more than one occasion in the previous chapters. Session level cache, as the name suggests, is scoped to a session. You may hear some people refer to it as first level cache. Every new ISession instance that we create would have a cache associated with it. There is no way to enable or disable this cache. This is always enabled and it works transparently to our data access code. This has a huge benefit as we do not need to do any setup or write our code in a particular way to make use of this cache. Every time NHibernate loads any entity instance, it is kept in the session level cache. Next time you try to retrieve the same instance using its identifier, NHibernate would return it from the cache instead of going to the database.

Sometimes it is confusing to understand how the session level caching works in different situations. For example, in the following code listing, the first call to ISession.Get<Employee> would be returned from the cache but the second query would hit the database:

[Test]
public void SessionLevelCacheIsUsedOnlyForGet()
{
  object id = 0;
  using (var tx1 = Session.BeginTransaction())
  {
    id = Session.Save(new Employee
    {
      Firstname = "John",
      Lastname = "Smith"
    });

    tx1.Commit();
  }

  using (var tx2 = Session.BeginTransaction())
  {
    var employee = Session.Get<Employee>(id);
    Assert.That(employee.CreatedAt,
                Is.Not.EqualTo(DateTime.MinValue));
    tx2.Commit();
  }

  using (var tx2 = Session.BeginTransaction())
  {
    var employee = Session.Query<Employee>().
    First(e => e.Firstname == "John");
    Assert.That(employee.CreatedAt, Is.Not.EqualTo(DateTime.MinValue));
    tx2.Commit();
  }
}

A rule of thumb to remember with this caching behavior is that only ISession.Get<T> and ISession.Load<T> would go via this cache; custom queries written using any of HQL, Criteria, QueryOver, or LINQ would always hit the database. This is because the session level cache holds the entity instances against their identifiers, so if you are retrieving entities by their identifiers, only then it is possible to use the session level cache. Since the session level cache is a map of identifiers, it is also called identity map. There are times when NHibernate would internally make use of this identity map to lazily load some entities.

Second level cache

Fundamental difference between the session level cache and second level cache is the scope in which they operate. While the session level cache is only limited to a single session object, the second level cache spans across all the sessions created from a session factory. Scope of the second level cache extends until the session factory. This extended scope means that NHibernate has to carefully handle multithreaded and concurrent cache accesses.

To that end, NHibernate's second level cache provides a setting called cache usage strategy, which dictates how a cache can be accessed by different sessions. We would look into different cache usage strategies but before that, let's look into enabling second level cache. To enable second level cache, we need to configure a caching provider that we want to use. Caching provider is a class that implements the NHibernate.Cache.ICacheProvider interface. Out-of-the-box NHibernate provides an in-memory, hashtable-based implementation of this interface named NHibernate.Cache.HashtableCacheProvider.

This is not meant to be used in production but feel free to use it in development and test environments. Other than this, there are community implementations of ICacheProvider based on ASP.NET cache, Redis, memcached, AppFabric caching, NCache, SysCache, and the like. If you search using the term NHibernate.Caches on NuGet package repository then you would find all the available implementations of the second level cache provider. Readers are encouraged to do their own research before they decide on a particular cache provider. Following code shows how to configure a caching provider using programmatic configuration:

public class SqlServerDatabaseConfiguration : Configuration
{
  public SqlServerDatabaseConfiguration()
  {
    this.Cache(cache =>
    {
      cache.Provider<HashtableCacheProvider>();
    });
  }
}

Note that there usually is lot of other configuration in the preceding code. To save space, we are only showing the configuration of the caching provider. Once the caching provider is configured, we need to specify the cache usage strategy. This needs to be specified at individual class or collection level. Following are the three strategies available:

  • Read only: Use this strategy if your application only needs to read the data from cache but never write it back. This is simplest to use, most performant, and is cluster-safe.
  • Read write: If your application needs to update the data, then the read-write strategy is more appropriate. Second level cache provides a reliable world-view of data to your application. It is important that updates to the cache are aligned with updates to the database. Updates to the database happen when transaction is committed. Isolation level of a transaction has an impact on the behavior of commit and the end result overall. This also has an indirect effect on the caching provider's ability to work with particular isolation levels. This is a complex topic to understand but the point to note here is that the read-write strategy does not work if serializable transaction isolation level is used.
  • Non-strict read write: If your application updates data very rarely and most of the time just reads the data, then it is highly unlikely that two transactions would update the same data concurrently. In such s situation, the caching provider does not need to lock items during updates, which provides some freedom to the caching provider while working with transaction isolation level. A non-strict read write strategy does not put any restriction on what isolation level you can use for your transactions.

Following code snippet shows how to specify the cache usage strategy during mapping of an entity:

public class AddressMappings : ClassMapping<Address>
{
  public AddressMappings()
  {
    //Mapping of other properties
    Cache(c =>
    {
      c.Usage(CacheUsage.Transactional);
      c.Region("AddressEntries");
    });
  }
}

We have used enumeration NHibernate.Mapping.ByCode.CacheUsage to specify what cache usage strategy we want to use. We have also specified a name for the region in the cache where instances of this entity should be cached. This is handy if you want a better organization of entities in the cache.

Usage strategy can also be specified on collections of an entity. In the next code sample, we specify cache usage strategy on the Benefits collection of the Employee entity:

Set(e => e.Benefits, mapper =>
{
  mapper.Key(k => k.Column("Employee_Id"));
  mapper.Cascade(Cascade.All.Include(Cascade.DeleteOrphans));
  mapper.Inverse(true);
  mapper.Lazy(CollectionLazy.Extra);
  mapper.Cache(c =>
  {
    c.Usage(CacheUsage.ReadWrite);
    c.Region("Employee_Benefits");
  });
},
relation => relation.OneToMany(mapping => mapping.Class(typeof(Benefit))));

Note

In theory, the second level cache is scoped to a session factory but it really depends on the implementation of the cache provider. A distributed cache provider such as NCache can be configured to provide a second level cache that can be shared between multiple session factories belonging to multiple applications.

Query cache

Query level cache, as the name suggests, can be enabled on individual queries. Below you will see a query that we have used in one of the chapters in this book. We have turned on the query cache for this query by calling Cacheable at the end.

var employees = Database.Session.Query<Employee>()
               .Where(e => e.Firstname == "John")
               .Cacheable();

Before we can cache the results of any query into query cache, it must be turned on in the NHibernate configuration. Following code snippet shows how to turn on the query cache using loquacious configuration:

var config = new Configuration();
config.Cache(cache =>
{
  cache.UseQueryCache = true;
});

After caching the query, you might expect that the next time this same query is executed then NHibernate would not need to go to the database and the results would be returned from the query cache. That is partially true and that is where the query cache gets interesting. Query cache in reality only caches the identifiers of the entities that form the result of the query. For the actual entity state, the query cache relies on the second level cache. So this is how it works internally – we execute a query that is marked Cacheable. For the first time, this query is executed against the database. Identifiers of all the entities returned from the database are stored in cache. Next time the same query is executed, NHibernate loads the identifiers from the query cache and retrieves the entity instances corresponding to those identifiers from the second level cache. If the entity instances are not present in the second level cache for any reason, then NHibernate goes to the database to load those entities.

There are two important things coming out from the previous discussion. One, in order for the query cache to work, the second level cache must be turned on. Second, if you use the query cache without the second level cache, then you may end up making the queries slower than they would normally run. This happens because of the way cached queries are executed. If you have got a cached query that returns 50 records which we expect to be present in second level cache. If they are not, then NHibernate would make 50 database trips to bring each record individually, after it has loaded the identifiers for each of those 50 records from the query cache. In such instances, if we disable the query cache then NHibernate would only make one database trip to fetch all the 50 records every time we run that query. This approach may be faster than hitting the database 50 times.

Now that you know the different caching mechanisms offered by NHibernate, you may be tempted to turn on caching at every level to get a performance boost. But my advice to you would be to use caching as a last resort and use it selectively. If you encounter a slow running query then try to fix that query first. See if the query is written in a wrong way and check if we can make use of other features such as join fetching, batch fetching, and so on, to get better SQL commands or better utilization of the database roundtrips. It is very easy to hide the underlying issue in queries by using caching to make everything look faster. Also, make sure that you do not cache entities that should not be cached. If there are other applications that are connecting to the same database as yours, then it is possible that the entities cached by your application are updated by other applications, making the cache entries in your application stale. Changes made by one application would not be visible to the other application. Situations like these and many more determine how to best use caching or whether to use it at all. Make sure you have ticked all the boxes before you turn on caching.

..................Content has been hidden....................

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