domingo, 3 de abril de 2016

Dapper vs EntityFramework, debo usar un MicroOrm o un Orm?


En muchos de nuestros proyectos hemos usado un Orm para lograr mapear nuestro modelo de datos a nuestros objetos y estructura de clases de nuestra aplicación, logrando con esto resolver el acceso y persistencia de datos sin mucho esfuerzo y poder enfocarnos en lo que verdaderamente nos importa, el dominio del negocio.



Los Orm han tomado mucha fuerza y actualmente son usados en la mayoría de aplicaciones dado las facilidades y bondades que ofrecen, Orms como Nhibernate, EntityFramework, Hibernate, Telerik Data Access, son ejemplos de Orms bastante populares y usados en la actualidad, debido a las facilidades que ofrecen y buenas prácticas y patrones que encapsulan.

Pese a esto siempre se ha discutido con respecto al rendimiento que estos nos ofrecen y cómo pueden afectar el desempeño de nuestras aplicaciones, debido a las facilidades y funcionalidades que ofrecen se vuelven bastante robustos para preparar y acceder a la información. Por ejemplo EntityFramework está basado sobre la arquitectura de Ado.Net lo cual sugiere que tenemos una capa intermedia, que obtiene el resultado a través de los componentes de este y posteriormente mapea el resultado a un objeto de nuestra aplicación, este proceso sugiere algunos milisegundos más en cada transacción.

Ahora, que pasa si tuviéramos una alternativa un poco más liviana y rápida que sacrifique algunas facilidades y características para garantizar el rendimiento en el acceso a datos? pues de esto se trata un MicroOrm, este nos permite realizar un mapeo de nuestro modelo relacional a objetos de nuestra aplicación, solo que lo hace de una forma más simple y sin algunas características como por ejemplo manejo de relaciones entre tablas, manejo de concurrencia, cargas por demanda, diseñador del modelo, etc.

Entre los MicroOrm más famosos encontramos los siguientes:
  • PetaPoco
  • Insight
  • NPoco
  • Dapper (Se puede decir que este es el MicroOrm más conocido y usado, debido a que se ha comprobado que es el que mejor rendimiento tiene, tan solo un milisegundo más que una consulta directamente con Ado.Net, adicional este es el MicroOrm usado por el reconocido foro StackOverflow, para soportar su robusta arquitectura)
Ahora que ya conocemos que es un micro Orm y sabemos cuál es su principal diferencia con un Orm, vamos a ver cómo funciona y cómo es su implementación, para esto vamos a usar Dapper, y adicional vamos a realizar una serie de comparaciones de rendimiento y de codificación con EntityFramework.

Para nuestro ejemplo vamos a usar la conocida base de datos Northwnd, la cual puedes descargar del siguiente enlace: https://northwinddatabase.codeplex.com/

Posteriormente vamos a crear en Visual Studio un nuevo de proyecto de tipo consola y en la clase Program, vamos a empezar a codificar el ejemplo.

Para iniciar vamos a abrir la consola del administrador de paquetes Nuget y vamos a ejecutar el siguiente comando para instalar el paquete de Dapper:

Install-Package Dapper -Version 1.42.0

Y hacemos el Using del NameSpace Dapper en la clase Program, con esto se agregan una serie de métodos de extensión a la clase SqlConnection las cuales nos permiten usar el MicroOrm para interactuar con la base de datos.

Para la conexión a la base de datos a través de Dapper solo necesitamos especificar la cadena de conexión, para esto crearemos una constante en clase Program en la cual la almacenaremos, para efectos de nuestro ejemplo vamos a usar cadenas mágicas en el código y no archivos de configuración, dado que esto no es relevante para el ejercicio:

private const string Cadena = "Initial Catalog=NORTHWND;Data Source=TAVOPC;Integrated Security=SSPI;";

Escenario de una consulta simple a una sola tabla

Ahora vamos a codificar nuestro primer escenario, en el cual vamos a consultar en la tabla Ordenes, a través de Dapper y vamos a mapear el resultado a la entidad Orden (entidad creada por nosotros)

