martes, 26 de mayo de 2015

Hagamos un buen uso de los ORM

¡Buenas! Quería compartir con ustedes mi experiencia técnica en el uso de los frameworks ORM. Puntualmente, les daré algunos tips que nos van a ayudar a lograr diseños más robustos y performantes.

Desde hace varios años que vengo utilizando estos frameworks en los proyectos en los que he participado, tanto como Technical Leader como Developer. He usado durante mucho tiempo dos ORMs muy conocidos en el mundo .Net: Entity Framework y NHibernate, y la verdad es que en general estoy muy conforme con sus prestaciones.

Más allá del auge de las bases de datos orientadas a objetos, en los proyectos actuales todavía se siguen usando con mayor frecuencia las clásicas bases de datos relacionales, por lo que el uso de un ORM que nos ayude a conectar nuestro modelo de objetos con el modelo de datos, aún es muy valioso, y bien usado, nos puede dar enormes ventajas.

 

Introducción a los ORM

 

clip_image002

ORM es la sigla correspondiente a Object-Relational Mapping (que en español podríamos traducirlo como mapeo objeto-relacional).

Es una técnica de programación mediante la cual se abstrae la comunicación, conversión y adaptación entre nuestro modelo de objetos y el modelo relacional a través del cual se persisten los datos de negocio.

Se implementa como una capa de abstracción individual, y es el último nexo entre nuestros objetos de negocio y el modelo de persistencia de la aplicación, permitiendo obtener y/o guardar fácilmente información en la base de datos.

Actualmente existen numerosos frameworks ORM disponibles en el mercado. Un framework ORM es una implementación particular del modelo ORM por un producto específico. Y si bien hoy en día existen varios de ellos disponibles, muchos desarrolladores aún siguen construyendo sus propias implementaciones para sus aplicaciones, tanto por cuestiones particulares del proyecto, como por desconocimiento de estas herramientas y las grandes prestaciones que pueden ofrecernos.

En el mundo .Net los productos más conocidos son ADO.Net Entity Framework, propietario de Microsoft, y NHibernate, que es open source y deriva del proyecto hibernate para Java.

 

¿Cuáles son las principales ventajas de usar un ORM?

Los frameworks ORM aparecieron en el mercado ya hace tiempo, y si bien siempre generaron polémica en torno a si su uso era una buena decisión de arquitectura o no, actualmente se siguen empleando en muchísimos desarrollos de software.

Básicamente, esto es porque más allá de algunas limitaciones y problemas que comentaremos luego, tienen importantes ventajas que aportarnos:

· Permiten almacenar y obtener información de una base de datos relacional de forma automática. Es decir, no tendremos la necesidad de escribir a mano las consultas SQL necesarias para persistir y recuperar los datos; esto lo hará el framework automáticamente utilizando su motor interno.

· Además, al abstraernos de la base de datos implementada, nos permite que, si el día de mañana tenemos que cambiarla, el impacto para la aplicación sea mínimo, ya que sólo se vería afectada la capa de repositorio y no el resto de las capas de la solución.

· Nos garantizan, a través del Identity Map, que en memoria sólo vamos a tener cargada una única instancia de cada entidad de negocio, sin duplicados. Esto es una gran ventaja, ya que más allá de mejorar la performance, al no tener que recargarla de nuevo si se pide reiteradamente, permite asegurar la robustez del modelo y evitar inconsistencias de datos.

· De lo anterior se desprende la implementación de una caché interna que nos ayudará en lo que se refiere a mejorar la performance de las consultas.

· En complemento con el patrón Unit of Work, el framework se encarga de detectar y persistir los cambios automáticamente, liberándonos de la tarea de tener que guardar los cambios realizados en los datos de forma explícita. Esto no quita que podamos hacerlo o no, pero es una facilidad más que podemos tomar o descartar de acuerdo a nuestras necesidades.

· Agilizan el desarrollo, ya que por un lado nos abstraen de la interacción con la base de datos, pero por otro lado, también nos permiten generar el modelo de datos a partir del esquema de objetos. Esto es lo que se conoce como el approach Code First.

