Try fast search NHibernate

30 April 2009

Empezando con NH: session

Posts anteriores: Empezando con NHibernate
Configuración

Cuando se empieza a trabajar con NH uno de los escollo es el manejo de sesiones; tal vez sea el más grande. La realidad es que hay muy poco que elegir y así muy poco que estudiar.

Los patrones de manejo de sesión son:

  • Open session in view (aka session-per-request)
    • Request, session y transaction tienen el mismo ciclo de vida
  • Session-per-Conversation (aka long-session)
    • Permanece abierta por más de un request
  • Conversation-per-Business Transaction (CpBT)
    • Permanece abierta por el tiempo que dura una transacción de negocio
  • Session-per-Call (anti pattern)
    • Permanece abierta por el tiempo que dure un query, o un save, o un update…
  • Session-per-application
    • Imposible usar en una aplicación real
    • Más que un pattern es una “Time bomb”

Sobre los último dos no vale la pena gastar ni una línea.

Con Session-per-Conversation (eschema disponible aquí) hay que tener cuidado porque no siempre funciona como uno se espera (léase abrir dos tabs en lugar que dos instancias de browser). Hay frameworks que facilitan el uso de session-per-request en mix con session-per-conversation y limitan la cantidad de conversations iniciadas a una. A mi gusto quedan sesión-per-request y conversation-per-business transaction (eschema disponible aquí).

Si pueden organizar la aplicación para usar session-per-request siempre, sería una buena solución. Si notan que los use-case, que necesitan una trasaction de negocio que involucra más de un request, son muchos, vayan pensando que session-per-request, en algún momento, le puede quedar corto. Si la aplicación es WinForm o WPF olvídense de session-per-request por el simple motivo que en esos ambientes no existe nada parecido a un request (ni siquiera el Call-context lo es).

Las recomendaciones

  • Recuerden que la session implementa UnitOfWork; la implementación del pattern UnitOfWork es la session de NHibernate.
  • Traten de mantener en la session lo que realmente se necesita que esté en la UnitOfWork; el Flush podría ser muy laaargo.
  • “Abracen” todas acciones de persistencia con una transaction, no importa que sean de lectura o escrictura.
  • Haganse la idea que la ISession no es la IDbConnection.
  • Haganse la idea que la ITransaction no es la IDbTransaction.
  • No encierren la ISession en una clase con Save, Update, FindBy etc., es completamente innecesario; en el DAO/Repository usen la ISession.
  • Si algo "no funciona" ante de pensar que sea un bug de NHibernate intenten escribir un test que reproduzca el error afuera de su aplicación (NH es muy usado, en el mundo, en varios entornos y en aplicaciones comerciales).
  • Antes de escribir su session-handler averigüen quien ya lo hizo; no intenten inventar, ya somos muchos los que han golpeado la cabeza, ustedes, en lo posible, evítenlo.
  • Asegúrense que su capa de acceso a datos (DAO o Repository) permita cambiar la estrategia de gestión de sesiones de persistencia; por ejemplo inyectando ISessionFactory a su DAO y usando GetCurrentSession(tambien se puede inyectar la ISession pero, en ese caso, asegúrense de conocer lo que pierden).
  • Eviten que sus DAO/Repository tengan referencia a un session-handler especifico (aunque suene similar no es lo mismo del punto anterior); en lo posible mantengan la implementación de DAO/Repository pegada solo y exclusivamente a NH.
  • Recuerden que el manejo de sesiones de persistencia es algo más cercano al manejo de cuestiones de infraestructura.
  • Al primer problema relacionado con lazy-loading asegúrense usar una estrategia de manejo de sesiones adecuada al caso de uso.
  • Si piensan necesitar un ISession.Evict preocúpense; es posible que el manejo de sesiones no sea el adecuado. Ni hablar si ven una aplicación con Evict por todos lados. El Evict es útil solo en algunos casos especiales.
  • Sepan que ISession no es solo Get, SaveOrUpdate, CreateQuery y CreateCriteria.
  • Investiguen porque parece que NH tiene dos métodos para hacer lo mismo; Get y Load no son lo mismo.
  • Aprendan a trabajar con entidades desconectadas (ISession.Lock, ISession.Merge)
  • Averigüen de qué se trata con IStatelessSession.