private static void ConsultarOrdenesDapper()
{
    using (IDbConnection db = new SqlConnection(Cadena))
    {
        var consulta = @"SELECT [OrderID] AS Id, [CustomerID] AS IdCliente, [EmployeeID] AS IdEmpledo,
                    [OrderDate] AS Fecha, [RequiredDate] AS FechaRequerida, [ShipCountry] AS Pais, [ShipCity] AS Ciudad
                    FROM [NORTHWND].[dbo].[Orders]";
 
        var stopwatch = new Stopwatch();
        stopwatch.Start();
 
        // Se ejecuta la consulta.
        var ordenes = db.Query<Orden>(consulta);
 
        stopwatch.Stop();
        System.Console.WriteLine("Dapper.Net: {0} registros obtenidos en {1} milisegundos.", ordenes.Count(),
          stopwatch.ElapsedMilliseconds);
    }
}

Como podemos ver es bastante sencillo usar Dapper, en este caso a través de la clase SqlConnection establecemos la conexión con la base de datos, y ejecutamos el método Query, especificando el tipo en el que queremos obtener el resultado de la consulta, cabe mencionar que el método Query no se encuentra en la clase SqlConnection por defecto, este es un método de extensión agregado por el Nuget de Dapper. Ahora invocaremos el método ConsultarOrdenesDapper desde el método Main de la clase program, y ejecutamos el programa para ver los resultados.


Como podemos apreciar obtenemos un resultado bastante óptimo de 856 registros en 4 milisegundos, ahora en cuánto tiempo resolverá EntityFramework esta consulta? pues vamos a averiguarlo.

Para crear nuestro modelo conceptual a través de EntityFramework, vamos a usar el enfoque "Code First From DataBase" el cual permite usar CodeFirst con una base de datos existente y crea el contexto y las entidades a partir de ella, observa aquí un tutorial detallado acerca de cómo usar este enfoque: CodeFirst From DataBase

Ahora que ya tenemos nuestro contexto vamos a codificar la misma consulta que realizamos con Dapper, pero esta vez la ejecutaremos a través de EntityFramework.

private static void ConsultarOrdenesEf()
{
    using (var contexto = new NorthwndModel())
    {
      var stopwatch = new Stopwatch();
      stopwatch.Start();
 
      var ordenes = contexto.Orders.ToList();
 
      stopwatch.Stop();
      System.Console.WriteLine("EntityFramework: {0} registros obtenidos en {1} milisegundos.", ordenes.Count,
      stopwatch.ElapsedMilliseconds);
    }
}

Ahora de igual forma vamos a invocar el método ConsultarOrdenesEf desde el método main de la clase Program, y ejecutamos de nuevo la aplicación para ver el resultado:


Pues la diferencia es bastante grande de 6 milisegundos de Dapper a 95 milisegundos de EntityFramework, sin lugar a dudas es una diferencia significativa, si bien 95 milisegundos siguen siendo poco esos milisegundos van sumando en cada transacción, y en una aplicación de alta demanda por ejemplo sí que se notarían y afectarían muy negativamente el rendimiento.

Ahora que hemos realizado este primer escenario comparativo podemos concluir que definitivamente el rendimiento de Dapper es muy superior al de EntityFramework, dado que como había mencionado antes el MicroOrm es muy rápido y liviano, por el contrario el Orm es bastante robusto y ofrece muchas más extensiones y funcionalidades que penalizan un poco el rendimiento. Sin embargo si analizamos y comparamos detenidamente el código de cada una de las implementaciones podemos decir que la implementación de EntityFramework es mucho más limpia, trazable y mantenible, otro aspecto muy importante en nuestras aplicaciones. Adicional vale la pena considerar el escenario en el cual trabajamos con tablas relacionadas, escenario que encontraremos en todas nuestras aplicaciones y que veremos a continuación.

Escenario de consulta a dos tablas relacionadas

Sin duda una de esas características que brilla por su ausencia en los MicroOrm con respecto a los Orm, es el manejo de relaciones entre entidades de una forma sencilla y objetual, obviamente el Micro Orm no tiene esto entre sus prioridades, pero si que hace falta. Para plantear este escenario vamos a realizar una consulta en la tabla ordenes y en la tabla ordenes detalle, en un escenario Eaguer, es decir donde necesitamos obtener la información en una sola consulta a la base de datos y no por demanda, vemos como sería la implementación con Dapper:

private static void ConsultarOrdenesDetallesDapper()
{
            var consulta = @"SELECT a.[OrderID] AS Id
	                    ,a.[CustomerID] AS IdCliente
	                    ,a.[OrderDate] AS Fecha
                          ,b.[ProductID] AS IdProducto
                          ,b.[UnitPrice] AS Precio
                          ,b.[Quantity] AS Cantidad
                          ,b.[Discount] AS Descuento
                      FROM [NORTHWND].[dbo].[Order Details] b
                      INNER JOIN [NORTHWND].[dbo].[Orders] a
                      ON a.OrderID = b.OrderID;";
 
            using (IDbConnection db = new SqlConnection(Cadena))
            {
                var stopwatch = new Stopwatch();
                stopwatch.Start();
 
                // Se ejecuta la consulta.
                var ordenesDetalle = db.Query<DetalleOrden>(consulta);
 
                stopwatch.Stop();
                System.Console.WriteLine("Dapper.Net: {0} registros obtenidos en {1} milisegundos.",
                    ordenesDetalle.Count(), stopwatch.ElapsedMilliseconds);
            }
}

De la implementación anterior podemos observar que se hace una consulta involucrando las dos tablas y se traen todos los registros de ambas, es decir se repiten los registros de la tabla ordenes por cada registro asociado en la tabla ordenesDetalle, y debido a esto se creó la entidad DetalleOrden, lo cual es una implementación que no nos permite orientar correctamente a objetos, ya que deberíamos tener la entidad orden y la entidad detalle separadas y cargarlas con la data, esto se debe a que el MicroOrm no permite manejar las relaciones y esta es una solución al escenario que planteamos de carga total. Ahora veamos como logramos la misma implementación pero usando EntityFramework.

private static void ConsultarOrdenesDetallesEf()
{
      using (var contexto = new NorthwndModel())
      {
         var stopwatch = new Stopwatch();
         stopwatch.Start();
 
         var ordenesDetalle = contexto.Orders.Include("Order_Details").ToList();
 
         stopwatch.Stop();
         .Console.WriteLine("EntityFramework: {0} registros obtenidos en {1} milisegundos.",
         ordenesDetalle.Count, stopwatch.ElapsedMilliseconds);
      }
}

De nuevo podemos observar a simple vista que la implementación con EntityFramework es mucho más limpia y orientada a objetos, hacemos una consulta sobre la tabla Ordenes y especificamos que se incluyan los datos de la tabla relacionada o propiedad de navegación Order_Details, de esta forma se obtienen los registros:


Todos los datos en sus respectivas entidades de una forma objetual, ahora veamos el rendimiento de cada de una de las implementaciones:


Sigue existiendo una diferencia abismal en el rendimiento de las consultas, y ahora que EntityFramework está gestionando las relaciones ha aumentado sustancialmente, dado que debe mapear las entidades y cada registro de las entidades de una forma anidada.

Escenario de inserción en una sola tabla

Ahora vamos a ver un par de escenarios en los cuales no consultaremos información si no que la guardaremos, para observar cómo se comportan tanto el MicroOrm como el Orm, veamos la implementación para Dapper y para EntityFramework:

private static void InsertarOrdenDapper()
{
    var consulta =
                $@"INSERT INTO [NORTHWND].[dbo].[Orders] (CustomerID, OrderDate) 
                    VALUES ('VINET', '{DateTime.Now.ToString("yyyy-MM-dd")}');";
    using (IDbConnection db = new SqlConnection(Cadena))
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();
 
        // Se ejecuta la instrucción.
        db.Execute(new CommandDefinition(consulta));
 
        stopwatch.Stop();
        System.Console.WriteLine("Dapper.Net: un registro insertado en {0} milisegundos.",
        stopwatch.ElapsedMilliseconds);
    }
}

private static void InsertarOrdenEf()
{
    using (var contexto = new NorthwndModel())
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();
 
        var orden = new Order
        {
            CustomerID = "VINET",
            OrderDate = DateTime.Now
        };
 
        contexto.Orders.Add(orden);
        contexto.SaveChanges();
 
        stopwatch.Stop();
        System.Console.WriteLine("EntityFramework: un registro insertado en {0} milisegundos.",
                    stopwatch.ElapsedMilliseconds);
    }
}

Como podemos ver dos implementaciones muy sencillas en Dapper y en EntityFramework para insertar un registro, ahora comparemos el rendimiento de ambas implementaciones:


En este escenario de inserción de un solo registro nos damos cuenta que en este aspecto la diferencia de rendimiento entre Dapper y EntityFramework no es tan grande, solo 7 milisegundos una diferencia en tiempo realmente pequeña.

Escenario de inserción en dos tablas relacionadas

Para terminar la comparación entre Dapper y EntityFramework vamos a ver el último escenario en el cual vamos a insertar información en dos tablas relacionadas, veamos entonces como es el código a implementar para cada tecnología:

private static void InsertarOrdenDetalleDapper()
{
      var consulta =
        $@"INSERT INTO [NORTHWND].[dbo].[Orders] (CustomerID, OrderDate) 
        VALUES ('VINET', '{DateTime.Now.ToString("yyyy-MM-dd")}') select SCOPE_IDENTITY();";
      using (IDbConnection db = new SqlConnection(Cadena))
      {
          var stopwatch = new Stopwatch();
          stopwatch.Start();
 
          // Se almacena la orden y se obtiene su Id Identity.
          var idOrden = (int)db.Query<decimal>(consulta).First();
 
          consulta = $@"INSERT INTO [NORTHWND].[dbo].[Order Details] 
          VALUES ({idOrden}, 41, 20, 100, 0);";
          db.Execute(new CommandDefinition(consulta));
 
          stopwatch.Stop();
          System.Console.WriteLine("Dapper.Net: registros relacionados insertados en {0} milisegundos.",
          stopwatch.ElapsedMilliseconds);
      }
}

private static void InsertarOrdenDetalleEf()
{
    using (var contexto = new NorthwndModel())
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();
 
        var orden = new Order
        {
            CustomerID = "VINET",
            OrderDate = DateTime.Now,
            Order_Details =
            {
                new Order_Detail
                {
                    ProductID = 41,
                    UnitPrice = 20,
                    Quantity = 100,
                    Discount = 0
                }
            }
        };
 
        contexto.Orders.Add(orden);
        contexto.SaveChanges();
 
        stopwatch.Stop();
        System.Console.WriteLine("EntityFramework: registros relacionados insertados en {0} milisegundos.",
        stopwatch.ElapsedMilliseconds);
    }
}

Como podemos observar en este último escenario para EntityFramework creamos un solo objeto, especificando la información para la entidad padre y también para para la propiedad de navegación y EntityFramework se encarga de ejecutar las dos instrucciones de inserción, por el contrario con Dapper se debe ejecutar en primera instancia la instrucción para la tabla padre y obtener su Id, para posteriormente ejecutar la instrucción de la tabla relacionada, ahora observemos el desempeño de ambas implementaciones:


Como en el escenario anterior vemos que la diferencia no es muy significativa, cuando se trata de operaciones de inserción. Dados los anteriores escenarios podríamos concluir que Dapper tiene un rendimiento muy superior a EntityFramework en cuanto a consulta de datos se refiere, sin embargo en operaciones de inserción tienen un rendimiento muy similar. También podemos concluir que usando un Orm podemos lograr implementaciones mucho más limpias y objetuales y un código más mantenible.

En resumidas cuentas podemos concluir que los Micro Orm nos permiten practicidad, simplicidad y óptimo rendimiento, sacrificando un poco la mantenibilidad y orientación a objetos, mientras que los Orm nos permiten mejor mantenibilidad y orden sacrificando un poco el rendimiento, elegir uno u otro siempre dependerá del escenario que estemos resolviendo y según lo requieran nuestro atributos de calidad, recordemos que en el desarrollo de software no existen las balas de plata, y ambas tecnologías pueden ser muy válidas en determinados escenarios.

Y bueno amigos con esto damos por terminado el artículo, espero que esta comparación entre Dapper y EntityFramework les haya resultado interesante y de utilidad.

El código fuente lo puedes descargar de mi repositorio en GitHub.



No olvides visitar mi página en Facebook para mantenerte actualizado de lo que pasa en el Tavo.Net https://www.facebook.com/eltavo.net

Saludos, y buena suerte!