· Por el otro lado, existen los approaches DataBase First y Model First, en los cuales el ORM permite generar un modelo de objetos a partir de un modelo de datos existente previamente.

· También a partir de los dos puntos anteriores, nos facilitan mantener sincronizados ambos modelos (el de objetos y el de datos) ya que facilitan el actualizar el modelo de datos a partir del modelo de objetos, y viceversa.

· Los frameworks ORM actuales suelen implementar mecanismos de seguridad robustos con el fin de evitar ataques del tipo SQL Injection.

· Si respetamos correctamente la separación entre capas, nos facilitan el mantenimiento y la evolución de nuestro código.

· Las aplicaciones pueden funcionar con un modelo conceptual orientado al modelo de objetos, con conceptos como herencia, miembros complejos y relaciones, entre otros.

· Proveen compatibilidad con Language Integrated Query (LINQ), lo que proporciona una validación de la sintaxis de los nombres de entidades y atributos en tiempo de compilación.

 

¿Cuáles son las principales limitaciones de los ORM?

Bien, hasta aquí hemos comentado las ventajas de los frameworks ORM, pero sería injusto si no evaluáramos también las desventajas que presenta su uso como parte del diseño de una solución:

· La desventaja más grande de un ORM es que puede tener un impacto negativo en la perfomance de la aplicación. Esto se debe en parte a que estamos introduciendo una capa más y esto tiene su overhead, y en parte a que, dependiendo de cómo armemos las consultas con LINQ, cuando cargamos datos, el ORM armará automáticamente las consultas SQL que ejecutará sobre la base de datos, y si trabajamos con muchas tablas o condiciones complejas, el query será complejo y demorará mucho más en ejecutarse.

· Basado en lo anterior, el motor interno de un ORM posee una lógica que le permite mapear una consulta en LINQ a una query SQL. Y si bien hace su mejor esfuerzo en generar queries performantes, no siempre lo logra y terminan siendo muy complejas y generando un impacto negativo en los tiempos de carga de datos de la aplicación. Si bien, con el paso de los años las queries generadas por los ORM mejoraron notablemente, si las consultas LINQ son complejas, el ORM no nos puede garantizar un buen query, tal y como lo escribiría un desarrollador por sus propios medios.

· Partiendo de los problemas de performance, podríamos decir que derivamos en problemas de escalabilidad a futuro, sea por un crecimiento de la cantidad de datos como por un mayor uso de la aplicación por parte de los usuarios.

· Añaden una complejidad adicional en nuestro modelo de solución y requiere prepararse para poder utilizarlos correctamente.

· Si ocurre un error de mapeo, es muy difícil debuggear dentro del ORM para detectar la causa real de nuestro problema.

· Al principio, posee un costo de aprendizaje para aquellas personas que nunca han trabajado con un ORM, más que nada acerca de cómo configurar correctamente el mapeo entre las entidades de objetos y las tablas de la Base de Datos.

 

Usar o no usar un ORM

Luego de evaluar ventajas y desventajas de utilizar un framework ORM seguramente les estén surgiendo algunas preguntas como: ¿realmente es válido usar un ORM? y ¿cuándo no es conveniente usar un ORM? Bueno, les cuento que ambas preguntas se responden fácilmente: depende…

Como toda herramienta o framework de desarrollo, existirán situaciones en donde no convenga su utilización. Por ejemplo, el primer punto a evaluar es qué motor de persistencia de datos estemos usando. Si vamos a usar una base de datos orientada a objetos, entonces carece de sentido usar un ORM.

Por otro lado, existen aplicaciones en las cuales un ORM no marcará la diferencia, o peor, terminará siendo un dolor de cabeza. Podemos mencionar, por ejemplo, aplicaciones críticas de planta o aquellas que manejen grandes volúmenes de datos. Recordemos los problemas de performance que pueden presentarse; o que requieran un modelo de seguridad superior al que ofrecen los ORM actuales.

Además, desde mi experiencia, si sólo lo vamos a usar como un DataMapper, entonces no se justifica su uso, ya que el overhead que introduce y los potenciales problemas de performance que puede traer, terminan haciendo que nuestra solución ofrezca una mala experiencia de usuario.

 