Donde encontrar session handlers

La primer fuente es NHibernate mismo. El name space NHibernate.Context contiene la clase WebSessionContext para manejo de sessiones en WEB y ThreadStaticSessionContext para manejo de sesiones en tests. Usando WebSessionContext no necesitan nada mas (a parte una implementación de IHttpModule) para manejar session-per-request y/o session-per-conversation en entorno WEB. Notar que las implementaciones disponibles en NHibernate se basan en el uso de GetCurrentSession y son descripats en NHibernate in Action.

Castle y Spring.NET ofrecen “semplificadores” de manejo de sessiones en WEB, WinForm y tests.

RhinoTools ofrece una implementación para WEB (session-per-request y/o session-per-conversation).

NHibernate.Burrow provee varias formas de manejo de sesiones orientado a WEB. Burrow tambien implementa algo muy similar a CpBT o mejor dicho implementa session-per-conversation sin limitaciones a una sola convesación por HttpSession.

uNhAddIns implementa todos los patterns. Como la implementación en NHibernate-Core tambien las implementaciones en uNhAddIns se basan en el uso de GetCurrentSession. Las implementaciones son disponibles en el namespace uNhAddIns.SessionEasier y uNhAddIns.Web.SessionEasier. La implementaciones de session-per-request y session-per-conversation ofrecen un uso “semplificado” respecto a las implementaciones en el core de NH ya que el Bind/Unbind es automatico (el Unbind automatico se activa solo si aceptan usar algún DynamicProxy, por ejemplo el mismo que usan en NH). Para lo que concierne la implementación de CpBT los invito a leer varios posts. Como ultima nota, sobre uNhAddIns, les aclaro que uNhAddIns no trae dependencias a algo mas que no sea NHibernate, todas las implementaciones de session-handlers pueden ser usadas a solas o con el IoC/AOP que prefieran.

NUnit 2.5 is out

Well… we are waiting it since looong time, but now it is ready.

Here is the Charlie’s post.

Here de documentation.

This new release has a lot of new features as Theory, parameterized tests (here and here), parallel testing, and many others very useful features (pay attention to each new attribute).

What you are waiting for ? Download and try NUnit2.5RC1right now.

And remember to take a look to the new test runner ;)

29 April 2009

Empezando con NH: configuración

Posts anteriores: Empezando con NHibernate

De aquí en más, cuando haga referencia a NHibernate (NH), usaré NH2.1.0.

NH es un framework muy flexible y por ese motivo la configuración de la session-factory es bastante extensa; sin embargo nadie nos obliga a usar o definir un valor para todas las features suportadas por NH. Acá les muestro la configuración que uso para mis cursos de introducción a NHibernate:

<hibernate-configuration  xmlns="urn:nhibernate-configuration-2.2" >
<
session-factory name="StartingNH">
<
property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>

<
property name="connection.connection_string_name">StartingNH</property>
<
property name="connection.isolation">ReadCommitted</property>

<
property name='proxyfactory.factory_class'>
NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu
</property>

<
property name="query.substitutions">true 1, false 0, yes 'Y', no 'N'</property>

<
property name="adonet.batch_size">50</property>

<
property name="command_timeout">10</property>

<
property name="format_sql">true</property>

</
session-factory>
</
hibernate-configuration>

Esta configuración puede ser usada tranquilamente en producción. Para su entorno de desarrollo, tal vez, sea mejor usar la propiedad connection.connection_string en lugar que user connection.connection_string_name.

En realidad una configuración perfectamente valida es:

<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2" >

<
session-factory>
<
property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
<
property name="connection.connection_string">
Data Source=localhost\SQLEXPRESS;Initial Catalog=BlogSpot;Integrated Security=True
</property>

<
property name='proxyfactory.factory_class'>
NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu
</property>
</
session-factory>
</
hibernate-configuration>

Como verán son tres las cosas que NH necesita para poder funcionar:

  1. El Dialect del RDBMS que vamos a usar (y hasta eso es opciónal pero bueno…)
  2. La connection string
  3. El sistema de DynamicProxy que usará para trabajar en forma transparente con lazy-loading.

No me digan que esa es una configuración compleja.

