No siempre hace falta un Stored Procedure

Los ORM´s suelen facilitarnos mucho el trabajo de interactuar con las bases de datos, ya que nos abstraen de los modelos relacionales al permitirnos trabajar directamente con un modelo orientado a objetos.

stored

Permiten además, realizar cambios en los modelos de entidades con un esfuerzo menor. Pero esa abstracción no es gratis. A veces, por olvidar que hay una base de datos detrás, se pueden cometer errores que afecten negativamente la performance de la aplicación.

Alguna vez te debe haber pasado que diseñaste un modelo de objetos impecable y puro, usando un ORM para no contaminarlo con lenguajes profanos como SQL. En resumen, una obra maestra. Se pone en producción y, al tiempo, comienzan a haber quejas por su lentitud y la base de datos se sobrecarga. En ese momento es cuando aparece el DBA y te dice: “¿Están usando un ORM? No…. eso hay que pasarlo a Stored Procedure para que ande rápido”.

Si bien en algunos casos puede ser necesario, la mayoría de las veces el sólo hecho de cambiar la forma en la que se escriben las consultas en el ORM, alcanza para mejorar significativamente la performance sin tener que realizar cambios de alto impacto en la aplicación y en la arquitectura de la solución.

A continuación, comparto una serie de tips que te permitirán mejorar la escritura de consultas en ORMs:

Select new… y lazy loading

Armar la consulta para generar un objeto, únicamente con las propiedades de cada entidad que vamos a utilizar. No tiene sentido levantar 20 columnas de una tabla si solo necesitamos 3 de ellas para la pieza de código que se está ejecutando.

Con esta técnica, además de reducir la cantidad de datos que leemos de la base de datos, evitamos generar una gran cantidad de desencadenadas por el lazy loading (conocido como el problema de select n+1).

Supongamos que estamos obteniendo un listado de productos de la base representados por la entidad “Product”. En un modelo relacional tendría dos tablas: Product y Category.

kkk

Una opción para obtener los datos y convertirlos a un view model para mostrar en una vista, podría ser esta:

mmm

Pero, ¿qué desventajas tiene esto?

-      Se obtienen propiedades de la entidad Product que luego no se van a utilizar

-     Para acceder a la propiedad Description de la categoría del producto, el mecanismo de lazy loading disparará una consulta por cada producto obtenido de la base.Trabajar con un volumen grande de datos puede ocasionar serios problemas de performance.

Una alternativa es hacerlo de esta manera:

3

En este caso, el ORM tiene la inteligencia para detectar que solo necesitamos 4 campos. Además, como estamos obteniendo un campo que está en otra tabla, hará el join correspondiente. En resumen, generará un único select que hará el join entre Product y ProductCategory, levantando solo las 4 propiedades que necesitamos.

Count vs any

Cuando se quiere conocer la existencia de una entidad o un registro bajo ciertos criterios, siempre conviene usar “Any” en lugar de un “Count”. Any será traducido a un Exists o a un Select TOP, dependiendo del ORM que estemos usando, mientras que el Count será traducido a la función de agregación de SQL count que tiene más costo de ejecución.

Los ToList

Siempre hay que tener cuidado al utilizar el ToList. Una consulta mediante LINQ no se ejecuta contra la base de datos hasta que no se enumeran los resultados. Esta enumeración puede ocurrir al hacer un foreach sobre los resultados, o al hacer un ToList. Si no se levantaran todos los registros de la base de datos antes de aplicar los filtros,  se deben aplicar los filtros sobre la tabla antes de hacer el ToList.

Por ejemplo, si hacemos algo así:

4

La primera sentencia va a hacer que el ORM genere un select sin where a la base de datos y una lista de objetos con todos los registros que haya en la tabla. La segunda sentencia va a filtrar los productos en la colección en memoria.

Con un sutil cambio, obtenemos el comportamiento deseado:

5

Al no usar to ToList en la primera sentencia, la consulta no se ejecuta. El where de la segunda sentencia, se agrega a la expresión de la consulta a construir y recién, en el ToList al final de la segunda línea, se ejecuta la consulta pero con el where correspondiente para obtener de la base solo los registros deseados.

Mapeos de relaciones bidireccionales

Muchas veces, cuando se mapean objetos relacionados, la relación se arma de forma bidireccional. Por ejemplo, si tenemos el objeto Venta con sus Ítems, se podría armar así:

6

7

En principio, esta relación no generaría problemas dado que los Ítems en una Venta deberían ser cantidades razonables. Ahora, si a la entidad Producto le agregamos la relación bidireccional con ItemDeVenta, en cuanto la base de datos comience a crecer en cantidad de datos, vamos a tener un problema de performance (y uno muy difícil de encontrar).

8

En el ejemplo anterior, los ItemDeVenta que referencian a un determinado Producto[AB1] , van a tender a crecer con el tiempo a medida que se procesan las transacciones del negocio. Y si se invoca a Producto.ItemsDeVenta se van a terminar levantando de la base todos los ItemsDeVenta donde el Producto esté incluido. Así mismo, si uno le asigna un Producto a un ItemDeVenta, el mismo ORM puede invocarlo internamente y, si no tomamos las precauciones debidas, no tendremos control sobre la degradación de la performance a medida que la aplicación comience a ser utilizada masivamente.

Una sentencia como la siguiente, puede desatar que el ORM levante millones de registros de ItemDeVenta:

9

Además, sin ni siquiera acceder a la propiedad se puede tener un problema. Por ejemplo, al crear un ItemDeVenta:

10

El ORM puede internamente acceder a la propiedad ItemsDeVenta de la clase Producto para mantener la consistencia de la relación bidireccional, por lo que, potencialmente estaríamos obteniendo de la base de datos, gran cantidad de registros sin siquiera acceder a una propiedad que es una colección. Acá es cuando el DBA te dice que en su profiler ve que estás haciendo una consulta que levanta un millón de registros y vos le juras que no estás haciendo ninguna consulta.

Value Types Nulleables

Es importante tener cuidado al usar Value Types. Si el tipo es nulleable en la tabla, entonces hay que mantenerlo también a nivel de objetos. Cuando se presenta esta inconsistencia entre modelo de tablas y objetos, los ORM suelen detectar que la entidad tuvo un cambio y tratan de actualizar el campo en la tabla con el valor default del Value Type.

Conclusión

Los tips mencionados son sólo algunos de los casos con los que me he encontrado (y sufrido). Más allá de estos ejemplos puntuales,  lo que siempre recomiendo es verificar qué consulta SQL se está generando a partir del ORM.

Cada ORM tiene su particularidad a la hora de traducir el código a una consulta, por ello, no se debe dejar de controlar que las consultas generadas sean performantes e investigar si hay alguna mejor forma de escribir el código LINQ para generar un mejor SQL.

En la mayoría de los casos se pueden lograr consultas eficientes y performantes sin necesidad de tener que escribir la consulta directamente en SQL o programar un Stored Procedure.

Es fundamental no desentenderse de la base de datos y adoptar la práctica de verificar como terminan traduciéndose a SQL las consultas que escribimos con LINQ, particularmente cuando son complejas. También es imprescindible conocer los detalles y limitaciones que posee el ORM que estamos utilizando y las características del motor de base de datos sobre el cual estamos persistiendo nuestros datos.

Autor:

Diego Tubello

Baufest Technical Expert 

 

Contacto

info@baufest.com