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:
ISession
caches every entity that it loads from the database. This is default behavior and cannot be changed or turned off.Let's dive into each type of cache in more detail to understand how to enable, disable, and make use of each 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.
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:
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))));
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 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.