EF Code First 6.1 – Estúdialo bien antes de usarlo

Últimamente he estado trabajando parar intentar migrar nuestros modelos de datos a EF, la razón principal es que buscamos ser más productivos en desarrollo, reducir nuestro número de procedimientos almacenados y vistas porque su coste de mantenimiento comienza a ser bastante alto. 

Después de estudiar como se comportaba el modelo tradicional de EF con Edmx con modelos de más de 600 tablas, decidí que una aproximación mas adecuada seria utilizar EF Code First, la simplicidad, los metadatos escritos con Fluent Api, la posibilidad de realizar migraciones y aparentemente un rendimiento mayor, fueron algunas de las razonas para seleccionar esta tecnología. En Code First los metadatos de las entidades y toda la información sobre sus relaciones están escritas directamente en nuestras clases C#, así que comencé a pelearme durante algunos días con las plantillas T4 para realizar algunos cambios que requerían nuestras entidades, acceder a los metatados en Code First es mucho más sencillo que hacerlo sobre un modelo tradicional basado en un fichero Edmx, de hecho en el este último hay datos como el nombre del esquema de la tabla y el nombre real de los campos que no pueden ser consultados si no es a través de funciones propias a la base de datos o la lectura directa del fichero de configuración Edmx en formato xml. En nuestro caso necesitaba generar un enumerador en cada entidad con los nombres de los campos de las tablas y el nombre real de la tabla con su esquema. 

General.Divisas

public enum Fields
{
Codigo,Descripcion,Name,Simbolo,Pais,Grafico,Observaciones
}

Algo inicialmente sencillo se complica un poco con aspectos como la pluralización y algunos cambios en los nombres de los campos que EF genera y a los que debemos prestar atención. La pluralización varia el nombre de algunas entidades y campos, si tienes una tabla llamada ‘clientes’ y un campo llamado ‘cliente’, la entidad pasa a llamarse cliente, el dbset clientes y el campo ‘cliente1’, así que hay que prestar atención si en algún momento queremos hacer cambios sin contar con el contexto de EF, lo bueno es que una vez generado, no cuesta mucho Refactorizar algunas entidades si queremos solucionar estos problemas, recordar que en EF Code First partimos de la base de que el modelo es escrito por nosotros. De momento la pluralización en Code First no se puede anular como en el modelo tradicional cuando lo generas a partir de la base de datos por primera vez, tampoco se puede generar el código de una o varias entidades previamente seleccionadas. Un pequeño truco para lograr esto es utilizar un login únicamente con permisos de acceso a las tablas que queramos generar.

La tabla anterior genera la entidad siguiente, o veis cambia el nombre del campo Test

namespace Entities.EntityFramework

{

using System;

using System.Collections.Generic;

public partial class Test

{
    public int Test1 { get; set; }
    public System.DateTime Fecha { get; set; }
    public string Zona { get; set; }
    public Nullable<System.DateTime> Fecha_alta { get; set; }
    public System.DateTime Fecha_modificacion { get; set; }
    public string Description { get; set; }
    public double Precio { get; set; }
    public decimal Cantidad { get; set; }
    public Nullable<double> Total { get; set; }
    public string Observaciones { get; set; }
    public byte[] Foto { get; set; }
}

}

Otros problemas surgen en las vistas, EF quiere que todos los registros tengan un campo clave para poder diferenciar cada uno, en vistas actualizables tiene sentido pero en vistas que solo realizan consultas no, cuando EF genera las entidades de las vistas establece la mayoría de los campos no nulos como campos claves, así que aplique la solución de añadir un rowId a cada vista y utilizar NullIf para identificar los campos que no quieras que sean claves, vamos un coñazo que no sé hasta qué punto tiene sentido, pero no quiero ver esos warnings en amarillo, la otra solución de cambiar el mapeo de las vistas a mano es también otra tarea tediosa y si vuelves a generar el modelo, lo deberás hacer de nuevo.

ISNULL(ROW_NUMBER() OVER (Order By fieldList),-1) as RowID, Nullif(Nombre,»)

Al menos casi todas mis vistas tienen un campo clave :), otras por temas de rendimiento no pude modificar, hay que analizar en detalle cada una, por los problemas de rendimiento que esto pueda ocasionar, sobre todo en aquellas que utilizan unión, recursividad y otros aspectos.

