A menudo he escuchado a las personas que se rehúsan a usar un ORM que su principal razón es "Tiene problemas de desempeño" o que "Crean un mal diseño de base de datos" lo cual no es completamente cierto, ¿A que me refiero con eso? Bueno, el ORM por si mismo no tiene problemas de desempeño. Aunque es cierto que tienen que hacer trabajo extra cuando están haciendo su magia, ese trabajo extra por si mismo no es en realidad nada por lo cual deberíamos preocuparnos y puedes crear un diseño de BD muy explicito y especifico cuando usas un ORM, puedes especificar exactamente que tipo de datos SQL usaras, indices y más.

Observemos un benchmark creado por el asombroso equipo que trabaja en Dapper un micro ORM creado y usado por stackoverflow

Nota: He removido algunos ORM de la lista para facilitar la concentración en EF y HandCoded, HandCoded significa sin usar ningún ORM, simplemente SQL puro si deseas ver el benchmark completo puedes verlo aquí: Dapper benchmark

youtube-playlist

Podrías alarmarte inicialmente al ver la diferencia en los números entre HandCoded y EF 6 o Core pero estamos hablando de (us) microsegundos y 1 microsegundo es un una millonésima de un segundo para obtener algo de perspectiva el ojo humano tarda 350,000 microsegundos para parpadear (aproximadamente un tercio de un segundo). así que no hay forma que un humano perciba una diferencia de 200 microsegundos y si comparamos esa diferencia en microsegundos contra el tiempo de desarrollo que nos ahorramos al usar un ORM contra HandCoded SQL usar un ORM definitivamente vale la pena.

Entonces si los problemas de desempeño no son causados directamente por el ORM ¿por que es que se han ganado ese tipo de reputación con tantas personas? Bueno porque si es cierto que puede ser peligroso usarlos si no estas consciente de los peligros y trampas comunes cuando usas uno y la parte tramposa es que muchos de estos problemas solo salen a la luz cuando la aplicación comienza a crecer y de nuevo no es que el ORM sea incapaz de manejar muchos datos (que es la conclusión a las que muchas personas llegan erróneamente) es principalmente porque lo estamos usando incorrectamente y no nos damos cuenta porque durante el desarrollo por lo general usamos base de datos que no tienen muchos datos.

Nota: Puede que en ocasiones el ORM tenga problemas por ejemplo cuando se tiene que hacer bulk insert o bulk update pero la mayoría de los ORM tienen su manera especial de tratar con esto pero tienes que ver la manera apropiada de hacerlo.

Adicionalmente la documentación y tutoriales de los ORM se enfocan en mostrarte lo rápido y fácil que son de usar, de tal forma que en cuestión de minutos podrías comenzar a usar un ORM, pero normalmente vas a encontrar una parte en la documentación relacionada con "Performance considerations" y te recomiendo buscar y leer esas partes antes de comenzar a usarlo.

En este post:

Para explorar estos problemas voy a usar un dominio muy simple de un Restaurante que quiere manejar su menú, necesitan poder manejar categorías del menú tales como Entradas, Platos fuertes, Bebidas, Postres, etc y cada categoría va a tener muchos elementos.

Voy a simplificar bastante para poder concentrarnos en entender los problemas de tal forma que solo usare dos tablas muy simple a como se ve en la siguiente imagen.

erd

¡Comencemos a explorar estos problemas!

# 1. No saber que tipo de datos SQL son generados para tus tablas

Este es particularmente peligroso cuando se usa un lenguaje fuertemente tipado ya que el ORM puede intentar inferir los tipos de datos SQL a partir de los tipos que existen en el lenguaje pero no siempre coinciden 1 a 1. Veamos el siguiente modelo como ejemplo usando C #:


    public class MenuCategory
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Order { get; set; }
        public string ImageUrl { get; set; }

        public DateTime DateCreated { get; set; }
    }

Esta es simplemente una clase de C # que no tiene nada de especial se le conoce como POCO, EF tiene la fortaleza para usar mi POCO y crear tablas SQL usando sus convenciones, todo lo que necesito hacer es agregar mi clase como DbSet a mí DBContext

Ahora puedo correr los comandos de EF para crear las migraciones y eso me genera un archivo de migración y un Model Snapshot

youtube-playlist