Tips para hacer un buen uso de los ORM

En el caso de que optemos por utilizar un framework ORM, evaluemos los siguientes puntos en pos de favorecer el diseño desde etapas tempranas y que esto nos permita lograr una buena solución sorteando las limitaciones enumeradas anteriormente, especialmente las referidas a la performance.

Consideraciones generales:

· Un buen ORM es una herramienta compleja de usar, con muchas opciones diferentes de parametrización y varias formas para hacer lo mismo, dependiendo el contexto, algunas mejores que otras.

· Estudiemos el framework, leamos material, investiguemos, consultemos con nuestros colegas sus experiencias previas y qué cosas debemos tener en cuenta.

· No nos quedemos sólo con incluirlo en nuestra solución y que salga andando como pueda, ya que podemos estar haciendo un mal uso de él y eso nos va a generar problemas a futuro.

· Este punto es más genérico, ya que aplica tanto si vamos a usar un ORM como si no: utilicemos un lote de datos de prueba lo más parecido a la realidad. Si en producción una determinada tabla va a tener millones de registros, en nuestro ambiente de desarrollo esa tabla deberá tener al menos esa misma cantidad, e idealmente el doble.

· Muchas veces sucede que no detectamos un problema de performance con el ORM no porque la consulta esté mal, sino porque estamos probando con el lote de datos equivocado.

· Cuando construyamos las bases de nuestra capa de repositorio, pensemos dónde vamos a ubicar los controles de auditoría y profiling. Son dos funcionalidades que, si bien no son requeridas formalmente por los usuarios de nuestras aplicaciones, nos ayudarán a detectar inconsistencias o problemas más adelante cuando nuestro modelo crezca y surja algún inconveniente.

Consideraciones sobre nuestro modelo de objetos:

· Si nuestras entidades de negocio tienen colecciones, tengamos la costumbre de inicializarlas siempre en sus constructores, esto nos va a evitar posibles errores en caso de que se trate de acceder a la propiedad cuando no tiene datos cargados.

· Cuando retornamos una lista de registros desde nuestro repositorio, es preferible hacerlo mediante la colección del tipo IQueryable <T> por sobre el tipo IEnumerable <T>. Y en un nivel más general, dentro de nuestros repositorios manejémonos exclusivamente con IQueryable<T>. Esto se debe a que si usamos IEnumerable<T> , se ejecuta la consulta físicamente sobre la base de datos retornando los registros de la primera condición, y si usamos IQueryable<T>,  irá concatenando las sucesivas consultas hasta que realmente se dispare la consulta en la base de datos, por ejemplo, al loopear dentro de un foreach, o hacer explícito un ToList(). Resumiendo, con IQueryable<T> tendremos mejores prestaciones de performance en nuestra aplicación.

· Utilicemos los patrones Unit of Work y Repository, ya que nos van a facilitar enormemente la gestión del estado de las entidades, la organización de los diferentes métodos de consulta a la base de datos y el manejo de transacciones para garantizar la integridad de los datos.

· Modelar las relaciones de forma explícita, por ejemplo, para representar las Foreign Keys o las entidades Composite. De esta forma podremos acceder a propiedades anidadas en nuestras consultas con una mejor respuesta en performance por parte del ORM.

· Si es posible, utilicemos Primary Keys únicas y no compuestas, esto facilita el manejo de entidades y nos permite crear abstracciones genéricas que nos ahorrarán esfuerzo de desarrollo.

· Prestemos atención en los casos en los que una propiedad de una entidad se complete llamando a una función externa, por ejemplo, ejecutando una consulta a otro sistema. Si yo voy a obtener una única entidad es una cosa, pero otra totalmente distinta es si tengo que ejecutar esa consulta sobre un listado grande de entidades. Por ende, prestemos atención, ya que el problema de performance no siempre será del ORM, sino que puede darse por una regla de negocio en un nivel superior.

· Sobre el punto anterior, evaluemos si ese dato externo es algo que siempre vamos a necesitar o si en determinados casos no, con lo cual podríamos tener dos tipos de carga a fines de evitar los problemas de performance. Seamos flexibles.