Hay otra parte de la configuración que, no es de NH pero, es importante en fase de desarrollo/tests: la configuración de log4net.

Acá va una muestra:

<log4net debug="false">
<
appender name="console" type="log4net.Appender.ConsoleAppender, log4net">
<
layout type="log4net.Layout.PatternLayout,log4net">
<
param name="ConversionPattern" value="%d{ABSOLUTE} %-5p %c{1}:%L - %m%n" />
</
layout>
</
appender>

<
appender name="CleanConsole" type="log4net.Appender.ConsoleAppender, log4net">
<
layout type="log4net.Layout.PatternLayout,log4net">
<
param name="ConversionPattern" value="%m%n" />
</
layout>
</
appender>

<
appender name="RollingFile" type="log4net.Appender.RollingFileAppender,log4net" >
<
param name="File" value="NHibernateLog.txt" />
<
param name="AppendToFile" value="false" />
<
param name="RollingStyle" value="Date" />
<
param name="DatePattern" value="yyyy.MM.dd" />
<
param name="StaticLogFileName" value="true" />

<
layout type="log4net.Layout.PatternLayout,log4net">
<
param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n" />
</
layout>
</
appender>

<
root>
<
priority value="WARN" />
<
appender-ref ref="RollingFile" />
</
root>

<
logger name="NHibernate.SQL">
<
level value="OFF" />
</
logger>

<
logger name="NHibernate.Tool.hbm2ddl.SchemaExport">
<
level value="ERROR" />
</
logger>
</
log4net>

Para quien no conoce log4Net aclaro que lo que está siempre activo son dos niveles de log: el WARN (warning) y el ERROR para el SchemaExport que es para quien usamos NH tambien para generar el DB schema en fase de tests.

Las recomandaciones para NH

  • Eviten usar frameworks que "ocultan" la configuración de NH; enrollar un XML con otro XML no tiene sentido (hasta que alguien demuestre lo contrario).

  • Si quieren usar algún framework que permita la configuración por codigo asegurense que lo haga heredando de NHibernate.Cfg.Configuration y/o usando una implementación de IHibernateConfiguration y no generando otro XML.

  • Nombren la/las session-factory especialmente si trabajan en multi-rdbms. El nombre de la session-factory puede ser la key para encontrar la session-factory asociada a un determinado DAO/Repository.

  • Definan siempre el isolation level aunque cada Drive tenga ya su default; los valores admitidos son definidos en System.Data.IsolationLevel.

  • Usen el tag <mapping> para agregar sus mappings a una configuración.

  • No olviden definir query.substitutions; evitarán sorpresas cuando ejecuten HQLs en donde especifiquen valores de clases (true,false) y no valore de DB (1 o 0).

  • Si saben que su drive suporta la ejecución de queries en batch (más de un queries en un solo round-trip) definan adonet.batch_size; si no lo definen el batcher no se activa y la diferencia en performance puede ser notable.

  • Definan el command_timeout (en segundos) a un valor congruo para su aplicación; en mi forma de endender si un comando SQL lleva mas de 10 segundos algo anda mal.

  • Controlar alguna SQL generada por NH puede ser un desafió no de poco (especialmente cuando le toman la mano al ORM). Configurar format_sql a true le permitirá ver las SQL en un formato mas human-readable (NO todo en una sola linea).

  • En desarrollo usen el file hibernate.cfg.xml y una base de datos local y no usen el App.config y base de datos común para los tests; cada desarrollador tiene el derecho de tener la base en local con su connection-string y hacer debug sin preocuparse de bloquear a los otros desarrolladores. El file hibernate.cfg.xml tendrá que ser excluido del repository de fuentes (svn:ignore) mientras que se puede incluir un file hibernate.cfg.template para que cada desarrollador tenga un ejemplo.

  • No activen la cache (aka second level cache) hasta que la applicación no tenga por lo menos una versión en producción. La cache no es la panacea de las performance; una applicación debe funcionar bien sin cache… luego le pondrán el “turbo” si y donde lo necesita.

