Caching is used frequently; rarely updated data can greatly improve the performance of websites and other high traffic applications. In this recipe, we'll configure NHibernate's cache, just as we would for a typical public facing website.
Complete the Getting Ready instructions at the beginning of Chapter 4, Queries.
NHibernate.Caches.SysCache
using NuGet Package manager console.App.config
file in the project.configSections
element, declare a section for the cache configuration:<section name="syscache" type="NHibernate.Caches.SysCache.SysCacheSectionHandler, NHibernate.Caches.SysCache" />
syscache
section:<syscache> <cache region="hourly" expiration="60" priority="3" /> </syscache>
Caching
to the QueryRecipes
project.hibernate.cfg.xml
to the folder. Set its Copy to Output directory property to Copy always:<?xml version="1.0" encoding="utf-8"?> <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2"> <session-factory> <property name="cache.provider_class"> NHibernate.Caches.SysCache.SysCacheProvider, NHibernate.Caches.SysCache </property> <property name="cache.use_second_level_cache"> true </property> <property name="cache.use_query_cache"> true </property> <class-cache class="NH4CookbookHelpers.Queries.Model.Product,NH4CookbookHelpers" region="hourly" usage="read-write"/> <class-cache class="NH4CookbookHelpers.Queries.Model.ActorRole,NH4CookbookHelpers" region="hourly" usage="read-write"/> <collection-cache collection="NH4CookbookHelpers.Queries.Model.Movie.Actors" region="hourly" usage="read-write"/> </session-factory> </hibernate-configuration>
Recipe
to the folder:using NH4CookbookHelpers.Queries; using NH4CookbookHelpers.Queries.Model; using NHibernate; using NHibernate.Cfg; namespace QueryRecipes.Caching { public class Recipe : QueryRecipe { protected override void Configure(Configuration nhConfig) { nhConfig.Configure("Caching/hibernate.cfg.xml"); } } }
Recipe
, add the following methods:protected override void Run(ISessionFactory sessionFactory) { ShowMoviesBy(sessionFactory, "Steven Spielberg"); ShowMoviesBy(sessionFactory, "Steven Spielberg"); UpdateMoviesBy(sessionFactory, "Steven Spielberg"); ShowMoviesBy(sessionFactory, "Steven Spielberg"); } private void ShowMoviesBy(ISessionFactory sessionFactory, string director) { using (var session = sessionFactory.OpenSession()) { using (var tx = session.BeginTransaction()) { var movies = session.QueryOver<Movie>() .Where(x => x.Director == director) .Cacheable() .List(); Show("Movies found:", movies); tx.Commit(); } } } private void UpdateMoviesBy(ISessionFactory sessionFactory, string director) { using (var session = sessionFactory.OpenSession()) { using (var tx = session.BeginTransaction()) { session.CreateQuery(@"update Movie set Description='Good' where Director=:director") .SetString("director", director) .ExecuteUpdate(); tx.Commit(); } } }
Caching
recipe.In the query log, you will see how the SELECT
query, which is used to find the movies, is only executed the first and third time we call ShowMoviesBy
. The second time, the query results are found in the query cache and the returned entities are loaded from the entity cache.
What happened after the second query? Why weren't the cached results used? The UpdateMoviesBy
method was called and while it didn't actually affect the movies included in the query, NHibernate has no way of knowing that. Such deduction logic would be extremely complex. Instead, a defensive but safe approach is taken, which sees that the Product
table was affected by the update and as a result all cached query results involving that table are invalidated. Consistency is much more important than performance.
We deliberately used separate sessions for each call to ShowMoviesBy
, so that the first level cache would not be involved.
The cache.provider_class
configuration property defines the cache provider to use. In this case, we're using syscache
, NHibernate's wrapper for ASP.NET's System.Web.Caching.Cache
.
The cache.use_second_level_cache
setting enables the second-level cache. If the second-level cache is enabled, setting cache.use_query_cache
will also allow query results to be cached.
Caching must be set up on a per-class hierarchy, per-collection, and per-query basis. That is, you must also set up caching for each specific item to be cached. In this recipe, we've set up caching for the product entity class, which, because they're in the same class hierarchy, implicitly sets up caching for book and movie with the same settings. In addition, we've set up caching for our ActorRole
entity class. Finally, because caching for collections is configured separately from entities, we set up caching for the movie's Actors
collection.
Each of these caches use a region named hourly. A cache region partitions the cached data and defines a set of rules governing when that data will expire. In this case, our hourly region is set to remove an item from the cache after 60 minutes or under stress, such as low memory. The priority can be set to a value from 1 to 5, with 1 being the lowest priority and thus the first to be removed from the cache.
The cache concurrency strategy for each item, set with the usage
attribute, defines how an object's cache entry may be updated. In this recipe, where we are both storing and retrieving entities, we've set all of our strategies to read-write. In other scenarios, such as a public-facing website, which never updates the data, it may be appropriate to use read-only.
It should be added that the class level caching configuration can be specified in the mapping files:
<class name="Product"> <cache region="hourly" usage="read-write"/> ... </class>
Using that approach tidies things up a bit, but it's worth considering whether caching should be a mapping concern (often embedded in the code) or a configuration setting.
Caching is only meant to improve the performance of a properly designed NHibernate application. Your application shouldn't depend on the cache to function properly. Before adding caching, you should correct poorly performing queries and issues, such as SELECT N+1
. This will usually give a significant performance boost, reducing the need for caching and its added complexity.
NHibernate allows us to configure a cache with the same scope as the session factory. Logically, this cache is divided into three parts.
The entity cache doesn't store the actual entities. Instead the objects are stored as a dictionary of POIDs to arrays of values.
The Movie.Actors
collection has a cache entry of its own. Also notice that in this entry, we're storing the POIDs of the ActorRole
objects, not the ActorRole
data. There is no data duplication in the cache. From the cached data shown in the diagram, we can easily rehydrate the entire object graph for the movie without the chance of any inconsistent results.
In addition to caching entities, NHibernate can also cache query results. In the cache, each query is associated with an array of POIDs for the entities of the query returns, similar to the way our movie actor collection is stored in the previous image. The entity data should already be stored in the entity cache. Again, this eliminates the chance of inconsistent results. However, it's very important that the return types of the queries are configured to be cacheable. If not, each returned entity will be fetched from the database, probably causing the cached query to be slower and more resource consuming than a non-cached query.
The third part of the cache stores a last-updated timestamp for each table. When data is first placed in the cache, the timestamp is set to a value in the Future
, ensuring that the cache will never return uncommitted data from a pending transaction. Once the transaction is committed, the timestamp is set back to the present, thus allowing that data to be read from the cache.
There are some basic requirements when using the cache:
IConnectionProvider
and set the connection.provider
configuration property as shown in the Using dynamic connection strings recipe in Chapter 7, Data Access Layer.52.14.204.142