Of course there is, its called Second level cache.
Before we move on to show how Second Level Cache can be enabled, lets focus on, why is Second Level Cache so important.
Why should you enable Second Level Cache:
Well, in almost every serious project, you will have some sort of Reference Data.
What information is considered Reference Data?
The information that acts as look-up information might be considered as Reference Data. Information like, Countries, Employee Types etc could be categorized as reference data. This information does not vary from User to User. This information is used at various places in the application and is mostly never updated.
Imagine the case when two different users are entering their address in the application. Let's say they are accessing the "Enter Your Address Page". This page allows users to enter their address, it shows various address fields for this purpose. This page also shows a Country drop down so that users can easily select their country. Now to populate this country drop down, you will have to load all countries in the system.
Since two user threads are trying to enter the address, two NHibernate sessions will try to load the countries from the DB. First level cache will not have the Countries List cached, hence, two exact same queries will be fired. These queries will result in exact same result.
With me so far?
Our aim in this post is to avoid two queries being fired when we try to load the Countries from two different NHibernate Session. Some how, we should inform NHibernate that, Countries list is cache-able and it should go to the DB only once to fetch it and then cache it!
Without wasting any more time lets jump right into the steps to enable second level cache with NHibernate.
How do they Do it!
Country class and its mapping file looks like:
Nothing special here.
Setting up the Second Level Cache:
To enable second level cache in NHibernate, we need to configure a Second Level Cache Provider class. There are numerous Second Level Cache provider's available. All have their merits and demerits. For the sake of this post we are going to use the SysCache provider as the second level cache provider. You can download cache providers from here
From the downloaded package include the NHibernate.Caches.SysCache.dll into the project.
Lets look at the Fluent NHibernate configuration which enables the SysCache provider.
The only special thing that you might have noticed in the previous configuration is the call to the "Cache" method. This call enables the second level cache. We also configure the ProviderClass as the NHibernate.Caches.SysCache.SysCacheProvider.
That's about it, we have finished configuring the second level cache. Lets see it in action, shall we?
Test code to see Second Level Cache in Action:
The test code simply calls the GetAllCountries method twice to get the list of all the countries in the system. GetAllCountries will first open a new NHibernate session and then fires off the query to fetch all the countries in the system.
We have enabled second level caching in the previous step, now if we run the test, lets see the SQL's generated.
Well not what we expected right?
Remember that our goal is that, only one query should be fired to load all countries. The other query should get all countries from the cache. What is the missing link? Why did NHibernate not cache the first query?
Well, because we didn't tell NHibernate to cache the query, that's why!
We need to inform NHibernate that certain queries are cache-able and that the results should be saved in the second level cache. Let's look at the update test code which informs NHibernate that the countries query is cache-able.
Only thing that we changed was added the calls to "Cacheable" and specified the "CacheMode".
- Cacheable information NHibernate that this particular query is cache-able
- There are various cache mode in which the Second Level Cache can be used.
- Normal mode means
- NHibernate will first look for information in second level cache, if found it returns it.
- If information is not found in the cache then, it will hit the DB to get the information.
- But before returning the results from the DB, it will also put the results in the Cache.
- Other CacheModes are Get, Ignore, Put and Refresh. I will not go into details of each. Can read about them in NHibernate documentation.
Hmm, looks like we have gone from bad to worse! This time NHibernate fired three queries instead of one!
But lets step back for a moment and look at the queries that it has fired.
- First query gets all the countries in the system, that's expected.
- Second query is different from the first query, it's trying to load the country using its ID. It loads the country with ID=1
- Third query is similar to the second query. Only difference is, it's trying to load the country with ID=2
If caching is going to increase the number of queries getting fired then we certainly do not need it right?
Wrong! The problem is that, we have just informed NHibernate that the query itself is cache-able. By default NHibernate will only cache the identifiers returned by the cache-able query. In our case the cache-able query (one that loads all countries) returned two countries, one with ID=1 and other with ID=2. Since we have informed NHibernate to cache the query, it caches the identifiers of the returned result set.
It effectively means that, NHibernate just caches the fact that, when a query to load all countries is fired, Country with ID=1 and 2 are to be fetched and returned. And since NHibernate has not cached the Country entity, it needs to fire two more queries to load the country instances with ID=1 and 2. That's why the other two queries are fired.
Well in that case this is horrible right?
Yes, if second level cache is not used correctly then, you can end up firing more queries than when it was not configured.
So let cut the chase here and see how we can configure it correctly.
Making the Entity itself cache-able:
This is the last and final step, we just need to inform NHibernate that not just the identifiers but the entire Country entity is cache-able. How do we do this? Simple, in the mapping file. Here's how the updated mapping file looks like
The only addition to the mapping file is the call to Cache.ReadOnly(). This informs NHibernate that this entity is a cache-able entity and its going to hold read only information.
Lets run the test after this update to see what queries are fired.
Yep, we have got it right this time around! Only one query is fired even though we are trying to load all countries from two different NHibernate Session! Mission Accomplished!
Now imagine if there are hundreds of users concurrently accessing your application, How many queries would you end up saving?