SQL Server CE y NHibernate: Mantener la conexión abierta

Si hace poco explicaba cómo resolver losproblemas con el truncado de datos al usar SQL Server CE con NHibernate, ahora toca ver cómo resolver otro problema que encontramos en nuestras pruebas con SQL Server CE: el rendimiento al abrir la conexión.

En NHibernate, cada vez que es necesario abrir una conexión a la base de datos (OJO: que no es lo mismo que abrir un ISession de NHibernate), se invoca al DriverConnectionProvider que tengamos definido en la configuración para obtener una conexión abierta, y cuando se termina de usar la conexión, se emplea nuevamente el DriverConnectionProvider para cerrarla.

La implementación por defecto de DriverConnectionProvider, abre y cierra la conexión cada vez que se invocan sus métodos GetConnection y CloseConnection. Este comportamiento es correcto para el 99% de los casos, ya que el propio proveedor de datos de ADO.NET implementará su propio connection pool que evitará reabrir la conexión continuamente (algo que, en general, puede ser costoso).

En el caso de SqlCeConnection, no hay ningún tipo de connection pool, por lo que cada que se abre una conexión se incurre en una penalización que puede llegar a ser de decenas de milisegundos. La forma más simple de evitar esto, es mantener la conexión abierta durante toda la vida de la aplicación. Sí, esto es una mala idea para bases de datos cliente/servidor, pero en el caso de una base de datos embebida, tiene cierto sentido. A fin de cuentas, no hay servidor al que le estemos consumiendo recursos por mantener la conexión abierta.

Un factor a tener en cuenta es que dos hebras no deberían utilizar simultáneamente la misma conexión, por lo que a la hora de diseñar la solución será un factor a tener en cuenta.

Basándonos en esto, la solución para por crear nuestro propio DriverConnectionProvider que se encargue de mantener la conexión abierta y además garantice que cada hebra utiliza su propia conexión. Algo así como:

public class ConnectionPerThreadConnectionProvider : DriverConnectionProvider
{
    private static readonly ILog log = LogManager.GetLogger(typeof (SingleConnectionProvider));

    [ThreadStatic]
    private IDbConnection thisThreadConnection;

    public override void CloseConnection(IDbConnection conn)
    {
        log.Debug("No se realizará el cierre de la conexión");
    }

    public override IDbConnection GetConnection()
    {
        if (thisThreadConnection == null || thisThreadConnection.State != ConnectionState.Open)
            thisThreadConnection = CreateOpenConnection();
			
        return thisThreadConnection;
    }

    private IDbConnection CreateOpenConnection()
    {
        // Necesario por si estamos creando una conexión porque la anterior se había cerrado 
        // como consecuencia de un error
        if (thisThreadConnection != null)
            thisThreadConnection.Dispose();
	
        thisThreadConnection = Driver.CreateConnection();

        try
        {
            thisThreadConnection.ConnectionString = ConnectionString;
            thisThreadConnection.Open();
            return thisThreadConnection;
        }
        catch
        {
            thisThreadConnection.Dispose();
            thisThreadConnection = null;
            throw;
        }
    }
}

Toda la gracia de esta clase está en thisThreadConnection que está marcado como ThreadStatic. Eso hará que .NET es encargue de garantizarnos que cada hebra utiliza su propia conexión.

OJO: Este código tiene una limitación muy importante. Si la aplicación que va a utilizar ConnectionPerThreadConnectionProvider está creando continuamente nuevas hebras que acceden a la base de datos (pensemos en, por ejemplo, un servidor que crea nuevas hebras para atender a cada cliente), estaremos expuestos a un memory leak por las conexiones que se van quedando abiertas (una por cada hebra que haya accedido en algún momento a la base de datos). En ese caso, sería necesario incluir algún sistema para cerrar automáticamente las conexiones después de un cierto tiempo.

Como siempre, para que NHibernate utilice este Driver será necesario indicárselo en la configuración:

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-configuration  xmlns="urn:nhibernate-configuration-2.2" >
    <session-factory>
        <property name="connection.driver_class">My.Namespace.SqlCeDriver, MyAssembly</property>
        <property name="dialect">My.Namespace.SqlCeDialect, My.Assembly</property>
        <property name="connection.connection_string">data source = data.sdf</property>
        <property name="connection.provider">My.Namespace.ConnectionPerThreadConnectionProvider, My.Assembly</property>
        <property name="proxyfactory.factory_class">NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle</property>
    </session-factory>
</hibernate-configuration>

Lo mejor de este tipo de cosas, más allá de poder resolver el problema concreto que tenemos en un momento dado, es comprobar la facilidad con que se puede modificar el comportamiento de NHibernate para ajustarlo a nuestras necesidades. Así da gusto utilizar un framework.

5 comentarios en “SQL Server CE y NHibernate: Mantener la conexión abierta

  1. Pingback: SQL Server CE y NHibernate: Precisión y escala en columnas NUMERIC | Koalite's blog

  2. Pingback: SQL Server CE y NHibernate: Precisión y escala en columnas NUMERIC | Koalite's blog

  3. Pingback: ¿Hacen los frameworks tontos a los desarrolladores? | Koalite's blog

  4. Me parece interesante, pero el hecho de mantener una conexión abierta puede traer consecuencias en seguridad

Comentarios cerrados.