Las recomandaciones para el logging

  • No olviden configurar el logging de NHibernate.

  • Ya que lo tendrán activo no olviden leerlo; en NH nos costó bastante definir algunos niveles de log y si se loguea un WARNING, en casi todos los casos, es porque hay que prestar attención a algo que está pasando. Ni hablar si el log es un ERROR.

  • Cuando escriban behavior-tests, con o sin WatIn, activen el log “NHibernate.SQL” a DEBUG y miren las SQL que NH está generando. Este proceso es indispensable en fase de optimización de la applicación.

  • Por lo mismos motivos del file de configuración de NH usen un file tambien para la configuración de log4net (no usen el App.config) y escluyan ese file del repository de fuentes; cada desarrollador tiene el derecho de activar los logs que necesite.

28 April 2009

Empezando con NHibernate

Desde que Microsoft empezó a divulgar Linq2SQL y Entitity-Framework la cantidad de empresas y programadores que usan NHibernate aumentó notablemente. Quiero aprovechar la ocasión para intentar definir algunas líneas guías que ayuden a iniciarse con NHibernate (NH para los amigos).

En este post empiezo… despacito iremos avanzando juntos.

¿Qué es NHibernate?

NHibernate es un framework de persistencia basado en ORM (Object Relational Mapping).

Que NH se base en ORM significa que conocer la técnica de ORM es fundamental, no es una opción.

En la red se pueden encontrar varios recursos sobre ORM, a mi gusto, los mejores son los escritos por Scott Ambler. Si piensan no tener tiempo de leer "Agile Database Techniques" no hay problemas pero no se pierdan una de sus White-paper que subí en los files de varios fórums.

Que NH sea un framework de persistencia significa que tienen que pensar en NH como algo que ejecuta queries SLQ (ejecuta INSERT, UPDATE, DELETE, SELECT). NH simplifica el acceso a datos y NO debería tener mayores responsabilidades que eso. Si entendieron esta simple definición entenderán que preguntas como "¿puedo usar NH con WPF?" o "¿NH funciona con WCF?" o "¿Hay algo para usar NH en WinForm?" son preguntas con muy poco sentido porque sería como preguntarse si pueden usar una clases de ejecute queries en cada uno de esos ambientes.

Tal vez una definición más cercana a la realidad es: NHibernate es un DAL (Data Access Layer).

Las técnicas de ORM nacieron exactamente para abstraer el acceso a DataBase relacionales en una capa que resuelva el acceso a datos. Lo que hay que tener en cuenta, en esta afirmación, es la palabra capa (Layer). En este caso "capa" identifica un estrato lógico de nuestra aplicación. Nuestra aplicación será completamente orientada a objetos (entidades de negocio) y nuestro DAL se ocupará de transformar esos objetos en instrucciones SQL para gestionar el estado persistente (los datos de cada entidad de negocio que nuestra aplicación deberá recordar).

De aquí naces algunas recomendaciones:

  • No intenten usar NH en una aplicación que no esté bien dividida en capas lógicas.
  • No hagan comparaciones entre usar ADO.NET, con data-binding, con usar NH; sería como paragonar un auto terminado con un motor.
  • No piensen en tabla-fila-columna; piensen, primeramente, en entidades de negocio, propiedades y relaciones.
  • Nunca olviden que todo termina en una base de datos relacional; empezar con objetos y pensar OO no significa que tengamos que olvidarnos que el data base existe.
  • Empiecen definiendo las clases y, con la ayuda de un DBA, definan su representación persistente (siguiendo las reglas de ORM) y luego definan el mapping. En mi caso personal yo suelo definir la representación persistente directamente escribiendo el mapping.
  • Usen como mínimo SchemaValidator y comparen siempre el schema que generaría NH con el schema que definieron ustedes.
  • Escriban behavior-tests de persistencia y controlen las SQL generadas por NH.

27 April 2009

A Spring in NHibernate’s Conversation per Business Transaction

In my last post on CpBT (Conversation per Business Transaction) I have used Castle AOP. In uNhAddIns you can find examples, of CpBT usage, in WinForm, Castle.MonoRail and an introduction example for WCF (thanks to Gustavo Ringel and Alexander Surin).

The news is that now you can use the same code with Spring.AOP.

uNhAddIns.SpringAdapters

