Mayo de 2016

Volumen 31, número 5

Puntos de datos: Dapper, Entity Framework y aplicaciones híbridas

Por Julie Lerman

Julie LermanProbablemente, ha observado que escribo mucho sobre Entity Framework, el asignador relacional de objetos (ORM) de Microsoft, la API principal de acceso a datos de .NET desde 2008. Existen otros ORM de .NET, pero una categoría concreta, los microORM, obtiene muchas reseñas por su excelente rendimiento. El microORM del que más he oído hablar es Dapper. Lo que finalmente despertó mi curiosidad lo suficiente como para dedicar tiempo a probarlo recientemente fue que varios proveedores indicaron haber creado soluciones híbridas con EF y Dapper, lo que permitía a cada ORM hacer aquello que se le daba mejor en una sola aplicación.

Después de leer numerosos artículos y entradas de blog, chatear con desarrolladores y jugar un poco con Dapper, quería compartir mis descubrimientos con ustedes, especialmente con aquellos que, como yo, quizás habían oído hablar de Dapper, pero no sabían qué es, cómo funciona o por qué a la gente le encanta. Tenga en cuenta que no soy, en absoluto, una experta. De momento, solo sé lo suficiente para satisfacer mi curiosidad y espero que para despertar su interés para que siga indagando.

¿Por qué Dapper?

Dapper tiene una historia interesante. Se deriva de un recurso que podría resultarle muy familiar: Marc Gravell y Sam Saffron crearon Dapper mientras trabajaban en Stack Overflow para resolver problemas de rendimiento de la plataforma. Stack Overflow es un sitio con un tráfico realmente elevado que está destinado a tener problemas de rendimiento. De acuerdo con la página About de Stack Exchange, Stack Overflow tenía 5700 millones de vistas de página en 2015. En 2011, Saffron escribió una entrada de blog sobre el trabajo que había realizado con Gravell, titulada, "How I Learned to Stop Worrying and Write My Own ORM" (Cómo aprendí a dejar de preocuparme y a escribir mi propio ORM) (bit.ly/), en la que se explican los problemas de rendimiento que Stack tenía en ese momento, derivados del uso de LINQ to SQL. Luego, detalla por qué escribir un ORM personalizado, Dapper, era la solución para optimizar el acceso a datos en Stack Overflow. Cinco años después, Dapper es una aplicación de código abierto que se usa ampliamente. Gravell y Stack, junto con el miembro del equipo Nick Craver, siguen administrando activamente el proyecto en github.com/StackExchange/dapper-dot-net.

Dapper en pocas palabras

Dapper se centra en permitirle ejercitar sus conocimientos de SQL para construir consultas y comandos como cree que deben ser. Está más cerca del "final" que un ORM estándar y alivia el esfuerzo de interpretar consultas como LINQ to EF en SQL. Dapper presenta algunas estupendas características transformacionales, como la capacidad de explosionar una lista pasada a una cláusula WHERE IN. Sin embargo, en gran parte, el lenguaje SQL que envía a Dapper está listo y las consultas llegan a la base de datos mucho más rápido. Si es bueno con SQL, puede estar seguro de que escribe los comandos con el más alto rendimiento posible. Debe crear algún tipo de conexión IDbConnection, como SqlConnection, con una cadena de conexión conocida, para poder ejecutar las consultas. Posteriormente, con su API, Dapper puede ejecutar las consultas por usted y, si el esquema de los resultados de las consultas puede relacionarse con las propiedades del tipo de destino, puede crear instancias automáticamente y rellenar objetos con los resultados de la búsqueda. Aquí existe otra ventaja de rendimiento considerable: Dapper almacena en caché eficazmente la asignación aprendida, lo que da como resultado una deserialización muy rápida de las consultas posteriores. La clase que rellenaré, DapperDesigner (Figura 1), se define para administrar diseñadores que crean prendas muy elegantes.

Figura 1 Clase DapperDesigner

public class DapperDesigner
{
  public DapperDesigner() {
    Products = new List<Product>();
    Clients = new List<Client>();
  }
  public int Id { get; set; }
  public string LabelName { get; set; }
  public string Founder { get; set; }
  public Dapperness Dapperness { get; set; }
  public List<Client> Clients { get; set; }
  public List<Product> Products { get; set; }
  public ContactInfo ContactInfo { get; set; }
}

