Using SysCache as secondary cache in NHibernate

Disclaimer: this is a roundup of things I got from the NHibernate documentation, the ufficial forum or some random blog post, they seem to work for me but I'm not completely sure this is the "correct" way of doing things.

NHibernate logo The custom CMS I'm currently working on (and that is managing this website) relies on NHibernate as its O/RM, which currently provides two separate caching mechanisms.

The first one is the first level cache. It's a local cache to each separate NHibernate Session (a single ADO connection to the database, that usually is opened when the server receives an HTTP request and is closed again when the request is completed). NHibernate will keep all objects associated to that particular session in its cache. This cache is lost as soon as the session is disposed.

NHibernate also allows you to select one of the many cache providers you can find on NHForge.org as a second level cache. This type of cache is persistent, lives across multiple request and is used by all sessions concurrently. Roughly speaking, when an object instance is fetched from the database all values of the object are stored in the cache. When the same object is requested again, NHibernate will dehydrate the object using the values found in the cache which are associated to that particular identifier of the object.

This means that no instances are stored in the cache, but only values. As explained in the documentation, this means that if you manipulate objects loaded through NHibernate you won't risk disrupting the cache, while relationships and associations between instances will always be consistent.

Installing the SysCache provider

There are several cache providers available: in my case I decided to use NHibernate.Caches.SysCache which is built upon the default ASP.NET cache provider. This will only work on a .NET web server of course. Other cache providers may have other settings than those I describe in the rest of the post.

Firstly download the cache pack from NHForge.org and copy NHibernate.Caches.SysCache.dll to the /Bin folder of your webserver. Some properties need to be set in the web.config file in order to ensure that NHibernate will actually use the cache provider:

<configuration>

	<configSections>
		<section name="hibernate-configuration" type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" requirePermission="false"/>
		<section name="syscache" type="NHibernate.Caches.SysCache.SysCacheSectionHandler, NHibernate.Caches.SysCache" requirePermission="false" />
	</configSections>

	<!-- NHibernate -->
	<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
		<session-factory>
			<!-- dialect, connection string, etc... -->

			<property name="cache.provider_class">NHibernate.Caches.SysCache.SysCacheProvider, NHibernate.Caches.SysCache</property>
			<property name="cache.use_second_level_cache">true</property>
		</session-factory>
	</hibernate-configuration>
	
	<!-- Caching -->
	<syscache>
		<cache region="LongTerm" expiration="3600" priority="5" />
		<cache region="ShortTerm" expiration="900" priority="3" />
	</syscache>
	
	<-- ... -->

The cache.provider_class property tells NHibernate which cache implementation to use and the cache.use_second_level_cache flag should ensure that the cache provider is actually used.

In the last section you see above, I set up a couple of query regions: a query region is a partition of the whole cache that can keep a certain subset of the entities managed by NHibernate, using individual priorities (the likelihood that an object of that region evicts another already cached object of a lower priority region) and expiration values (the time in seconds after which an object is automatically evicted from the cache and this will be fetched again from the database).

The values above are almost completely random: you'll have to measure the performance of your application and adapt them accordingly.

Update all class mappings

In order to use the cache regions defined above, you need to update all XML mapping of your entities with a special <cache /> tag. For instance:

<?xml version="1.0" encoding="utf-8" ?>

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Assembly" namespace="Namespace">
	<class name="YourEntity" table="YourTable">
		<cache usage="nonstrict-read-write" region="LongTerm" />

		<id name="Id" type="Int32" column="Id" access="field.pascalcase-underscore">
			<generator class="identity" />
		</id>

		<!-- properties, sets, bags, etc... -->

	</class>
</hibernate-mapping>

The "region" attribute of the cache tag determines in which of the two Cache regions (defined above) this particular entity is mapped to. All cached values from instances of this entity will be kept in the cache with the expiration and priority values you define in your web.config file.

The "usage" attribute can assume one of the following values, depending on how aggressively you intend to cache your entities:

  • read-only: most aggressive caching option, can only be used if your entities are immutable. NHibernate will raise an exception if you try to update or save an entity with this caching setting, but it will also give you the best caching performance.
  • read-write: the most conservative option, will ensure that if an object is updated in the database it is also evicted from all caches. This will ensure that caching doesn't return any stale data, but has a negative impact on performance and has some limitations if you run on a cluster (not my case  :)).
  • nonstrict-read-write: this option will ensure that updated objects are eventually removed from all caches, but doesn't guarantee when this happens. You might get stale data from NHibernate in this case, but if your application is more likely to read data than to write it, this shouldn't be a problem.

In my case all entities use the nonstrict-read-write setting and everything is working fine. Your mileage may vary, though.

Is it working?

To check whether the secondary cache is working or not, set your log4net NHibernate logger to the "DEBUG" level, run two data loading functions of your application for the same data and then open the logs. You should find something like this:

