I am trying to setup nhibernate second level caching and i see in this article, and i am trying to understand the difference between query caching and entity caching. It says you need to add
Cache.ReadOnly(); or Cache.ReadWrite();
on every single entity mapping like this:
public class CountryMap : ClassMap<country>
{
public CountryMap()
{
Table("dropdowns");
Id(x => x.Id, "pkey");
Map(x => x.Name, "ddlong");
Map(x => x.Code, "dddesc");
Where("ddtype = 'COUNTRY'");
//Informing NHibernate that the Country entity itself is cache-able.
Cache.ReadOnly();
}
}
But when using nhibernate profiler, i see things hitting the second level cache and I don't have this Cache.ReadOnly() value set.
Is that really required? Should I be doing this for every single entity (no matter how often that entity changes?).
If the answer is yes, that i should be doing this for all entities, I saw a page that mentioned there is a risk of setting an entity with this line as it might lead to Select n + 1 query problem if you are trying to join that entity with other entities in a query. I am using nhibernate profiler and it looks like someething are hitting the second level cache just from the code below. In my session setup, i have the following code:
return configuration
.Mappings(m => m.FluentMappings.AddFromAssemblyOf<ApplicationMap>().Conventions.Add(typeof(Conventions)))
.ExposeConfiguration(
c => {
c.SetProperty("cache.provider_class", "NHibernate.Caches.SysCache.SysCacheProvider, NHibernate.Caches.SysCache");
c.SetProperty("cache.use_second_level_cache", "true");
c.SetProperty("cache.use_query_cache", "true");
c.SetProperty("expiration", "86400");
})
.BuildSessionFactory();
and i have a generic "Query" method that does this:
ICriteria c = Session.CreateCriteria(typeof(T));
c.SetCacheable(true);
return c.Future<T>().AsQueryable();
so basically I am trying to confirm if i setup caching correctly as I see some second level cache hits when I using nhibernate profiler but I have not set the Cache in the entity mapping code. I am trying to determine if there are other things i need to do to get caching working (or working better)
When I use nhibernate profiler (without having the Cache.ReadWrite() set at an entity level), it still seems like it does hit the second level cache. (see screenshot below)
Query cache only stores the identifiers of entities returned as result of a query. Actual entities are stored in entity cache region. Therefore entity must be configured cacheable to be used with query cache. If query cache is used without setting entities cacheable, still only identifiers of query results will be stored in query cache. As stated in a blog
Query cache does not cache the state of the actual entities in the
result set; it caches only identifier values and results of value
type. So the query cache should always be used in conjunction with the
second-level cache.
When re-executing the same query, what NHibernate does is, it gets list of identifiers in query result from query cache, and fetch each entity from entity cache and if not found in cache, queries that element from database (ending up in multiple queries; one for each entity).
Therefore it is always recommended to use 2nd level cache for entities along query cache i.e. you need to specify Cache.ReadOnly();
or Cache.ReadWrite();
in entity mapping else query caching will even further reduce your application performance by making multiple database queries against one cached query result.
I would like to provide some summary about the (2nd level) Caching options we do have in NHibernate. First of all, there are 4 kinds of settings. These in fact, do represent the real power of a very granular caching settings.
<property name="cache.use_second_level_cache">
- Global switch
<property name="cache.use_query_cache">
- Global switch
<cache usage="read-write" region="xxx"/>
- Class/Instance level
.SetCacheable(true).SetCacheMode(CacheMode.Normal).SetCacheRegion("yyy")
- per Query
The first two are global enablers/disablers. They must be turned on before we can use the next/last two settings.
But in fact, there is already the answer. The global means - support caching, local means - decide 1) how whill be 2) which class/query handled - if will or not at all.
for example, this is a snippet of the SessionFactory.cs:
...
bool useSecondLevelCache = PropertiesHelper
.GetBoolean(Environment.UseSecondLevelCache, properties, true);
bool useQueryCache = PropertiesHelper
.GetBoolean(Environment.UseQueryCache, properties);
if (useSecondLevelCache || useQueryCache)
{
// The cache provider is needed when we either have second-level cache enabled
// or query cache enabled. Note that useSecondLevelCache is enabled by default
settings.CacheProvider = CreateCacheProvider(properties);
}
else
...
Let me explicitly point out the comment:
The cache provider is needed when we either have second-level cache enabled
or query cache enabled. Note that useSecondLevelCache is enabled by default
(NOTE: also see that the first PropertiesHelper.GetBoolean()
call passes the last default value true
)
That would mean, that if the 3. setting (<cache usage="read-write" region="xxx"/>
) would not be important... all the mapped instances would be cached ... unmanaged, default way...
Luckily this is not true. The 3. setting, class level, is important. It is a must. Without explicit setting like this:
// xml
<class name="Country" ...>
<cache usage="read-write" region="ShortTerm" include="non-lazy/all"/>
// fluent
public CountryMap()
{
Cache.IncludeAll() // or .IncludeNonLazy
.Region("regionName")
.NonStrictReadWrite();
The 2nd level cache (class/instance level) won't be used. And this is great - because it is in our hands how to set it.
Fluent NHibernate - apply it as a convention
There is a Q & A discussing how to apply (in fact not apply) these settings globally - via special Fluent NHibernate feature - Convention (In my case, this Q & A was suggested aside of this question):
NHibernate second level cache caching entities with no caching configuration
Small code snippet cite:
public class ClassConvention : IClassConvention
{
public void Apply(IClassInstance instance)
{
instance.Table(instance.EntityType.Name);
instance.LazyLoad();
instance.Cache.NonStrictReadWrite();
}
}
Finally, we should mention here that the 4. option (query level .SetCacheable(true)
) should come together with the 2nd level cache:
19.4. The Query Cache
Query result sets may also be cached. This is only useful for queries that are run frequently with the same parameters. To use the query cache you must first enable it:
<add key="hibernate.cache.use_query_cache" value="true" />
... Note that the query cache does not cache the state of any entities in the result set; it caches only identifier values and results of value type. So the query cache should always be used in conjunction with the second-level cache...
Summary: Caching with NHibernate is really powerful. The reason is 1) because it is plugable (see how many providers we can use out of the box e.g. here) and 2) it is configurable. The fact, that we can use different Concurrency strategies, regions, lazy loading handling ... or even NOT to use them... is essential. Some entities are "almost-read-only" (e.g. Country code list), while some are highly changing...
The best we can do is to play with. To experiment. At the end we can have well oiled machine with a good performance.
Yes you have to do this for all entities. However this can be done via xml configuration rather than the mapping:
Configuring NHibernate second level caching in an MVC app