El proyecto donde ejecuto consultas presenta una referencia a Dapper, que recupero a través de NuGet (install-package dapper). He aquí una llamada de ejemplo de Dapper para ejecutar una consulta de todas las filas de la tabla DapperDesigners:

var designers = sqlConn.Query<DapperDesigner>("select * from DapperDesigners");

Tenga en cuenta que para los listados de código de este artículo, uso select * en lugar de proyectar columnas explícitamente para las consultas cuando quiero todas las columnas de una tabla. sqlConn es un objeto SqlConnection del que ya he creado una instancia, junto con la cadena de conexión correspondiente, pero que no he abierto.

El método Query es un método de extensión proporcionado por Dapper. Cuando se ejecuta esta línea, Dapper abre la conexión, crea un comando DbCommand, ejecuta la consulta exactamente como la he escrito, crea una instancia del objeto DapperDesigner para cada fila de los resultados e inserta los valores de los resultados de la consulta en las propiedades de los objetos. Dapper puede relacionar los valores de los resultados con las propiedades por medio de unos pocos patrones, aunque los nombres de propiedad no coincidan con los nombres de columna y aunque las propiedades no estén en el mismo orden que las columnas correspondientes. Pero no puede leer la mente. Por tanto, no espere descifrar asignaciones que impliquen, por ejemplo, numerosos valores de cadena donde los órdenes o los nombres de las columnas y las propiedades no estén sincronizados. Intenté llevar a cabo algunos experimentos extraños para ver cómo respondía y también existen algunas opciones de configuración globales que controlan cómo Dapper puede inferir asignaciones.

Dapper y las consultas relacionales

Mi tipo DapperDesigner tiene varias relaciones. Existe una de uno a varios (con productos), una de uno a uno (ContacInfo) y otra de varios a varios (clientes). He experimentado con la ejecución de consultas entre ellas y he comprobado que Dapper es capaz de controlar las relaciones. Definitivamente, no es tan fácil como expresar una consulta LINQ to EF con un método Include o incluso una proyección. Pero mis conocimientos de TSQL se llevaron al límite, dado que EF me ha permitido relajarme mucho durante los últimos años.

Este es un ejemplo de consulta en una relación de uno a varios mediante SQL que usaría directamente en la base de datos:

var sql = @"select * from DapperDesigners D
           JOIN Products P
           ON P.DapperDesignerId = D.Id";
var designers= conn.Query<DapperDesigner, Product,DapperDesigner>
(sql,(designer, product) => { designer.Products.Add(product);
                              return designer; });

Observe que el método Query me exige especificar ambos tipos que deben construirse, así como indicar el tipo que se va a devolver (expresado por el parámetro de tipo final, DapperDesigner). Uso una expresión lambda de varias líneas para construir los gráficos en primer lugar y agrego los productos correspondientes a sus objetos de diseñador principal. Luego, devuelvo cada diseñador al objeto IEnumerable que el método Query devuelve.

El inconveniente de hacerlo con mi mejor intento en SQL es que los resultados se aplanan, como estarían con el método Include de EF. Obtendré una fila por producto con los diseñadores duplicados. Dapper presenta un método MultiQuery que puede devolver varios conjuntos de resultados. Combinado con el objeto GridReader de Dapper, el rendimiento de estas consultas eclipsará definitivamente los métodos Include de EF.

Más difícil de codificar, más rápido de ejecutar

Expresar SQL y rellenar objetos relacionados son tareas que he dejado que EF realice en segundo plano, lo que implica sin duda más esfuerzo a la hora de codificar. No obstante, si trabaja con montones de datos y el rendimiento en tiempo de ejecución es importante, el esfuerzo valdrá realmente la pena. Tengo unos 30 000 diseñadores en mi base de datos de ejemplo. Solo algunos de ellos tienen productos. Realicé algunas pruebas comparativas en las que me aseguré de comparar manzanas con manzanas. Antes de observar los resultados de las pruebas, existen algunos aspectos importantes que debe comprender sobre cómo realicé estas mediciones.