Surgieron también otros problemas con las relaciones, que aún hoy no he logrado explicarme bien y seguro que tienen algún motivo lógico en EF, pero tener que cambiar tu modelo de base de datos para que tu modelo de EF funcione bien no sé si tiene mucho sentido, por ejemplo relaciones de una tabla con solo dos campos en las que cada uno tiene relación con una tabla, provoca problemas al generar las relaciones, aunque hay alguna regla al respecto que dice que las claves primarias de una tabla no deberían relacionarse con ninguna otra.

Nuestro sistema trabaja con diferentes bases de datos, la mayor de mas de 700 entidades, la segunda con unas 70 y algunas otras con menos, así que definí un proyecto independiente para cada contexto con las mismas plantillas T4 para que las entidades que se generasen fueran iguales.

Después de resolver estas incidencias, comienzo a realizar algunos test, una simple consulta a la base de datos sobre el modelo mas grande que devuelve un solo valor y la primera sorpresa, nada más y nada menos que 65 segundos, comienzo a leer algunos artículos sobre bajo rendimiento de EF Code First en la primera query, al parecer el sistema, lo que hace es leer los metadatos de las clases de mapeo, luego genera un fichero Edmx internamente para guardar compatibilidad hacia atrás con EF, posteriormente lo compila y comienza a funcionar igual que EF tradicional, eso si esto se toma la friolera de 65 segundos para hacer todo esto y alguna cosilla mas, en mi maquina un I7 con 4 nucleos, 8 gigas de ram y un disco SSD, vamos , de asustar…

Utilizando el profiler de .net me di cuenta de que el problema estaba en el motor de EF ya que la penalización viene de una la linea similar a esta:

var metadata = ((IObjectContextAdapter) context).ObjectContext.MetadataWorkspace;

En ella EF carga todos los metadatos de las entidades, relaciones, campos compuestos, etc. así que en un principio no me queda mucho que hacer si no investigar un poco al respecto, de todos los artículos que he leído, os aseguro que no me quedan muchos, voy a destacar tres de ellos que os pueden ayudar un minimizar parte del problema. El primer blog de indispensable lectura para todos aquellos que trabajen con EF. Os aconsejo su lectura para entender el artículo al completo.

http://romiller.com/category/entity-framework/
http://www.fusonic.net/en/blog/2014/07/09/three-steps-for-fast-entityframework-6.1-first-query-performance/
http://www.dotnet-tricks.com/Tutorial/entityframework/J8bO140912-Tips-to-improve-Entity-Framework-Performance.html

De todas las ideas que se comentan aquí, la compilación de vistas proporciona algunos segundos de mejora, pero que queréis que os diga, sigue siendo una chapuza, cada vez que modificas algo en el modelo, tienes que volver a regenerar las vistas y lo cierto es que en nuestro modelo el rendimiento de unos pocos segundos más con los tiempos de los que hablamos apenas se notan.

Utilizar ngen con EntityFramework.SqlServer.dll en el GAC, también ahorra algunos segundos, pero tener que integrarlo en cada equipo cada vez que salga una versión de EF también tiene su tela. En resumen algunas mejoras pero que complican el trabajo con EF Code First.

Así que se me ocurrió la idea de analizar exactamente las necesidades básicas de mi modelo, lo cierto es que los campos compuestos y relaciones para acceder a los datos, hacen que la escritura de las consultas en EF sea más cómoda, pero podía prescindir fácilmente escribiendo las relaciones yo mismo tal y como lo hago en Sql Server, ya sé que esto rompe con el modelo EF Code First y alguno pondrá el grito en el cielo, pues cuando haga una migración estas no se generan, pero no se me ocurría otra manera que aumentar el rendimiento, así que a matar pulgas a cañonazos….

Con el replace de Visual Studio y con ayuda de las expresiones regulares, voila, me cargue todo el código relativo a relaciones y campos de referencia a otras entidades.

Y por supuesto después de eliminar tanto código, logre una mejora sustancial, el tiempo bajo a los 32 segundos desde los 65, que le voy a hacer, tengo un montón de relaciones con integridad referencial, continúe y genere las vistas compiladas y metí la librería de Entity Framework en el GAC y logre bajar a la friolera de 14 segundos, pero aquí llegue a un punto de no retorno, ya no sabía cómo podía reducir mas el tiempo, con ayuda de Resharper me puse a buscar entidades no utilizadas y me encontré unas cuantas sobre todo derivadas de vistas que ya no se utilizaban, así que reduje mi modelo en casi 40 entidades más, no hay mal que por bien no venga y lógicamente, mejoro un par de segundos más hasta los 12 segundos.