I’m not an expert with Spring and frankly was not so easy to find the better way to implements “all” classes needed. The mayor difficult was only the fact that Spring has a lot of very interesting features and the API is really fine-grained. You can obtain similar results in different ways. Erich Eichinger point me in various places of Spring's documentation before find all needed to completely reuse uNhAddIns.Adapters.Common.

Inheriting from AbstractConversationInterceptor the interceptor it self, the implementation for Spring and Castle, is not a big deal. My problem was only find the better way to wire the proxy factory to my interceptor; when I find it, between AutoProxyCreators, Advisor, Advise, custom ProxyFactory and so on, the result was very very simple:

public class ConversationalAttributeAutoProxyCreator : AbstractFilteringAutoProxyCreator
{
private readonly ReflectionConversationalMetaInfoStore store;

public ConversationalAttributeAutoProxyCreator(IConversationalMetaInfoStore store)
{
this.store = (ReflectionConversationalMetaInfoStore)store;
}

protected override bool IsEligibleForProxying(Type objType, string name)
{
if (store.GetMetadataFor(objType) == null)
{
return store.Add(objType);
}
return true;
}
}

As you can see… was trivial (Erich Eichinger said me: “no worries, once you get it, it is quite easy”).

The Spring configuration

Before look for “Spring programmatic configuration” I ask a link to Erich and he said :”you are just hitting our Achilles heel; programmatic conf. is not so nice ”.

In uNhAddIns.Adapters.CommonTests there are some common tests for CpBT AOP; there I’m using CommonServiceLocator and some abstract methods to configure the test for a specific IoC/AOP framework.

Well… this is the implementation using Spring’s programmatic configuration:

protected overrideIServiceLocator NewServiceLocator()
{
    IConfigurableApplicationContext context = newStaticApplicationContext();
    var objectFactory = context.ObjectFactory;
    objectFactory.RegisterInstance<ISpringConfigurationAccessor>(newTestSpringConfigurationAccessor(objectFactory));

    objectFactory.RegisterDefaultConversationAop();

    // Services for this test
   
var sl = newSpringServiceLocatorAdapter(objectFactory);

    objectFactory.RegisterInstance<IServiceLocator>(sl);

    objectFactory.Register<IConversationContainer, ThreadLocalConversationContainerStub>();

  objectFactory.Register<IConversationsContainerAccessor,ConversationsContainerAccessorStub>();

    objectFactory.Register<IDaoFactory, DaoFactoryStub>();

    objectFactory.Register<ISillyDao, SillyDaoStub>();

    return sl;
}

protected override voidRegisterAsTransient<TService, TImplementor>(IServiceLocator serviceLocator)
{
    var ca = serviceLocator.GetInstance<ISpringConfigurationAccessor>();
    ca.ObjectFactory.RegisterPrototype<TService, TImplementor>();
}

protected override voidRegisterInstanceForService<T>(IServiceLocator serviceLocator, T instance)
{
    var ca = serviceLocator.GetInstance<ISpringConfigurationAccessor>();
    ca.ObjectFactory.RegisterInstance(instance);
}

Really, I don’t understand why Erich said that the “programmatic conf. is not so nice”; I can’t see anything so nasty, do you ? ;)

kick it on DotNetKicks.com

14 April 2009

NUnitEx: assertion.Should().Satisfy(lambda);

In the first post about NUnitEx, Liviu has left a comment with this assertion:

CustomAssert(()=> DateTimeToday < DateTime.Now);

It sound very interesting from someone of us and Simone Busoli began an experiment named “Language Integrated NUnit” (LinUnit). The main concepts behind LinUnit are :

  • pure .NET based assertion
  • assertions easy to extend

LinUnit is now part of NUnitEx.

Examples

const string somethig = "something";

somethig.Should()
.Satisfy(x => x.Contains("some"));

somethig.Should()
.Satisfy(x => !x.Contains("also"));

somethig.ToUpperInvariant().Should()
.Satisfy(x => x.ToUpperInvariant().Contains("SOME"));

somethig.Should()
.Satisfy(x => x.StartsWith("so") && x.EndsWith("ing") && x.Contains("meth"));

somethig.Should()
.Satisfy(x => !x.StartsWith("ing") && !x.EndsWith("so") && !x.Contains("body"));