Recuerde que, de manera predeterminada, EF está diseñado para realizar un seguimiento de los objetos que son los resultados de consultas. Esto significa que crea objetos de seguimiento adicionales, lo que implica algún esfuerzo, y también que necesita interactuar con estos. Dapper, en cambio, solo vuelca los resultados en la memoria. Por tanto, es importante extraer el seguimiento de cambios de EF del bucle al realizar comparaciones de rendimiento. Para ello, defino todas mis consultas de EF con el método AsNoTracking. Asimismo, al comparar el rendimiento, debe aplicar varios patrones de pruebas comparativas estándar, como el calentamiento de la base de datos, la repetición de la consulta muchas veces y la extracción de las repeticiones más lentas y más rápidas. Puede ver los detalles de cómo realicé las pruebas comparativas en la descarga de muestra. Sigo considerando que son pruebas comparativas "ligeras" que solo proporcionan una idea de las diferencias. Para las pruebas comparativas reales, debería realizar la iteración muchas más veces que mis 25 (empecemos por 500) y factorizar el rendimiento del sistema en el que las ejecute. Estoy ejecutando estas pruebas en un portátil con una instancia LocalDB de SQL Server, de modo que mis resultados solo son útiles para fines de comparación.

Las veces en que realizo un seguimiento en mis pruebas son para ejecutar la consulta y compilar los resultados. La creación de instancias de conexiones o clases DbContext no se tiene en cuenta. La clase DbContext se reutiliza, de modo que el tiempo que EF necesita para compilar el modelo en memoria tampoco se tiene en cuenta, ya que solo tendría lugar una vez por instancia de la aplicación, no para cada consulta.

La Figura 2 muestra las pruebas de "select *" para Dapper y la consulta LINQ de EF, de modo que puede ver la construcción básica de mi patrón de pruebas. Observe que aparte de la recopilación de tiempos real, estoy recopilando el tiempo de cada iteración en una lista (denominada "tiempos") para realizar más análisis.

Figura 2 Pruebas de comparación de EF y Dapper al consultar todas las clases DapperDesigner

