ADO.NET Entity Framework: Buenas prácticas para contextos de tiempo de vida cortos

En este post comento una serie  buenas prácticas para mejorar la eficiencia de ADO.NET Entity Framework (EF) en escenarios de aplicaciones empresariales dónde es recomendable utilizar contextos de tiempo de vida cortos (Short Lived Contexts)


AVISO: Lo aquí expuesto es aplicable a ADO.NET Entity Framework Beta 3. Es posible que en versiones posteriores esta información no sea correcta.


Introducción


Los principales patrones arquitectónicos para aplicaciones empresariales emplean una fachada que expone al UI o UIC la lógica de negocio de la misma. Esta fachada idealmente no debe mantener el estado (stateless) para que, entre otras razones, pueda utilizarse desde aplicaciones web como ASP.NET, servicios web,  WCF o clientes inteligentes ligeros en forma de aplicación de escritorio tradicional.


Si empleamos EF en nuestra capa de negocio tendremos métodos  que crean un ObjectContext local al inicio de su ejecución y liberan al final del método tras realizar una serie de operaciones de lógica de negocio mayormente breves. Se dice que estos ObjectContext son Short Lived  o de tiempo de vida corto.


Por el contrario, los contextos de tiempo de vida largo (long lived contexts), suelen estar asociados a aplicaciones de escritorio o consola tradicionales y se crean y destruyen al inicio y fin de la aplicación. Si bien también se aplica cada vez más el patrón arquitectónico anteriormente descrito en estas aplicaciones empresariales de escritorio. Así es posible distribuir la lógica de negocio y que la aplicación empresarial  con interface de escritorio (WPF, WindowsForms, Consola…) se comporte como un cliente inteligente ligero. Además podríamos aprovechar dicha lógica de negocio para ser usada con aplicaciones con interfaz web o desde servicios web o incluso WCF.


Los ORMs (Object Relational Mapping) como Entity Framework o NHibernate se emplean principalmente en estos escenarios empresariales y es curioso que la mayoría de la literatura y documentación al respecto inciden en presentar sus características en entornos con contextos de tiempo de vida largo y apenas se encuentran referencia a los Short Lived Context cuando son prácticamente el escenario arquitectónico empresarial más común.


Una de las ventajas de los ORMs es que nos dan una indirección estupenda del almacén final de datos y su consulta específica. Pero también nos añade el desconocimiento de cómo trabaja internamente y si el código que escribimos es o no eficiente. Para crear aplicaciones empresariales eficientes, algo imprescindible en entornos empresariales con alto número de peticiones, es preciso conocer cómo trabaja internamente el ORM que empleamos y el Entity Framework no es una excepción. El principal desafío es que pocos tenemos el tiempo suficiente para investigar su funcionamiento y deducir buenas prácticas al respecto.


Gracias a dos artículos publicados en ADO.NET TEAM Blog sobre rendimiento del Entity Framework [1] [2]y la fantástica FAQ de Entity Framework [3] recopilada por Danny Simmons en su blog Sys.Data.Objects dev guy se pueden extraer algunas buenas prácticas para mejorar el rendimiento en Entity Framework, especialmente para aquellos entornos con contextos de tiempo de vida cortos.


Buenas prácticas para mejorar la eficiencia en escenarios con Short Lived Contexts


Vamos a centrarnos en los siguientes aspectos:


·         Acelerar la creación de ObjectContexts


·         Afinar el ObjectStateManager


·         Optimizar la ejecución de consultas en LINQ to Entities


Acelerando la creación de ObjectContext


Uno de los principales problemas a lo que nos enfrentamos es que crear un ObjectContext es una actividad costosa, especialmente la primera vez. En una aplicación con short lived context se suele crear y desechar un ObjectContext prácticamente por petición a la lógica de negocio. En una sesión o en un petición desde el interface es fácil tener una o más invocaciones a la fachada de la lógica de negocio que utilizan varios ObjectContexts. Si multiplicamos estas instanciaciones por peticiones concurrentes, el número de ObjectContext creado se dispara. El EF proporciona alguna optimización al respecto [1]; la primera vez que se crea un ObjectContext se cachea la información de metadata del modelo que se construye a partir de los archivos CSDL, MSL y SSDL. Esta información se cachea y se comparte a nivel de dominio de aplicación (38% del tiempo). Otra gran parte del tiempo de esta primera instanciación se emplea en la creación de la vista abstracta de la base de datos en memoria (56% del tiempo), que también se cachea.  Solo el 7% del tiempo se emplea en la materialización (por ejemplo un DBReader obteniendo los datos físicos)


