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!