Yo personalmente recomiendo inspeccionar las migraciones generadas para estar más al tanto de lo que vas a ejecutar en tu base de datos. Especialmente cuando empiezas a usar un nuevo ORM si abro el Snapshot de mi modelo, ya puedo ver algunas cosas que son problemáticas.

Si ves las líneas 23 y 26, puedes ver el nvarchar (max) Eso no es lo que quiero, lo bueno es que si tengo la costumbre de verificar el archivo de migraciones es que puedo detenerme aquí antes de ejecutar las migraciones y solucionar cualquier problema, pero continuaré y ejecutaré la migración y veré qué obtenemos.

youtube-playlist

Puedes ver que termino con dos nvarchar (max) que son nulables y eso no es lo que deseo, quiero definir un límite y que los campos no sean nulables. Las convenciones son útiles ya que ayudan a desarrollar más rápido, pero debes ser consciente de cuándo debes ser explícito en tus configuraciones, como en este caso necesito detallar cómo quiero asignar explícitamente a los tipos de SQL. En EF tengo dos opciones dos para esto Data Annotations de datos o Fluent API

Data Annotations

Fluent API

Ambos ayudarán a lograr nuestro objetivo, que es tomar más control sobre los detalles de los tipos SQL, como definir una longitud máxima y no nulo si creo y ejecuto migraciones ahora Obtengo exactamente lo que quiero.

youtube-playlist

Tal vez te resulte tedioso tener que definir esto para cada campo string, en ese caso, se puede sobre-escribir la convención predeterminada para los valores de string y establecerla en 250 en lugar de Max y no nulo en lugar de nulo de esa manera solo necesitas definir este tipo de mapeo para campos string que estás seguro usará menos o más de 250.

En el siguiente ejemplo, configuro todos los campos string para que tengan una longitud máxima de 250 y no sean nulos, por lo que solo necesito indicar que deseo que mi campo ImageUrl tenga una longitud máxima de 500.

Esto no solo se aplica a los campos string, sino también a otros tipos, otro error común que he visto es que las personas usan DateTime cuando en realidad solo necesitaban Date, por lo que el código está lleno de AlgunCampoDatetime.Date y extrañas cláusulas where para eliminar la parte de tiempo de los campos DateTime, esto normalmente sucede porque C # solo tiene el tipo DateTime y normalmente se asigna a DateTime en SQL a menos que especifique específicamente el tipo como he mostrado.

En resumen:

  • Ten en cuenta las convenciones que utiliza el ORM
  • Si el ORM genera las migraciones de la base de datos, ten la costumbre de revisarlas
  • Trata de ser específico y explícito sobre cómo mapear tus modelos a tablas SQL
  • Use el poder del ORM para tratar de evitar escribir muchos código repetitivo, por ejemplo: Convenciones personalizadas
  • Verifica las tablas SQL que se generaron cuando eres nuevo usnado el ORM

# 2. No saber el momento en que la consulta SQL es ejecutada

La mayoría de los ORM te permitirán preparar y agregar a tu consulta todo lo que necesites antes de ejecutarla, supongamos que quiero obtener todos las Categorías de menú activas. Idealmente, eso significa que quiero una consulta SQL que se vea como Select * from MenuCategories donde Active = 1 usando EF esto muy simple.

Esto ejecutará el tipo de consulta que quiero y producirá los resultados esperados, sin embargo, si invertiera el orden en el que estoy llamando a los métodos Where y ToList, aún produciré los resultados que quiero pero en realidad cargaré todos los registros del base de datos y luego hare un filtro en memoria con solo unos pocos registros no notaré ninguna diferencia de rendimiento, pero cuando la cantidad de datos aumenta, el rendimiento se vera seriamente afecta. Puedes pensar que esto es obvio, pero es solo así si sabes cuándo se ejecuta tu consulta SQL. Supongamos que ahora necesitamos ordenar por el campo de Order que tenemos sobre en Tabla y el desarrollador no sabe sobre esto y solo hace una búsqueda rápida en Google y aprende sobre el método OrderBy de Linq el podría venir y simplemente hacer lo siguiente:

Verá los resultados esperados y considerará su trabajo terminado sin saber que no está ordenando en la base de datos, sino en la memoria, lo mismo podría suceder cuando tuviera que agregar filtros si no se entiende cuando se ejecuta la consulta.