En la práctica tenemos que una primera ejecución de un método con una consulta a través de EF es del orden de 300 veces más lenta que las consultas sucesivas. Las consultas en caliente son relativamente rápidas y el 74% del tiempo se emplea en la materialización de la consulta y solo el 4,13% en la creación del ObjectContext.


Antipatrones


Si a alguien se le ocurre compartir un único ObjectContext por dominio de aplicación debe saber que no es una buena idea porque el EF no es thread Safe, como la gran mayoría de clases del framework, si bien podría implementarse un mecanismo de bloqueo propio. Ver [3]


Si estamos en una aplicación ASP.NET puede parecernos tentador guardar en la sesión el ObjectContext pero tampoco es una buena idea ya que el tamaño de la sesión crecería enormemente y su correcta serialización tampoco es trivial. Ver [3]


Buena práctica


La principal aproximación para rebajar el tiempo de creación del contexto es en su primera instanciación. Se puede reducir el tiempo de generación de la vista (un 58% del tiempo de creación) teniéndola compilada previamente.


Para ello deberemos ejecutar edmgen.exe con la opción /mode:ViewGeneration, esto crea un fichero de código que puede ser incluido en nuestro proyecto.


En las pruebas realizadas en [1] se obtuvo un descenso del 28% de tiempo en la primera instanciación. La desventaja  es que si hacemos cambios en los archivos de metada del EDM necesitaremos recompilar la aplicación. Pero si eso podemos asumirlo, deberíamos aplicar siempre esta optimización.


Afinando el ObjectStateManager


Cada ObjectContext tiene un ObjectStateManager que, entre otras tareas, mantiene el estado correcto de los objetos cacheados en memoria. Por defecto si se recuperan nuevas entidades del almacén de datos a través de una consulta, ESQL o LINQ  to Entities, EF solo cargará aquellas que no están aun en memoria, llevando un seguimiento efectivo de las entidades en memoria con respecto a su persistencia física en el almacén de datos.


Llevar este seguimiento o tracking es útil en aquellos contextos dónde realicemos varias consultas que impliquen un mismo conjunto de entidades.


Pero lo más normal es disponer de abundantes métodos en nuestra fachada que simplemente recuperan una enumeración de entidades para ser enlazadas a su vez a un control visual como un GridView o un DropDownList.


Buena práctica


Es posible modificar esta política de seguimiento del ObjectStateManager para un ObjectQuery concreto, desactivándola en casos como el anteriormente descrito, obteniendo una ligera mejora en los tiempos.  Un ObjectQuery tiene un atributo llamado MergeOption que es un enum con los valores: AppendOnly, NoTracking, OverwriteChanges, PreserveChanges. Con la opción MergeOption.NoTracking no se hacen comprobaciones de seguimiento.


Por ejemplo:

using (MyTravelPostEntities entities = new MyTravelPostEntities())
{
    entities.BlogPosts.MergeOption = MergeOption.NoTracking;
    IQueryable<BlogPost> postQuery = (from bp in entities.BlogPosts
                         where bp.Comments.Count() > 0
                         select bp);
    BlogPost post = postQuery.First();

De todas formas, según los tiempos obtenidos en los artículos referenciados, la comprobación de seguimiento por parte del ObjectStateManager  solo abarca de un 1% a un 3% del tiempo total de la consulta.


Optimizando las consultas en LINQ  to Entities


De la misma forma que se cachean los metadatos también se cachean las consultas a nivel de Entity Framework, por eso las segundas ejecuciones de la misma consulta son más rápidas. Al igual que el caché de metadatos, el caché de consultas tienen un ámbito global de dominio de aplicación.


Ojo, no confundir este caché de consultas de EF con el caché de planes de ejecución de consultas SQL en SQL Server u otro gestor de base de datos relacional.


Si bien una consulta en ESQL se cachea prácticamente toda, no ocurre lo mismo con una consulta en LINQ To Entities, algunas partes tienen que construirse de nuevo en las ejecuciones sucesivas; por ejemplo el árbol de expresión resultante tiene que ser calculado o compilado en memoria en tiempo de ejecución y para cada invocación de la consulta LINQ to Entities necesita ser reconstruido.


Lo ideal sería que estas consultas LINQ solo se tuvieran que compilar una vez.  Es posible hacerlo utilizando, por ejemplo, una función estática anónima[1] que actúe de delegado. Dando lugar a consultas compiladas LINQ; que son más rápidas que las consultas LINQ To Entities tradicionales siempre que se ejecuten con cierta frecuencia.


Buena práctica


Supongamos esta petición con contexto de tiempo de vida corto (obsérvese la aplicación de la buena práctica del apartado anterior):

using (PFCNetEntities ctx = new PFCNetEntities())
{
  ctx.Projects.MergeOption = ctx.MergeOption.NoTracking;
  IQueryable<ProjectRegister> query =
      from project in ctx.Projects
      where project.User.UserName == UserLogin
      select project;  pr = query.FirstOrDefault<ProjectRegister>();
} 

Para que no sea necesario volver a construir el árbol de expresión de la consulta LINQ to Entities utilizamos una expresion lambda1 estática que se ejecute al comienzo del tiempo de vida de nuestro dominio de aplicación, obsérvese que admite el paso del parámetro UserLogin para filtrar la consulta:

 

private static Func<PFCNetEntities, String, IQueryable<ProjectRegister>>
    compiledProjectRegisterByUserQuery = CompiledQuery.Compile(
        (PFCNetEntities ctx, string UserLogin) =>
             (from project in ctx.ProjectRegister
              where project.User.UserName == UserLogin
              select project)); 

public ProjectRegister GetProjectRegisterByUser(string UserLogin)
{
     ProjectRegister pr = null;
 
    using (PFCNetEntities ctx = new PFCNetEntities())
     {
        ctx.ProjectRegister.MergeOption = MergeOption.NoTracking;
        pr = compiledProjectRegisterByUserQuery(ctx,
                  UserLogin).FirstOrDefault<ProjectRegister>();
    }
    return pr;
}
 

En las pruebas realizadas en [2] se observa que para la primera ejecución de una consulta LINQ frente a una consulta LINQ Compilada se obtiene paradójicamente que la consulta LINQ es un 23% más rápida. Si bien en sucesivas ejecuciones (suponemos siempre que en nuestro escenario empresarial las consultas son invocadas frecuentemente) se obtiene que las consultas LINQ compiladas, que por ejemplo tienen un filtro, son del orden de un 80% más rápidas. Este dato es importante porque es una mejora significativa en los tiempos de ejecución. Siendo muy recomendable utilizar LINQ to Entities compiladas en aquellas consultas que sabemos a priori que van a tener una alta tasa de invocaciones.


Resumen


Los principales patrones arquitectónicos para aplicaciones empresariales implican el uso de contextos de tiempo de vida corto, especialmente en aquellas aplicaciones ASP.NET, servicios web o WCF. Se han presentado tres buenas prácticas para mejorar la eficiencia en este tipo de escenarios.


La primera reduce el tiempo de creación del primer ObjectContext en un dominio de aplicación.


La segunda desactiva la comprobación de seguimiento para el ObjectStateManager en aquellas operaciones de mera recuperación de datos.


La tercera y última buena práctica define un patrón para utilizar consultas LINQ To Entities compiladas.


Por último, este post está totalmente abierto y con vuestras aportaciones y lo que vaya surgiendo de los blogs de ADO.NET y System.Data.Object Dev Guy lo iré ampliando hasta tener un catálogo de buenas prácticas que mejoren la eficiencia de EF en escenarios comunes como los de contextos con tiempo de vida cortos.


Referencias

 











[1]

Exploring the Performance of the ADO.NET Entity Framework – Part 1
http://blogs.msdn.com/adonet/archive/2008/02/04/exploring-the-performance-of-the-ado-net-entity-framework-part-1.aspx
(04/02/2008) 

[2]

Exploring the Performance of the ADO.NET Entity Framework – Part 2 http://blogs.msdn.com/adonet/archive/2008/02/11/exploring-the-performance-of-the-ado-net-entity-framework-part-2.aspx
(11/02/2008)
 

[3]

EF FAQ updated – v0.3
http://blogs.msdn.com/dsimmons/archive/2008/01/04/ef-faq-updated-v0-3.aspx(04/02/2008) 
 






[1] para aquellos que no les suene, una función anónima es una expresión típica del lambda cálculo introducida en C# 3.0 y que ha mejorado la forma de definir delegados. Más información.