...
19:43:52 DEBUG NHibernate.Event.Default.DefaultLoadEventListener - loading entity: [Babil.Data.Setting#PreferencesCookieName]
19:43:52 DEBUG NHibernate.Event.Default.DefaultLoadEventListener - attempting to resolve: [Babil.Data.Setting#PreferencesCookieName]
19:43:52 DEBUG NHibernate.Cache.NonstrictReadWriteCache - Cache lookup: Babil.Data.Setting#PreferencesCookieName
19:43:52 DEBUG NHibernate.Caches.SysCache.SysCache - Fetching object 'NHibernate-Cache:Settings:Babil.Data.Setting#PreferencesCookieName@-83065912' from the cache.
19:43:52 DEBUG NHibernate.Cache.NonstrictReadWriteCache - Cache miss
19:43:52 DEBUG NHibernate.Event.Default.DefaultLoadEventListener - object not resolved in any cache: [Babil.Data.Setting#PreferencesCookieName]
19:43:52 DEBUG NHibernate.Persister.Entity.AbstractEntityPersister - Fetching entity: [Babil.Data.Setting#PreferencesCookieName]
19:43:52 DEBUG NHibernate.Loader.Loader - loading entity: [Babil.Data.Setting#PreferencesCookieName]
...bunch of data loading logs...
19:43:52 DEBUG NHibernate.Engine.TwoPhaseLoad - adding entity to second-level cache: [Babil.Data.Setting#PreferencesCookieName]
19:43:52 DEBUG NHibernate.Cache.NonstrictReadWriteCache - Caching: Babil.Data.Setting#PreferencesCookieName
19:43:52 DEBUG NHibernate.Caches.SysCache.SysCache - adding new data: key=NHibernate-Cache:Settings:Babil.Data.Setting#PreferencesCookieName@-83065912&value=NHibernate.Cache.Entry.CacheEntry
19:43:52 DEBUG NHibernate.Engine.TwoPhaseLoad - done materializing entity [Babil.Data.Setting#PreferencesCookieName]
...
19:43:52 DEBUG NHibernate.Event.Default.DefaultLoadEventListener - loading entity: [Babil.Data.Setting#PreferencesCookieName]
19:43:52 DEBUG NHibernate.Event.Default.DefaultLoadEventListener - attempting to resolve: [Babil.Data.Setting#PreferencesCookieName]
19:43:52 DEBUG NHibernate.Event.Default.DefaultLoadEventListener - resolved object in session cache: [Babil.Data.Setting#PreferencesCookieName]
...
...next session...
19:43:57 DEBUG NHibernate.Event.Default.DefaultLoadEventListener - loading entity: [Babil.Data.Setting#PreferencesCookieName]
19:43:57 DEBUG NHibernate.Event.Default.DefaultLoadEventListener - attempting to resolve: [Babil.Data.Setting#PreferencesCookieName]
19:43:57 DEBUG NHibernate.Cache.NonstrictReadWriteCache - Cache lookup: Babil.Data.Setting#PreferencesCookieName
19:43:57 DEBUG NHibernate.Caches.SysCache.SysCache - Fetching object 'NHibernate-Cache:Settings:Babil.Data.Setting#PreferencesCookieName@-83065912' from the cache.
19:43:57 DEBUG NHibernate.Cache.NonstrictReadWriteCache - Cache hit
19:43:57 DEBUG NHibernate.Event.Default.DefaultLoadEventListener - assembling entity from second-level cache: [Babil.Data.Setting#PreferencesCookieName]
19:43:57 DEBUG NHibernate.Event.Default.DefaultLoadEventListener - Cached Version: 
19:43:57 DEBUG NHibernate.Engine.StatefulPersistenceContext - initializing non-lazy collections
19:43:57 DEBUG NHibernate.Event.Default.DefaultLoadEventListener - resolved object in second-level cache: [Babil.Data.Setting#PreferencesCookieName]
...

As you can see (the relevant parts are in bold), the first request for the object ("PreferencesCookieName" in this case) generates a cache miss and NHibernate loads the entity from the database. All request after the first, in the same Session, will generate cache hits directly in the first level cache.

The second session instead will have to query the second level cache, which generates a cache hit because the object was stored by the previous session, and the object is never loaded from the database.

Using the Query Cache

Now that the second level cache seems to be working, you'll probably notice that caching only works when you explicitly request an object by its identifiers (like for instance using the Get and Load methods of ISession). Complex queries (either through HQL or using ICriteria) are not cached by default... but you can do it if you want.  :)

First of all you must enable the Query Cache in your web.config file:

<property name="cache.provider_class">NHibernate.Caches.SysCache.SysCacheProvider, NHibernate.Caches.SysCache</property>
<property name="cache.use_query_cache">true</property>
<property name="cache.use_second_level_cache">true</property>

The line use_query_cache tells NHibernate to create a separate Cache Region where the results of all cached queries are stored, as lists of identifiers. Thus, if a query is found in the cache, NHibernate simply takes the identifies from the cache region and then hydrates all single instances using their own Cache Region (or falling back to the database if needed). Therefore, query caching only makes sense if used together with second level caching.

Not every query is cached by NHibernate though: you must set all queries you think will be executed frequently as "cacheable". This can be done with both the IQuery and the ICriteria interface:

ICriteria crit = session.CreateCriteria(typeof(MyEntity))
	.SetCacheable(true)
	.SetFetchMode("Collection", FetchMode.Eager)
	.Add(Expression.Eq("Property", true);

var results = crit.List<MyEntity>();

And that's it. The query will be cached by NHibernate: if you issue the same exact query multiple times only a single database query should be executed.

I hope this article will be helpful in some way and would also like to thank the NHibernate team for the awesome work they have done.
Just consider having to implement all of this by yourself...  :S