As you can see the parameter of Satisfy method is a pure C# lambda.

Note that the matter here is the assertion message; for example given this

const string somethig = "something";
somethig.Should()
.Satisfy(x => x.Contains("also"));

the failing message say:

Expected: "x.Contains("also")"
But was:  "something"

One more example using Linq extensions

var ints = new[] { 1, 5, 5, 3 };

ints.Should()
.Satisfy(x => x.SequenceEqual(new[] { 1, 5, 5, 3 }));

The implementation of  “Language Integrated NUnit”, available in NUnitEx, need some more improvement in order to use conventional NUnit constraints, where available, and more complex expressions (basically an improvement of the expression visitor).

Which syntax I should use ?

Well… this is exactly the news. You can use the syntax you have more feeling in the place you think is the best place.

For example for action-exception I prefer this syntax

Assert.Throws<ArgumentNullException>(() => new AClass(null))
.ParamName.Should().Be.Equal("obj");

I’m using NUnitEx in some projects and what I have noticed is that I’m not using Assert.That(…) anymore. For sure, at the begin, you may have some problem to identify the assertion because you can’t easy find “Assert”, with its different color, in the source code but believe me is only a matter of time. The advantage of “Should” with the intellisense helping you will be clear from the first usage.

Entschuldigen, diese sache kann ich nicht lesen

What mean “readability” ? What is readable ?

Probably the most readable is your natural language.

In Italian
{1, 2, 3} dovrebbe avere la stessa sequenza di {1, 2, 3} ed essere un sub-insieme di {5,1,2,3}

In Spanish
{1, 2, 3} tendría que tener la misma secuencia de {1, 2, 3} y ser un subconjunto de {5,1,2,3}

In English
{1, 2, 3) should have same sequence as {1, 2, 3} and be a subset of {5,1,2,3}

In C#
(new[] {1,2,3}).Should().Have.SameSequenceAs(new[] {1,2,3}).And.Be.SubsetOf(new[] {5,1,2,3})

In the natural language we are using spaces to separate words, but in C# we can’t so, in general, we are using the ‘_’ (underscore) or ‘.’ (dot).

In my opinion something like ShouldNotBeNullOrEmpty is less readable than

Should().Not.Be.Null().And.Not.Be.Empty()

A dot-separated-syntax help to separate each word and give you the chance to be helped by intellisense (and obviously is better extensible). A one-PascalCase-syntax mean that you should remember many words of the assertion name before the intellisense can help you.

When my wife (she are using the PC for others matters) read 5.Should().Be.LessThan(10) said : “seriously your work is ask, to the PC, such kind of things” (well… exactly she said “¿me estas cargando?”); what was interesting is that, even a no C# expert, can read it.

When the implementation of LinUnit-syntax will be more complete, probably, many .NET developer will feel better with it because it is pure .NET and you don’t need to remember which is the assertion.


kick it on DotNetKicks.com

03 April 2009

Tuning NHibernate: Tolerant QueryCache

Before reading this post you should know something about QueryCache and its imply tuning NH.

Resuming:

  • Using IQuery.SetCacheable(true) you can put/get the entirely result of a query from the cache.
  • The cache is automatically invalidated when the query-space change (mean that the cache will be throw when an Insert/Update/Delete is executed for one of the Tables involved in the query).
  • Using IQuery.SetCacheMode(CacheMode.Refresh) you can force the cache refresh (for example if you need to refresh the cache after a Delete/Insert).

The Case

eBayShot

(the picture is a e-Bay snapshot)

Take a look to the left side. Near each option you can see a number and I’m pretty sure that it not reflect exactly the state in the DB. That number is only “an orientation” for the user probably calculated few minutes before.

Now, think about the SQLs, behind the scene, and how many and how much heavy they are. A possible example for “Album Type”, using HQL, could look like:

select musicCD.AlbumType.Name, count(*) from MusicCD musicCD where musicCD.Genre = ‘Classical’ group by musicCD.AlbumType.Name

How much time need each “Refine search” ?

Ah… but there is no problem, I’m using NHibernate and its QueryCache… hmmmm…

Now, suppose that each time you click an article you are incrementing the number of visits of that article. What happen to your QueryCache ? yes, each click the QueryCache will be invalidated and thrown (the same if some users in the world insert/update/delete something in the tables involved).