Realizo otra prueba en un equipo virtual con menos memoria sin disco duro SSD para ver cómo se comporta el sistema de nuevo el tiempo se incrementa hasta los 25 segundos, algo que no entiendo muy bien, pues los dos procesadores no pasan de un 25% en el proceso de carga, pero bueno, en este punto, casi que prefiero no saber lo que EF estará haciendo internamente, como soy muy positivo comienzo a pensar que los equipos que utilizamos son mejores, incorporan procesadores de 64 bits y 8 gigas de Ram y como el tiempo es solo en la carga inicial del sistema, decido asumirlo temporalmente hasta ver si el equipo de desarrollo de EF proporciona alguna solución, así que el miércoles por la noche lo pongo en producción.

A las 8 de la mañana ya tenía un par de mensajes de algunos usuarios para entrar en la aplicación tienen una demora de más de 2 minutos, incluso para abrir un formulario después de haber cargado los metadatos, la apertura de los contextos que apenas penaliza en mi equipo en la de ellos si lo hace, la única diferencia es que ellos utilizan Windows 7 64 bits y yo Windows 8 64, pues aunque parezca increíble sucede esto, compruebo que ambos tienen instalado .net Framework 4.5.1, lo curioso es que mientras en Windows 8 cargo el sistema en 14 segundos, la misma prueba en Windows 7 64 en un equipo sin SSD tarda 22 minutos!!!!!!, así que no me queda mas remedio que buscar alguna solución, se me ocurrió grabar los metadatos de cada contexto en una tabla de la base de datos y cargarlos desde ahí, por supuesto con un DataReader sin contextos ni nada que ponga EF, los metadatos los almaceno en un campo de formato Xml y los cargo al inicio de la aplicación, el modelo completo de más de 800 entidades de contextos diferentes son cargados en menos de dos segundos, evitando hacerlo desde EF Code First, no me queda más remedio que volver a utilizar mis entidades poco y no usar los contextos de EF Code First hasta encontrar una solución a estos problemas de rendimiento.

Cada entidad del modelo almacena la información de cada entidad en un campo XML, como ejemplo:

La parte positiva es que mis entidades, vistas de sql y otros aspectos han sido preparadas para en un futuro, poder utilizar EF Code First, (el que no se conforma es porque no quiere….),

Espero que en algún momento corrijan estos problemas, algo que empiezo a dudar, pues buscando información he encontrado incidencias sobre el rendimiento con modelos de muchas entidades desde el 2011, como ejemplo http://social.msdn.microsoft.com/Forums/en-US/d6edb32d-8479-4a0d-8dc0-caa50f181e5d/extremely-slow-performance-using-codefirst-with-entity-framework-41-release?forum=adodotnetentityframework, así que pondré una vela a San Judas Tadeo…

Sobre los metadatos de EF en el modelo Edmx, todo hay que decirlo, es una auténtica aberración, hasta Rowan Miller Product Manager de EF lo reconoce en algunas de las incidencias en codeplex, pero están trabajando para mejorar esto, pienso que Code First debería ser en parte un modelo desacoplado del actual Entity FrameWork, pues si tiene que generar un archivo edmx desde las clases de mapeo nunca aprovechara todo su potencial, hace algunos años hice algunas pruebas de concepto para cargar los metadatos de esta manera y los resultados son espectaculares, es una pena no poderle sacarle partido.

No entiendo como una tecnología de acceso a datos genere tantos problemas con modelos de muchas entidades sobre todo ahora que las necesidades de las bases de datos son cada vez mayores, quizás es que la mayor parte de la gente trabaja con modelos que no exceden las 100 tablas y no se preocupan de analizar modelos mayores, personalmente sigo pensando que la idea de EF Code First es excelente, desgraciadamente les queda bastante trabajo todavía, sobre todo para modelos este.

Tengo alguna tentación para hacer algún comentario en numerosos post que hablan sobre las bondades de EF Code First, como en http://www.itworld.com/development/405005/3-reasons-use-code-first-design-entity-framework, pero voy a contenerme.. :), pese a todo confió en que algún día solucionen estos problemas y confió en que pronto EF Code First sea una apuesta de futuro.

Lo cierto es que el equipo de Entity Framework sobre todo Rowan Miller ha contestado a todas y cada una de las dudas e incidencias que he comentado, de hecho han asignado a una persona del equipo de EF para analizar nuestro modelo y ver si podemos optimizarlo de alguna forma, el soporte técnico ha sido excelente.

Mi consejo es que si utilizas modelos pequeños de menos de 100 entidades sin muchas relaciones EF Code First puede ser una buena alternativa, pasando de aquí hay que estudiar en detalle los problemas de rendimiento que se puedan producir. Espero que en la versión 7 se corrijan algunos de estos.