10 comentarios:

  1. Excelente aporte, ojala sigas compartiendo mas informacion sobre dapper que esta muy interesante y las buenas practicas en desarrollo.

    ResponderEliminar
    Respuestas
    1. Muchas gracias por tu comentario Cristian, esa es la idea seguir compartiendo conocimiento a través de este blog, que bueno que te haya resultado interesante, saludos!

      Eliminar
    2. Por segunda vez gracias, lo he empezado a usar, y hasta ahora solo me ha ahorrado trabajo, excelente dapper.

      Eliminar
    3. Que bueno que te haya servido Cristian, la verdad es que Dapper si ahorra bastante trabajo, Saludos!

      Eliminar
  2. Hola Tavo, has tenido la oportunidad de probarlos con procedimientos almacenados? Saludos

    ResponderEliminar
    Respuestas
    1. Hola Eric, la verdad no he hecho la comparación usando sp, me imagino que debe ser muy similar el rendimiento al obtener los datos como en las consultas (me refiero al mapeo de los datos una vez son devueltos por el SP) pero ya que lo mencionas vale la pena realizar la comparación, saludos!

      Eliminar
  3. De acuerdo en que en algunos casos, tal vez la mayoría pensando en la facilidad de mantenimiento del código base, importa más la legibilidad del código. Desde que uno no esté haciendo ya un twitter (aunque las aplicaciones que hacemos son cada vez más para uso masivo), es mejor algo más limpio siempre que la diferencia en rendimiento no sea muy significativa.

    Una cosa en la que no estoy de acuerdo es en donde se dice respecto a un select algo complejo que se compara implementándolo con EF y Dapper: “la implementación con EntityFramework es mucho más limpia y orientada a objetos”... Es que en general (99.99% de los casos), las consultas no son un asunto “orientado a objetos” si no “orientado a datos". Yo suelo preguntar:

    ¿En donde se ve mejor un código con lógica de negocios? en C# o en SQL (SPs + cursores)?

    ¿En donde se ve mejor un código de consulta? en C# (LINQ) o en SQL?

    Cada uno es mejor en uno de los dos terrenos, para el que fueron concebidos y bien pensados. Entonces una consulta que comienza a ponerse compleja es mejor en una simple vista desde el motor de base de datos, se le pone un nombre descriptivo y desde el código de aplicación pues hasta con purito ADO.NET, tal vez envuelto en un pequeño helper (o bueno un microORM), queda claro. En ese caso me parece que el código SQL tiene tanto mejor rendimiento como mayor legibilidad.

    Entonces faltó considerar que no todo es ORM o microORM. Por supuesto para consultas sencillas de a una tablita y algo más allá, no vale la pena armarle una vista o SP, entonces estaría bien algo en el nivel de aplicación. Como todo en la vida, los extremos pues son malos.

    Por lo general el código de comandos pues va mejor en aplicación (para tu ejemplo en C#) mientras que el de consultas complejas y algo menos (en SQL para tu ejemplo que es en el contexto "relacional").

    ResponderEliminar
    Respuestas
    1. Saludos Jorge, antes que nada agradecerte por tu valioso aporte y comentario, de acuerdo contigo en cuanto a que en ocasiones podemos sacrificar un poco de rendimiento por mantenibilidad siempre y cuando esto no sea muy significativo. En el otro punto que mencionas, cuando implementamos nuestras aplicaciones orientadas a objetos es bastante importante la forma como interactuamos con los datos en función de esto, para esto nacieron los ORM, no digo que las consultas a la DB sean orientadas a objetos (A no ser de que estemos hablando de una base de datos orientada a objetos...) me refiero a la forma en que el ORM permite resolver esos resultados de una forma objetual, y el MicroOrm en este caso no lo hace tanto así. Estoy totalmente de acuerdo con lo que mencionas con respecto a los SP o vistas en la base de datos, de hecho soy partidario de usarlos en los escenarios que así lo ameritan, en este artículo me enfoque en un ORM (EF) y un MicroOrm (Dapper) en unos escenarios muy específicos, por eso no traje a colación consultas con ADO.NET por ejemplo ni SP, aunque de hecho no van en contra de un ORM, dado que estos soportan la ejecución o mapeo de SP, o ejecución de queries a través de ADO.NET en el caso particular de EF, dado que su arquitectura esta soportada por ADO.NET.
      Como menciono en el artículo no creo en las balas de plata en el desarrollo de software, y soy partidario de usar las tecnologías o formas de trabajo que apliquen según los diferentes escenarios.
      En este artículo me enfoqué en estos escenarios, sin embargo son totalmente válidos los que mencionas, de nuevo gracias por tu comentario y espero que los lectores de este artículo se remitan también a estos comentarios.

      Eliminar
    2. Este artículo lo considero interesante

      http://blog.koalite.com/2012/07/orms-vs-microorms-vs-ado-net-cuando-usar-cada-uno

      Tenemos ORM, MicroORM-DataMapper, ADO.NET y yo combinaría con generación de código básica (C#, SQL) para los típicos Insert - Update - Delete de tablas.

      Eliminar
  4. Hola, felicidades excelente artículo, no conocía Dapper y me pareció bastante amigable.

    Ahora una pregunta, en un proyecto con una base de datos de unas 15 tablas, sería práctico trabajar con EF y Dapper para diferentes escenarios? Por ej: para sentencias Insert, usar EF y para consultas más complejas a múltiples tablas usar dapper o vistas ?

    Saludos, muy buen artículo.

    ResponderEliminar