En resumen:

  • Asegurate de comprender cuándo se ejecuta la consulta, Un ORM puede tener varias instrucciones que harán que la consulta se ejecute, intente conocerlas todas
  • Ten en cuenta que se puede ordenar y filtrar con el lenguaje de programación y producir los mismos resultados, pero podrías pagar un precio alto de rendimiento
  • Asegurate de saber cuándo estas haciendo una operación en la base de datos versus en la memoria, depura si es necesario para saber cuándo se ha ejecutado tu consulta

# 3. No usar proyección

La proyección se refiere a indicar exactamente qué campos deseas extraer de la base de datos, toma la siguiente consulta como ejemplo Select foo, var from ... la parte [foo, var] es la proyección que indica exactamente qué campos de la base de datos quiero extraer, el numero de campos se traducen en lecturas físicas que pueden afectar el rendimiento en función del número de campos y filas que la tabla tiene.

Imagina el siguiente escenario, necesitamos llenar un menú desplegable con las categorías de menú activas en nuestra base de datos ordenadas por el campo Orden. Puedo usar la consulta que ya tengo, necesito los campos Id y Nombre, así que simularé eso escribiendo solo esos campos en la Consola.

results-without-projections

He logrado los resultados que necesitaba, pero ¿solo lei los campos Id y Nombre de la BD? podemos averiguar usando el SQL Server Profiler que rastrea cada comando ejecutado contra la base de datos. Si no has usado SQL Server Profiler, puedes abrirlo desde SQL Server Management Studio en Herramientas -> SQL Server Profiler está disponible incluso en la edición express / gratuita.

sql-without-projection

Puedes ver que en realidad estoy sacando todos los campos, para esta tabla esto podría no ser tan importante, pero a medida que crece el número de campos y registros, la cantidad de lecturas también aumentará, así que ten en cuenta esto.

Usar proyección en EF es bastante simple, solo necesito especificar qué campos quiero en un método Seleccionar y asignar a un objeto anónimo o una clase específica en mi caso usaré un objeto anónimo

Esto producirá exactamente el mismo resultado que antes en la consola, pero si inspeccionamos SQL Server Profiler nuevamente

sql-with-projection

Puedes ver ahora que solo estoy leyendo los campos que realmente necesito. En resumen:

  • Ten en cuenta la cantidad de campos que estás extrayendo de la base de datos, si es necesario, se explícito y extrae solo los que necesitas
  • En caso de duda, utiliza cualquier herramienta que te permita ver la consulta SQL generada y ejecutada

# 4. No saber como el ORM maneja las trasacciones Atomic/Autocommit

Pasemos a los elementos del menú ahora, supongamos que tenemos algún tipo de interfaz de usuario que permite al usuario agregar los datos de una categoría de menú y también sus correspondientes elementos hijos, atomic significa que guardaremos todo de una vez o no guardaremos nada. Esto es muy común en aplicaciones empresariales cuando debes asegurarte de que todo tenga éxito o, de lo contrario, no guardar nada, mientras que Autocommit significa que haremos múltiples inserciones / actualizaciones de manera que si algo falla, ya hicimos algunas operaciones en la base de datos que se guardaron. Ambos enfoques tienen su uso correspondiente pero necesitamos entender cómo lo maneja nuestro ORM

EF por defecto es Atomic, puede manejar transacciones, por lo que podemos guardar todo de una vez, lo que puede ayudar al rendimiento al evitar múltiples viajes de ida y vuelta a la base de datos, sin embargo, podemos perder esta ventaja si no estamos seguros de lo que estamos haciendo, mira el siguiente código que guarda una categoría de menú y sus elementos de menú

El ejemplo anterior funcionará, sin embargo, provocará múltiples viajes de ida y vuelta a la base de datos, uno para guardar la categoría del menú y otro para cada elemento del menú y si el código falla al insertar alguno de los elementos, ya habrá guardado la categoría del menú en la base de datos debido a que db.SaveChanges () hará que EF guarde esos cambios. A veces eso puede ser lo que se desea, pero a veces las personas cometen este error porque no son conscientes de que EF es atomic y tiene el poder de mantener todos los cambios en la memoria. y luego guardar todo de una vez cuando esté listo, o el desarrollador solo ha usado ORM autocommit antes que te obliga a guardar el padre antes de poder guardar sus hijos correspondientes tal como como Django ORM , algunas veces los desarrolladores se ven obligados a usar SaveChanges varias veces porque tienen alguna extraña arquitectura de código que no tomó en cuenta el patrón unit of work de EF y deben llamar a SaveChanges en cada Servicio de dominio, pero cualquiera que sea el caso, puede llamar a SaveChanges una vez y se encargará de todo, por lo que nuestro código puede quedar de la siguiente manera

