Tema ya trillado en otros idiomas pero sobre el cual me apecete disertar en la lengua de Cervantes. Premisas: trabajamos en un proyecto con Entity Framework 4, que genera nuestro modelo de entidades (con relaciones entre ellas) a partir de nuestro repositorio (típicamente un SQL Server). Exponemos parte de ese modelo a través de una capa de servicios WCF utilizando como valores de retorno de los métodos del servicios las propias clases del modelo. Ningún problema puesto que WCF se encargará de serializar adecuadamente esas clases y las ofrecerá a los clientes del servicio como clases proxy.
Un buen día descubrimos que el aplicativo empieza a ofrecer un rendimiento más que discutible y nos preguntamos por qué. Acude al rescate una herramienta de pago con trial de 30 días que bien vale, por lo menos, evaluarla. Entity Framework Profiler (EFProf), de los conocidos Hibernating Rhinos, permite hacer un profiling de las SQL lanzadas contra nuestra base de datos por el engine de Entity Framework. Simplemente hay que incluir la siguiente llamada en la inicialización de nuestra aplicación (p. ej. en Application_Start de global.asax):
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
La herramienta EFProf monitorizará desde ese momento cada todo Object Context de EF y nos mostrará qué comandos SQL está utilizando para cargar datos:
Dos cosas: primero, hay un contexto que necesita 97 SQL queries?!?!? Esto es mucho comparado con el resto de contextos. Segundo, ese mismo contexto está lanzando advertencias Using a single object context in multiple threads is likely a bug, SELECT N+1 y Too many database calls per object context.
Bueno, ahí hay un problema, claramente. Pero esas alarmas es difícil que provengan de mi código. Al fin y al cabo, mi interacción con EF se limita a utilizar queries de LINQ-To-Entity Framework del estilo:
var result = myObjectContext.Reports
.OrderBy(r => r.Title);
¿Como puede generar eso tanta query SQL? Bien, pues desvelo el misterio sin más. Hay dos claves: la primera está en WCF, que juega un papel más importante del que podríamos pensar. WCF necesita devolver los objetos del modelo y para ello serializar sus propiedades y colecciones. El proceso de serialización no sabe lo que hay por debajo, simplemente accede a esas propiedades para obtener sus valores, y punto. La segunda clave es lo que hay realmente por debajo, que es la configuración de lazy loading del Object Context de EF. Por defecto en la versión 4 los contextos son creados con la propiedad LazyLoadingEnabled a true. Esto significa que al acceder a propiedades de un objeto del modelo, si esas propiedades son a su vez otros objetos relacionados, esos accesos van a generar queries de carga adicionales. WCF pide, y EF se lo da. Todo. Propiedad por propiedad. Prácticamente nos estamos trayendo la base de datos entera query a query, sin un solo join que agrupe un poquito las peticiones. Mal.
Lo primero: desactivar el lazy loading en el contexto que utiliza WCF (LazyLoadingEnabled = false). Pero ahora tenemos un problema. Esta configuración produce que la consulta de un objeto no devuelva los objetos relacionados, a menos que se lo especifiquemos manualmente.
Por ejemplo, imaginemos que nuestra entidad Reports tiene relacionados unos Users que deben estar disponibles al realizar la consulta LINQ anterior. Entonces esos users no se cargarán a menos que hagamos algo como lo siguiente:
myObjectContext.LazyLoadingEnabled = false;
var result = myObjectContext.Reports
.Include("Users")
.OrderBy(r => r.Title);
Este eager loading que estamos haciendo utilizando el método Include generará una query SQL con un join entre la tabla de Reports y la de Users, que es lo que buscábamos desde el principio. Vuelta a los clásicos. Y el Entity Framework Profiler contento, puesto que habremos reducido drásticamente la carga de queries.
Bibliografía para insomnes:
EF – Loading related objects