domingo, 11 de noviembre de 2012

Un rápido acceso a datos en .net

El otro día tuve que hacer una pequeña aplicación para mantener unos datos. Tenía que hacer algo rápido y sencillo. Para la parte de acceso a datos iba a usar Ado.Net a directamente, pero al final decidí hacer una clase que gestionara la conexión con base de datos y si fuera necesario el uso de transacciones.
Aplicando el principio KISS empecé a hacer algo muy sencillo. Creé una clase llamada DataContext,  compuesta por una conexión Ado.Net (IDbConnection) y un objeto transacción (IDbTransaction). Para poder hacer consultas siempre va a ser necesario una conexión, así que la inicio en el constructor. Lo sobrecargo por si lo paso por parámetro.
Sería recomendable utilizar un log para escribir las consultas, como era una cosa pequeña y rápida me decidí por usar nLog.
Como la clase va a gestionar recursos externos, como la conexión, se debe de implementar IDisposable.
public class DataContext : IDisposable 
    {
        Logger logger = LogManager.GetCurrentClassLogger();

        bool _disposed = false;

        IDbConnection _conn;
        IDbTransaction _trans;

        public DataContext()
        {
            _conn = GetConnection();
            if (logger.IsTraceEnabled)
                logger.Trace("IDbConnection created with ConnectionString:{0}", _conn.ConnectionString);
        }
        public DataContext(string ConnectionStringName)
        {
            _conn = GetConnection(ConnectionStringName);
            if (logger.IsTraceEnabled)
                logger.Trace("IDbConnection created with ConnectionString:{0}", _conn.ConnectionString);
        }
        public DataContext(IDbConnection conn)
        {
            _conn = conn;
            if (logger.IsTraceEnabled)
                logger.Trace("IDbConnection created with ConnectionString:{0}", _conn.ConnectionString);
        }
}

Gestionando transacciones.


He visto muchos programas que utilizan la infraestructura que ofrece System.Transactions para gestionar las transacciones. El código queda muy limpio, pero es muy costoso, siempre tengo la sensación de estar matando moscas a cañonazos cuando lo veo usara para transacciones que no son distribuidas.

Por otro lado, el concepto de transacción ambiental, me gusta, el código es muy limpio y solo necesitas preocuparte de hacer un complete para marcar que la transacción ha finalizado correctamente. Imitando este modelo, mi DataContext tendrá un método para abrir una transacción y un método complete que hará commit y destruirá la transacción. Si cuando se destruya el objeto la transacción sigue existiendo, se hará rollback en la transacción.
        public void BeginTrans()
        {
            if (_trans != null)
                throw new Exception ("a transactions is allredy opened");
            else
                _conn.Open ();
                _trans = _conn.BeginTransaction();
        }

        public void CommitTrans ()
        {
            _trans.Commit();
            _trans.Dispose();
            _trans = null;
            _conn.Close();
        }

Accediendo a la base de datos


Me gustaría hacer una serie de post hablando de los MicroORMs, he mirado unos cuantos por encima, pero no he podido jugar con ellos. El tener que hacer esta clase me dio la oportunidad de probar uno de forma rápida y me decidí por el Dapper de Samm Saffron. Si este microOrm vale para Stack Overflow, valdrá para mi pequeña aplicación. (intentaré hablar más de Dapper en futuros posts)

Implementé un método Query<T> que devuelve un IEnumerable del mismo tipo. También hice un ExecuteNonQuery para ejecutar inserts, updates y deletes.

Estos métodos comprueban si hay transacción abierta y si la hay la utilizan y si no abren y cierran una conexión. ¿eso no será muy costoso? la mayoría de drivers de Ado.Net es mantiene un pool de conexiones, es decir no manejamos directamente la conexión, así que no debería haber problemas de rendimiento. Además, esta es la estrategia que sigue Entity Framewok.
        public IEnumerable<T> Query<T>(string query, dynamic param=null)
        {
            IEnumerable<T> queryResult;
            if (logger.IsDebugEnabled)
                logger.Debug("Executing query<{0}>\n {1}\n with params: {2} \n is Transaction Open {3}", typeof(T).Name, query, param, (_trans!=null));

            
            if (_trans !=null)
            {
                queryResult = Dapper.SqlMapper.Query<T>(_conn,query, param, _trans);
            }
            else
            {
                
                _conn.Open();
                queryResult = Dapper.SqlMapper.Query<T>(_conn, query, param);
                _conn.Close();
            }

            if (logger.IsDebugEnabled )
                logger.Debug ("Executedquery<{0}>\n {1}\n with params: {2} \nReturn {3} records", typeof (T).Name, query, param, queryResult.Count()); 
 
            return queryResult;
            
        }

        public int ExecuteNonQuery(string query, dynamic param = null)
        {
            int queryResult;
            if (logger.IsDebugEnabled)
                logger.Debug("Executing query\n {0}\n with params: {1} \n is Transaction Open {2}", query, param, (_trans != null));

            if (_trans != null)
            {
                queryResult = SqlMapper.Execute(_conn, query, param, _trans);
            }
            else
            {
                _conn.Open();
                queryResult = SqlMapper.Execute(_conn, query, param);
                _conn.Close();
            }
            if (logger.IsDebugEnabled)
                logger.Debug("Executed query\n {0}\n with params: {1} \n returned value {2}", query, param, queryResult);

            return queryResult;
        }

