En un artículo anterior, ya vimos las mejoras de EntityFramework Core 6 respecto a su anterior versión. Se llegó a la conclusión de que efectivamente es más rápido;
¿Pero qué pasa si lo comparamos con Dapper? Lo vamos a ver en este artículo.
Dapper es el Micro ORM más popular ya que al igual que EF Core, facilita el trabajo con la base de datos. Aunque EF Core nos da más capacidades, trackear objetos, migrar esquemas de base de datos e interactuar con la base de datos sin necesidad de escribir código SQL. No obstante, Dapper tiene la ventaja que es sustancialmente más rápido a la hora de la lectura de base de datos.
¡Hasta ahora! Ya que con la llegada de mejora en el performance de EF Core 6 esta diferencia se ha visto reducida. Concretamente hay literatura que nos dice que que esta diferencia se ha reducido del 55% al 4.5%
«In the end, the gap between EF Core and Dapper was reduced from 55% to 4.5% and the overall speed of EF Core’s queries based on the Fortunes benchmark improved by 70%.» — Julie Lerman
He incluso Microsoft, ha proporcionado estos datos:
Así que vamos a comprobar estos datos. para ello, crearemos un modelo de datos con 3 entidades. Y realizaremos sobre las distintas lecturas, tanto con EF Core como con Dapper. Poblaremos la base de datos con más de 1 millón de registros, para tener un entorno lo más parecido a la realidad.
Modelo de datos
Usaremos el siguiente esquema de datos:
Básicamente tenemos deportes que tienen varios equipos, y cada uno de estos equipos tiene varios jugadores.
Como hemos comentado, necesitamos una cantidad de datos suficiente para ello, introduciremos 1000 deportes, 100.000 equipos, y 10 jugadores por equipo, lo que hacen 1.000.000 jugadores.
Queries
Realizaremos 3 tipos de consultas a base de datos:
GetPlayerById: La consulta más simple, recuperamos un jugador por Id
GetPlayersByTeam: Recuperamos el listado de todos los jugadores para un equipo
GetTeamPlayersForSport: Recuperamos un listado con todos los equipos y sus jugadores para un deporte
Estas 3 consultas las haremos a través de EntityFramework con tracking y sin tracking de entidades, y a través de Dapper. Con EF Core, haremos uso del IDbContextFactory.
Resultados
Lo primero es hacer una prueba rápida, ejecutaremos estas consultas y veremos qué tiempos nos devuelven.
Tenemos lo siguiente:
1 iteración:
EntityFramework | EntityFramework No Tracking | Dapper | Diff | |
GetPlayerById | 1566,53 ms | 8,42 ms | 59,42 ms | 86% |
GetPlayersByTeam | 108,74 ms | 8,24 ms | 10,83 ms | 24% |
GetTeamPlayersForSport | 28,18 ms | 20,03 ms | 35,08 ms | 43% |
10 iteraciones:
EntityFramework | EntityFramework No Tracking | Dapper | Diff | |
GetPlayerById | 7,29 ms | 3,75 ms | 2,41 ms | -55% |
GetPlayersByTeam | 6,66 ms | 5,95 ms | 4,48 ms | -32% |
GetTeamPlayersForSport | 154,90 ms | 76,4 ms | 76,77 ms | 0,4% |
Vemos que con 1 sola consulta, Entity sin tracking es más rápido que Dapper, no obstante, conforme aumenta el número de consultas Dapper se va haciendo más rápido, incluso pasa a Entity, siendo un 50% más rápido para la consulta mas sencilla. Aunque en consultas mas «complejas» y con muchos resultados, tienen tiempos equiparables.
Vemos claramente que, en las dos primeras consultas, Dapper es más rápido, siguiendo la progresión que habíamos visto con pocas iteraciones. Sin embargo, para la última consulta, tenemos unos tiempos similares o incluso menores de Entity sin tracking respecto a Dapper.
Para ver está diferencia porcentual, tenemos lo siguiente:
Se aprecia sin lugar a duda que conforme crece el número de iteraciones Dapper es más rápido para las dos primeras consultas. No obstante, para la tercera, tenemos a Entity como claro ganador en el primer cuartil, reduciéndose la diferencia respecto aumentamos las iteraciones. No obstante, vemos que tienen tiempos equiparables para un número alto de iteraciones.
Vamos a comprobar los resultados, haciendo un benchmark de todas las operaciones:
Method | Mean | Error | StdDev | StdErr | Median | Min | Q1 | Q3 | Max | Op/s | ||||||
GetPlayerById | 631.9 μs | 32.02 μs | 306.8 μs | 9.70 μs | 592.8 μs | 383.9 μs | 521.8 μs | 671.9 μs | 6,015.8 μs | 1,582.6 | ||||||
GetPlayerById_NoTracking | 615.3 μs | 19.78 μs | 189.5 μs | 5.99 μs | 589.3 μs | 374.1 μs | 516.4 μs | 663.9 μs | 4,241.0 μs | 1,625.2 | ||||||
GetPlayerById_Dapper | 449.0 μs | 14.25 μs | 136.5 μs | 4.32 μs | 422.1 μs | 237.1 μs | 377.9 μs | 489.3 μs | 2,759.7 μs | 2,227.2 | ||||||
Method | Mean | Error | StdDev | StdErr | Median | Min | Q1 | Q3 | Max | Op/s | ||||||
GetPlayersByTeamId | 805.7 μs | 27.49 μs | 263.4 μs | 8.33 μs | 783.0 μs | 511.8 μs | 680.8 μs | 873.8 μs | 7,261.6 μs | 1,241.1 | ||||||
GetPlayersByTeamId_NoTracking | 864.7 μs | 29.78 μs | 285.3 μs | 9.02 μs | 822.1 μs | 395.0 μs | 671.2 μs | 1,017.1 μs | 5,237.9 μs | 1,156.5 | ||||||
GetPlayersByTeamId_Dapper | 468.5 μs | 18.04 μs | 172.8 μs | 5.47 μs | 448.6 μs | 237.3 μs | 377.7 μs | 518.1 μs | 3,790.5 μs | 2,134.6 | ||||||
GetTeamPlayersForSport | 6,967.5 μs | 180.44 μs | 1,729.0 μs | 54.68 μs | 6,480.6 μs | 5,305.1 μs | 6,017.0 μs | 7,166.1 μs | 21,726.2 μs | 143.5 | ||||||
GetTeamPlayersForSport_NoTracking | 4,037.5 μs | 185.12 μs | 1,773.8 μs | 56.09 μs | 3,502.0 μs | 2,594.5 μs | 3,093.8 μs | 4,174.2 μs | 21,885.5 μs | 247.7 | ||||||
GetTeamPlayersForSport_Dapper | 4,530.4 μs | 184.09 μs | 1,763.9 μs | 55.78 μs | 3,910.2 μs | 3,227.3 μs | 3,653.0 μs | 4,396.8 μs | 19,343.6 μs | 220.7 |
Podemos observar que los datos concuerdan con el resto de las pruebas, ya que la tónica y orden de magnitud de las diferencias son las mismas. Obteniendo para las dos primeras consultas una diferencia del 35% y 70% respectivamente, siendo más rápido Dapper como ya habíamos visto. Y para la tercera tenemos una diferencia del 10%, dando como ganador a Dapper para todas las consultas.
Conclusiones
A la vista de los resultados, podemos afirmar que Dapper sigue siendo más rápido que EntityFramework en la mayoría de las situaciones que nos podemos encontrar.
Sin embargo, como ya había anunciado Microsoft, el gap que existía entre ambos ORMs se ha reducido hasta un 5%, en nuestras pruebas hemos comprobado que, en ciertos casos, sí se cumple, estando en torno al 10% para el último caso de prueba. Lo cual está en orden de magnitud, y hay que recordad que las pruebas tampoco has sido realizadas en el entorno más fiable para un test de rendimiento.
Como conclusión se puede decir que solo podemos comparar ambos ORMs para queries complejas donde se van a devolver muchos resultados. En los resultados del benchmark, podemos apreciar que para la query GetTeamPlayersForSport, el tiempo mínimo de Entity es casi un 20% que Dapper. Y la mediana sigue estando por debajo para Entity.
Por tanto, ahora mismo en mi opinión, aun no se puede hacer un cambio de Dapper a EntityFramework Core 6, ya que las diferencias en tiempo para la mayoría de los casos que nos vamos a encontrar siguen siendo significativas a favor de Dapper.
No obstante, para los casos en los que Entity es equiparable a Dapper puede ser recomendable el uso de Entity, ya que si no se tiene un conocimiento de SQL y la consulta es complicada, Entity nos aporta una gran ayuda para realizar la misma, y si no somos capaces de tener una consulta SQL con buena performance, será mejor hacer uso de EntityFramework.
Si quieres ver o descargar el código correspondiente a este artículo, está disponible en el siguiente enlace benchmark-EFCore-Dapper.
Siente libre de hacer cualquier aportación al mismo.