· Al margen de que el ORM implemente una caché interna, es una muy buena práctica definir  nuestra propia estrategia de caché, la que puede tener diferentes niveles y tiempos de vida de acuerdo a las necesidades particulares de cada proyecto.

Consideraciones respecto a las consultas que podemos ejecutar a través del ORM:

· Un ORM es muy bueno con las operaciones CREATE, UPDATE y DELETE del modelo CRUD. Específicamente hablando de performance, son instrucciones simples, que pueden ser codificadas utilizando el modelo LINQ, y que el ORM las mapeará correctamente y no generarán problemas de performance al respecto.

· En cuanto a las operaciones del tipo READ, recomiendo usar el modelo LINQ de los ORM para los casos simples, como por ejemplo los clásicos GetAll y GetById; o consultas en donde no tengamos una gran complejidad en la query final generada.

· Ahondando en lo anterior, ¿dónde no les recomiendo usar el modelo LINQ con un ORM?

o En consultas destinadas a cargar un listado de entidades con múltiples opciones de ordenamiento.

o En consultas que obtienen listados de registros considerando una cantidad muy grande de filtros condicionales y/o relacionados.

o En consultas en donde debamos paginar los resultados para mostrar sólo un subconjunto.

o En consultas en donde queden involucradas varias tablas de la base de datos. Cuando más tablas participen, más grande será el universo de datos que deberá manejar el ORM, más compleja será la consulta a realizar, y por ende, más costo a nivel performance.

o Definitivamente, en consultas en donde se combinen las opciones anteriores. Para mapearlo con algo funcional, es típicamente el estilo de consultas que se requieren cuando tenemos que llenar un listado en pantalla con opciones de filtros, ordenamiento y paginado. Ya vamos a ver más adelante que existen otras opciones para resolver esto.

o En consultas que posean subconsultas. Si bien en algunos casos el ORM las puede resolver de forma simple, la realidad es que en la mayoría de los casos vamos a tener problemas de performance graves. Esto se da especialmente con las subqueries dependientes.

o En consultas que se utilicen dentro de procesos Batch o Jobs, ya que, si bien suelen correr en horarios en que no afecten a la aplicación, suelen consumir grandes cantidades de información de la Base de Datos y hacen un uso muy intensivo de ella.

· Entonces ¿cómo resolveríamos las consultas que no recomendamos hacer mediante el modelo LINQ de un ORM? Como generalmente sucede, suele haber más de una opción.

o La que yo recomiendo es utilizar Stored Procedures e invocarlos a través del mismo ORM. Sí, los ORM también permiten ejecutar Stored Procedures definidos en la Base de Datos y manejar sus resultados, y esto es una muy buena noticia ya que nos va a permitir utilizar un esquema u otro dependiendo de la complejidad de la consulta a construir.

o ¿Y la otra opción? Es generar el SQL dentro de nuestra capa de repositorio y ejecutarlo en la Base de Datos a través del ORM. Sinceramente no lo recomiendo ya que es una mala práctica, más que nada porque atenta contra el concepto de Separation of Concerns y dificulta el posterior mantenimiento de nuestra solución.

o Si necesitamos traernos todos los campos de una entidad, usemos un SELECT explícito y no el SELECT * ya que es una muy mala práctica. Además, si no necesitamos todos los campos y tenemos este tipo de SELECT vamos a estar trayéndonos datos de la Base de Datos demás, afectando la performance de la aplicación.

Bien, espero que la lista de tips anteriores les sea de utilidad en sus proyectos. Y recuerden, un ORM es una herramienta más, y hay que saber usarla correctamente para tener buenos resultados.

Usemos el criterio, busquemos un balance. No todo es LINQ, pero tampoco lo condenemos en pos de los Stored Procedures. En mi experiencia, la clave está en usar el sentido común para definir la mejor forma de implementar cada una de las consultas a construir, y basarse en la amplia documentación disponible para definir un conjunto de reglas claras que ayuden a todo el equipo a ir en la misma dirección y utilizar correctamente el ORM elegido.

Autor:

Ing. Ariel Martín Bensussán

.Net Practice Manager