Use the second-level cache

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.

Getting ready

Complete the Getting Ready instructions at the beginning of Chapter 4, Queries.

How to do it...

  1. Add a reference to NHibernate.Caches.SysCache using NuGet Package manager console.
  2. Open or create a new App.config file in the project.
  3. In the configSections element, declare a section for the cache configuration:
    <section name="syscache"
    type="NHibernate.Caches.SysCache.SysCacheSectionHandler, 
    NHibernate.Caches.SysCache" />
  4. Add the syscache section:
    <syscache>
    <cache region="hourly" expiration="60" priority="3" />
    </syscache>
  5. Add a new folder named Caching to the QueryRecipes project.
  6. Add a new XML file named 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>
  7. Add a new class named 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");
        }
      }
    }
  8. In 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();
        }
      }
    }
  9. Run the application and start the 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.

How it works...

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.

Note

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.

There's more...

NHibernate allows us to configure a cache with the same scope as the session factory. Logically, this cache is divided into three parts.

Entity cache

The entity cache doesn't store the actual entities. Instead the objects are stored as a dictionary of POIDs to arrays of values.

Note

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.

Query cache

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.

Update timestamp cache

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.

The rules

There are some basic requirements when using the cache:

  • Always begin a transaction before any database interaction, even when reading data from the database. This is a recommended practice with NHibernate in general, but it is especially important for interacting with the cache. Without an explicit transaction, caching is bypassed.
  • When opening a session, don't provide your own database connection. This also affects caching. If you need to use different connections, implement your own IConnectionProvider and set the connection.provider configuration property as shown in the Using dynamic connection strings recipe in Chapter 7, Data Access Layer.

See also

  • Configuring NHibernate with App.config
  • Using dynamic connection strings
  • Configuring the cache with code
..................Content has been hidden....................

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