En resumen:

  • Entiende cómo el ORM maneja las transacciones y aprovéchalo
  • Verifica cómo el ORM maneja las inserciones/actualizaciones por lotes y hazlo de manera apropiada cuando manejes grandes cantidades de datos

# 5. No saber sobre el infame problema de consulta N + 1

Es posible que no conozcas este problema por su nombre formal, pero es posible que te hayas encontrado con él, puede causar un problema importante de rendimiento que consiste en consultar la base de datos de manera ineficiente cuando tienes algún tipo de relación maestro-detalle, y la mejor manera de entenderlo es con un ejemplo. Tenemos MenuCategory y MenuItem, una MenuCategory es algo así como Entradas y tendré muchos MenuItems relacionados que pertenecen a esa categoría. Así que imaginemos que necesito crear un menú que muestre todas las categorías y elementos de menú en esa categoría

Hay dos formas principales en que el ORM te permitirá cargar datos relacionados, Lazy Loading y Eager Loading. Este problema ocurre cuando se usa Lazy Loading, por lo que era más común encontrarlo en EF6, ya que viene con Lazy Loading habilitado de forma predeterminada en EF core está deshabilitado por defecto, pero voy ha habilitar lazy loading para demostrarlo. Usaré la misma consulta que estábamos usando pero sin proyección e imprimiré todas las categorías de menú con sus elementos de menú correspondientes.

La salida será exactamente lo que quería como puedes observar en la consola

output-menu-lazy

Ahora inspeccionando en Profiler el SQL generado para esto, puedo ver que he creado un problema de consulta n + 1

sql-lazyloading

Puede ver que se están ejecutando 5 consultas en lugar de 1, la primera consulta para tomar solo las categorías de menú y luego una consulta para cada categoría de menú para obtener sus elementos de menú de la base de datos, de ahí el n(n = Número de categorías = 4) +1 (La consulta original para tomar las categorías) como los otros problemas con solo unos pocos datos no noto ningún problema de rendimiento y si los datos en esta tabla no van a crecer mucho, esto podría estar bien, pero si estos datos crecen, puedo comenzar fácilmente a hacer miles de consultas en lugar de una y eso va a producir rápidamente serios problemas de rendimiento.

Simplemente podría usar Eager Loading para obtener todo desde el principio y solo hacer una consulta en EF. Simplemente necesito usar Include

Ahora el resultado será el mismo, pero si verifico nuevamente en SQL Server Profiler puedo ver que solo ejecuté una consulta con el correspondiente JOIN.

sql-eagerloading

Todavía estoy sacando más campos de los que necesito y puedo arreglarlo nuevamente usando proyección, de hecho, siempre usar proyección es probablemente la ruta más segura ya que proyección de forma predeterminada usará eager loading de esa manera, no tendre que preocuparme por recordar el uso de Include. Actualizando mi código para usar proyección quedara de la siguiente forma

Ahora es más código porque estoy siendo explícito sobre lo que quiero extraer de la base de datos, sin embargo, todavía obtengo el mismo resultado y mi consulta SQL ahora está optimizada

sql-eagerloading-projection

En resumen:

  • Comprende qué tipo de carga (Lazy, Eager) usa el ORM por defecto
  • Ten en cuenta al cargar datos relacionados utilizando un ORM si la tabla podría crecer, asegúrate de que tu consulta esté optimizada
  • Verifique el SQL que se genera, no confies en el rendimiento cuando pruebas con pocos datos
  • Usa proyección siempre que puedas

Estos son algunos de los problemas más comunes que he encontrado cuando las personas comienzan a usar un ORM simplemente saber sobre ellos te ayudará a ser más consciente al usar un nuevo ORM ya que la mayoría de estos conceptos se aplica a muchos ORM. ¿Hay algún otro peligro común que haya visto al usar un ORM? Déjame saber abajo en los comentarios