[TestMethod,TestCategory("EF"),TestCategory("EF,NoTrack")]
public void GetAllDesignersAsNoTracking() {
  List<long> times = new List<long>();
  for (int i = 0; i < 25; i++) {
    using (var context = new DapperDesignerContext()) {
      _sw.Reset();
      _sw.Start();
      var designers = context.Designers.AsNoTracking().ToList();
      _sw.Stop();
      times.Add(_sw.ElapsedMilliseconds);
      _trackedObjects = context.ChangeTracker.Entries().Count();
    }
  }
  var analyzer = new TimeAnalyzer(times);
  Assert.IsTrue(true);
}
[TestMethod,TestCategory("Dapper")
public void GetAllDesigners() {
  List<long> times = new List<long>();
  for (int i = 0; i < 25; i++) {
    using (var conn = Utils.CreateOpenConnection()) {
      _sw.Reset();
      _sw.Start();
      var designers = conn.Query<DapperDesigner>("select * from DapperDesigners");
      _sw.Stop();
      times.Add(_sw.ElapsedMilliseconds);
      _retrievedObjects = designers.Count();
    }
  }
  var analyzer = new TimeAnalyzer(times);
  Assert.IsTrue(true);
}

Existe otro aspecto a tener en cuenta sobre la comparación de "manzanas con manzanas". Dapper usa instancias SQL sin procesar. De forma predeterminada, las consultas de EF se expresan con LINQ to EF y requieren algún esfuerzo para realizar la compilación de SQL automáticamente. Después de la compilación de SQL, aunque SQL se basa en parámetros, se almacena en la memoria caché de la aplicación, por lo que el esfuerzo disminuye en las repeticiones. Además, EF ofrece la posibilidad de ejecutar consultas con SQL sin procesar, por lo que he tenido en cuenta ambos enfoques. La Figura 3 muestra los resultados comparativos de cuatro conjuntos de pruebas. La descarga aún contiene más pruebas.

Figura 3 Tiempo medio en milisegundos que se tarda en ejecutar una consulta y rellenar un objeto basándose en 25 iteraciones, eliminando las más rápidas y las más lentas

*Consultas AsNoTracking Relación LINQ to EF* SQL sin procesar de EF* SQL sin procesar de Dapper
Todos los diseñadores (30 000 filas) 96 98 77
Todos los diseñadores con productos (30 000 filas) 1 : * 251 107 91
Todos los diseñadores con clientes (30 000 filas) * : * 255 106 63
Todos los diseñadores con contacto (30 000 filas) 1 : 1 322 122 116

 

En los escenarios que se muestran en la Figura 3, es fácil crear un caso para usar Dapper en lugar de LINQ to Entities. No obstante, las estrechas diferencias entre las consultas de SQL sin procesar podrían no justificar en todos los casos el cambio a Dapper para tareas concretas en un sistema en el que, por el contrario, usa EF. Naturalmente, sus propias necesidades serán diferentes y podrían influir en el grado de variación entre las consultas de EF y Dapper. No obstante, en un sistema de tráfico elevado, como Stack Overflow, incluso el puñado de milisegundos ahorrado por consulta puede ser crítico.

Dapper y EF para otras necesidades de persistencia

Hasta el momento, he medido consultas simples en las que simplemente retiraba todas las columnas de una tabla que coincidían exactamente con las propiedades de los tipos devueltos. ¿Qué sucede si proyecta consultas en tipos? Siempre que el esquema de los resultados coincida con el tipo, Dapper no observa ninguna diferencia en la creación de los objetos. Sin embargo, EF debe ir más allá si los resultados de la proyección no se alinean con un tipo que forma parte del modelo.

DapperDesignerContext incluye una clase DbSet para el tipo DapperDesigner. En mi sistema, tengo otro tipo denominado MiniDesigner que tiene un subconjunto de propiedades DapperDesigner:

public class MiniDesigner {
    public int Id { get; set; }
    public string Name { get; set; }
    public string FoundedBy { get; set; }
  }

MiniDesigner no forma parte de mi modelo de datos de EF, por lo que DapperDesigner­Context no conoce este tipo. Descubrí que consultar las 30 000 filas y proyectarlas en 30 000 objetos MiniDesigner era un 25 % más rápido con Dapper que con EF usando SQL sin procesar. De nuevo, le recomiendo que cree sus propios perfiles de rendimiento para tomar decisiones para su sistema.

Dapper también se puede usar para insertar datos en la base de datos con métodos que le permitan identificar qué propiedades deben usarse para los parámetros especificados por el comando, tanto si usa un comando INSERT o UPDATE sin procesar como si ejecuta una función o un procedimiento almacenado en la base de datos. No realicé ninguna comparación de rendimiento para estas tareas.

Dapper híbrido y EF en el mundo real

Existen muchísimos sistemas que usan Dapper para toda la persistencia de datos. Recuerde que fueron los desarrolladores que hablaban de las soluciones híbridas lo que despertó mi curiosidad. En algunos casos, son sistemas que tienen EF instalado y que buscan modificar áreas problemáticas concretas. En otros, los equipos han optado por usar Dapper para todas las consultas y EF para el almacenamiento.

En respuesta a mi pregunta sobre el tema en Twitter, recibí varios comentarios.

@garypochron me indicó que su equipo estaba "avanzando con Dapper en áreas con un alto volumen de llamadas importantes y usando archivos de recursos para mantener la organización de SQL". Me sorprendió saber que Simon Hughes (@s1monhughes), autor del popular EF Reverse POCO Generator, vaya en la dirección contraria, usando Dapper como aplicación predeterminada y EF para problemas complejos. Me dijo: "Uso Dapper cuando puedo. Si se trata de una actualización compleja, uso EF".

También he visto varias discusiones donde el enfoque híbrido se debe a una separación de las preocupaciones y no a una mejora del rendimiento. La más común aprovecha la confianza predeterminada de ASP.NET Identity en EF y, luego, usa Dapper para el resto de la persistencia de la solución.

Trabajar más directamente con esta base de datos tiene otras ventajas aparte del rendimiento. Rob Sullivan (@datachomp) y Mike Campbell (@angrypets), ambos expertos en SQL Server, están encantados con Dapper. Rob destaca que puede aprovechar las características de la base datos a las que EF no proporciona acceso, como, por ejemplo, la búsqueda de texto completo. A largo plazo, esta característica concreta tiene que ver con el rendimiento.

Por otro lado, hay cosas que se pueden hacer con EF y que Dapper no permite, aparte del seguimiento de cambios. Un buen ejemplo es uno que aproveché al compilar la solución que creé para este artículo: la capacidad de migrar la base de datos a medida que el modelo cambia por medio de Migraciones de Code First de EF.

No obstante, Dapper no es válido para todo el mundo. @damiangray me dijo que Dapper no es una opción para su solución porque necesita poder devolver interfaces IQueryables de una parte de su sistema a la otra, no datos reales. Este tema sobre la ejecución de consultas en diferido se incluye en el repositorio de GitHub de Dapper en bit.ly/22CJzJl, por si quiere leer más información al respecto. Al diseñar un sistema híbrido, el uso de algún tipo de separación de comandos y consultas (CQS) en el diseño de modelos diferentes para tipos particulares de transacciones (algo que me encanta) es una buena opción. De este modo, no intenta compilar código de acceso a datos que sea suficientemente simple para funcionar con EF y Dapper, lo que suele provocar el sacrificio de algunas ventajas de cada ORM. Mientras trabajaba en este artículo, Kurt Dowswell publicó una entrada titulada "Dapper, EF, and CQS" (Dapper, EF y CQS) (bit.ly/1LEjYvA). Práctico para mí y para usted.

Para aquellos que prevén usar CoreCLR y ASP.NET Core, Dapper ha evolucionado para incluir también compatibilidad con estas aplicaciones. Puede encontrar más información en un subproceso del repositorio de GitHub de Dapper en bit.ly/1T5m5Ko.

Finalmente, me fijé en Dapper. ¿Qué pienso?

¿Qué hay de mí? Lamento no haber dedicado tiempo a conocer Dapper antes y me alegro de haberlo hecho finalmente. Siempre he recomendado AsNoTracking o usar vistas o procedimientos en la base de datos para reducir los problemas de rendimiento. Es una solución que nunca me ha fallado, y a mis clientes tampoco. Pero ahora sé que tengo otro truco para recomendar a los desarrolladores interesados en exprimir aún más rendimiento de sus sistemas que usan EF. No es coser y cantar, como solemos decir. Mi recomendación es explorar Dapper, medir la diferencia de rendimiento (a escala) y buscar un equilibrio entre rendimiento y facilidad de codificación. Considere el uso obvio de StackOverflow: realizar consultas, comentarios y respuestas y, luego, devolver gráficos de una pregunta con sus comentarios y respuestas junto con algunos metadatos (ediciones) e información de usuario. Realizan los mismos tipos de consultas y asignan la misma forma de resultados una y otra vez. Dapper se ha diseñado para destacar con este tipo de consultas repetitivas, y ofrece cada vez más inteligencia y rapidez. Aunque no tenga un sistema con el número desmesurado de transacciones para el que se diseñó Dapper, es posible que descubra que una solución híbrida es la solución a sus necesidades.


Julie Lerman es una Microsoft MVP, mentora y consultora de .NET que vive en las colinas de Vermont. Puede encontrarla haciendo presentaciones sobre el acceso a datos y otros temas de .NET a grupos de usuarios y en conferencias en todo el mundo. Su blog es thedatafarm.com/blog y es la autora de "Programming Entity Framework", así como de una edición de Code First y una edición de DbContext, de O’Reilly Media. Sígala en Twitter en @julielerman y vea sus cursos de Pluralsight en juliel.me/PS-Videos.

Gracias a los siguientes expertos técnicos de Stack Overflow por revisar este artículo: Nick Craver y Marc Gravell
Nick Craver (@Nick_Craver) es diseñador, ingeniero de fiabilidad de sitios y, en ocasiones, administrador de bases de datos de Stack Overflow. Es especialista en ajustar el rendimiento en todos los niveles, la arquitectura global del sistema, el hardware del centro de datos y el mantenimiento de los proyectos de código abierto, como Opserver. Encuéntrelo en.

Marc Gravell es desarrollador en Stack Overflow. Su trabajo se centra principalmente en las bibliotecas y herramientas de alto rendimiento de .NET, en especial, las API de red, acceso a datos y serialización, que contribuyen en varios proyectos de código abierto en estas áreas.