Implementando IDisposable


Como hemos explicado anteriormente, tenemos que implementar IDisposable ya que no podemos permitirnos que se destruya una instancia dataContext y no se destruya su conexión asociada y si tiene su transacción, la implementación que he utilizado es muy sencilla
        public void Dispose()
        {
            Dispose (true);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    if (_trans != null)
                    {
                        _trans.Rollback();
                        logger.Warn("transaction Rollback when Disposing");
                        _trans.Dispose();
                    }
                    if (_conn != null)
                        _conn.Dispose();
                    logger.Trace("DataContext is disposed");
                }

                _trans = null;
                _conn = null;
                _disposed = true;
            }
        }

Ejemplos de uso


El uso de esta clase es muy sencillo por ejemplo recuperar uno o todos los empleados e la tabla empleados de la famosa Northwind. No me suele gustar el uso del tipo dynamic, pero he de reconocer que me encanta como lo usa dapper para recibir los parámetros de las consultas.
            using (var context = new DataContext ())
            {
                List<Employee> employees = context.Query<Employee>("select * from Employees").ToList();
                Employee employee = context.Query<Employee>("select * from Employees Where EmployeeID = @EmployeeID", new { EmployeeID = 1 }).First();
            }

La inserción es un poco especial ya que la hago con un método select, ya que la tabla tiene un campo Identity (un autonumérico) y necesito recuperar el valor. El udate y el delete funcionan de forma más tradicional.  Además en este ejemplo se puede ver un uso de la transaccionalidad, si sale del using sin pasar por el Complete, hará un rollback
                using (var context = new DataContext())
                {
                    Employee employee = new Employee
                    {
                        LastName = "Martínez",
                        FirstName = "Manel",
                        BirthDate = new DateTime(1976, 12, 28),
                        Notes = "Employee dapper Test"
                    };

                    context.BeginTrans();

                    employee.EmployeeID = context.Query<int>(@"
     INSERT INTO Employees (LastName, FirstName, BirthDate, Notes)
     VALUES (@LastName, @FirstName, @BirthDate, @Notes);
     SELECT CAST(SCOPE_IDENTITY() as int)", employee).First();

                    employee.FirstName = "Manel Modi";

                    context.ExecuteNonQuery("UPDATE Employees SET FirstName = @FirstName WHERE EmployeeID = @EmployeeID", employee);

                    context.ExecuteNonQuery("Delete from Employees Where EmployeeID = @EmployeeID", employee);
                    context.CommitTrans();
                }

Inyectando que es gerundio, uso en aplicaciones reales


El uso de esta clase en una aplicación real sería equivalente al uso de un dbContext de EntityFramework o a una Session de NHibernate. Por ejemplo, en una aplicación web se crearían al iniciarse la petición web, y se inyectarían en los objetos que necesitaran acceder a base de datos o manejar transacciones.

Conclusiones


No es una solución perfecta, pero para la pequeña aplicación que tenía que hacer me sirvió, además, me permitió probar un poco Dapper. No lo programé pensando en que durase, prueba de ello es que uso Commons.Loggin sino que uso NLog de forma directa.

Pero de todas formas me gustó mucho el uso de Dapper y como se manejan las transacciones, lo rápido que se programó esta clase y la utilidad que le he dado y me apetecía compartirla.
using System;
using System.Collections.Generic;
using System.Linq;
using Dapper;
using System.Data.Common;
using System.Data;
using NLog;

namespace Utils
{
    public class DataContext : IDisposable 
    {
        Logger logger = LogManager.GetCurrentClassLogger();

        bool _disposed = false;

        IDbConnection _conn;
        IDbTransaction _trans;

        public DataContext()
        {
            _conn = GetConnection();
            if (logger.IsTraceEnabled)
                logger.Trace("IDbConnection created with ConnectionString:{0}", _conn.ConnectionString);
        }
        public DataContext(string ConnectionStringName)
        {
            _conn = GetConnection(ConnectionStringName);
            if (logger.IsTraceEnabled)
                logger.Trace("IDbConnection created with ConnectionString:{0}", _conn.ConnectionString);
        }
        public DataContext(IDbConnection conn)
        {
            _conn = conn;
            if (logger.IsTraceEnabled)
                logger.Trace("IDbConnection created with ConnectionString:{0}", _conn.ConnectionString);
        }

        private IDbConnection GetConnection(string ConnectionName = null)
        {
            IDbConnection output = null;

            try
            {
                System.Configuration.ConnectionStringSettings connectionStr;
                if (String.IsNullOrEmpty(ConnectionName))
                    connectionStr = System.Configuration.ConfigurationManager.ConnectionStrings["local"];
                else
                    connectionStr = System.Configuration.ConfigurationManager.ConnectionStrings[ConnectionName];


                if (connectionStr == null)
                {

                    string ErrorMessage;
                    if (string.IsNullOrEmpty(ConnectionName))
                    {
                        ErrorMessage = "There's not connection string named " + ConnectionName + " in config file";
                    }
                    else
                    {
                        ErrorMessage = "There's not connection string in config file";
                    }

                    if (logger.IsErrorEnabled )
                    {
                        logger.Error(ErrorMessage);
                    }
                    
                    throw new Exception(ErrorMessage);
                }

                DbProviderFactory factory = DbProviderFactories.GetFactory(connectionStr.ProviderName);
                output = factory.CreateConnection();
                output.ConnectionString = connectionStr.ConnectionString;

                return output;
            }
            catch (Exception ex)
            {
                logger.ErrorException("Error creating connection ", ex);
                return null;
            }
        }

        public IEnumerable<T> Query<T>(string query, dynamic param=null)
        {
            IEnumerable<T> queryResult;
            if (logger.IsDebugEnabled)
                logger.Debug("Executing query<{0}>\n {1}\n with params: {2} \n is Transaction Open {3}", typeof(T).Name, query, param, (_trans!=null));

            
            if (_trans !=null)
            {
                queryResult = Dapper.SqlMapper.Query<T>(_conn,query, param, _trans);
            }
            else
            {
                
                _conn.Open();
                queryResult = Dapper.SqlMapper.Query<T>(_conn, query, param);
                _conn.Close();
            }

            if (logger.IsDebugEnabled )
                logger.Debug ("Executedquery<{0}>\n {1}\n with params: {2} \nReturn {3} records", typeof (T).Name, query, param, queryResult.Count()); 
 
            return queryResult;
            
        }

        public int ExecuteNonQuery(string query, dynamic param = null)
        {
            int queryResult;
            if (logger.IsDebugEnabled)
                logger.Debug("Executing query\n {0}\n with params: {1} \n is Transaction Open {2}", query, param, (_trans != null));

            if (_trans != null)
            {
                queryResult = SqlMapper.Execute(_conn, query, param, _trans);
            }
            else
            {
                _conn.Open();
                queryResult = SqlMapper.Execute(_conn, query, param);
                _conn.Close();
            }
            if (logger.IsDebugEnabled)
                logger.Debug("Executed query\n {0}\n with params: {1} \n returned value {2}", query, param, queryResult);

            return queryResult;
        }

        public void BeginTrans()
        {
            if (_trans != null)
                throw new Exception ("a transactions is allredy opened");
            else
                _conn.Open ();
                _trans = _conn.BeginTransaction();
        }

        public void CommitTrans ()
        {
            _trans.Commit();      
            _trans.Dispose();
            _trans = null;
            _conn.Close();
        }

        

        public void Dispose()
        {
            Dispose (true);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    if (_trans != null)
                    {
                        _trans.Rollback();
                        logger.Warn("transaction Rollback when Disposing");
                        _trans.Dispose();
                    }
                    if (_conn != null)
                        _conn.Dispose();
                    logger.Trace("DataContext is disposed");
                }

                _trans = null;
                _conn = null;
                _disposed = true;
            }
        }
    }
}

No hay comentarios:

Publicar un comentario