The Tolerant QueryCache abstract

The Tolerant QueryCache should be an implementation of IQueryCache which understands, through its configuration properties, that updates, to certain tables, should not invalidate the cache of queries based on those tables.

Taken the above example mean that an update to MusicCD does not invalidate all “Refine search” queries, if we are caching those statistics heavy queries.

The integration point

Well… at this point you should know how much NHibernate is extensible and “injectable”.

For each cache-region NHibernate create an instance of IQueryCache through an implementation of IQueryCacheFactory and, as you could imagine, the IQueryCacheFactory concrete implementation can be injected through session-factory configuration.

<property name="cache.query_cache_factory">YourQueryCacheFactory</property>

At this point we know all we should do to have our TolerantQueryCache :


  1. Some configuration classes to configure tolerated tables for certain regions.
  2. An implementation of IQueryCacheFactory to use the TolerantQueryCache for certain regions.
  3. The implementation of TolerantQueryCache.

The Test

Here is only the integration test; all implementations are available in uNhAddIns.

Domain
public class MusicCD
{
public virtual string Name { get; set; }
}

public class Antique
{
public virtual string Name { get; set; }
}
<class name="MusicCD" table="MusicCDs">
<
id type="int">
<
generator class="hilo"/>
</
id>
<
property name="Name"/>
</
class>

<
class name="Antique" table="Antiques">
<
id type="int">
<
generator class="hilo"/>
</
id>
<
property name="Name"/>
</
class>
Configuration
public override void Configure(NHibernate.Cfg.Configuration configuration)
{
base.Configure(configuration);
configuration.SetProperty(Environment.GenerateStatistics, "true");
configuration.SetProperty(Environment.CacheProvider,
typeof(HashtableCacheProvider).AssemblyQualifiedName);

configuration.QueryCache()
.ResolveRegion("SearchStatistics")
.Using<TolerantQueryCache>()
.TolerantWith("MusicCDs");
}

The configuration is only for the “SearchStatistics” region so others regions will work with the default NHibernate implementation. NOTE: the HashtableCacheProvider is valid only for tests.

The test
// Fill DB
SessionFactory.EncloseInTransaction(session =>
{
for (int i = 0; i < 10; i++)
{
session.Save(new MusicCD { Name = "Music" + (i / 2) });
session.Save(new Antique { Name = "Antique" + (i / 2) });
}
});

// Queries
var musicQuery =
new DetachedQuery("select m.Name, count(*) from MusicCD m group by m.Name")
.SetCacheable(true)
.SetCacheRegion("SearchStatistics");

var antiquesQuery =
new DetachedQuery("select a.Name, count(*) from Antique a group by a.Name")
.SetCacheable(true)
.SetCacheRegion("SearchStatistics");

// Clear SessionFactory Statistics
SessionFactory.Statistics.Clear();

// Put in second-level-cache
SessionFactory.EncloseInTransaction(session =>
{
musicQuery.GetExecutableQuery(session).List();
antiquesQuery.GetExecutableQuery(session).List();
});

// Asserts after execution
SessionFactory.Statistics.QueryCacheHitCount
.Should("not hit the query cache").Be.Equal(0);

SessionFactory.Statistics.QueryExecutionCount
.Should("execute both queries").Be.Equal(2);

// Update both tables
SessionFactory.EncloseInTransaction(session =>
{
session.Save(new MusicCD { Name = "New Music" });
session.Save(new Antique { Name = "New Antique" });
});

// Clear SessionFactory Statistics again
SessionFactory.Statistics.Clear();

// Execute both queries again
SessionFactory.EncloseInTransaction(session =>
{
musicQuery.GetExecutableQuery(session).List();
antiquesQuery.GetExecutableQuery(session).List();
});

// Asserts after execution
SessionFactory.Statistics.QueryCacheHitCount
.Should("Hit the query cache").Be.Equal(1);

SessionFactory.Statistics.QueryExecutionCount
.Should("execute only the query for Antiques").Be.Equal(1);

Fine! I have changed both tables but in the second execution the result for MusicCD come from the Cache.



Code available here.



kick it on DotNetKicks.com