domingo, 23 de junio de 2013

LazyLoading en Code First

Aunque normalmente preferimos desactivar la carga diferida en los proyectos de Entity Framework, hemos decidido activarla en el proyecto actual en que el estamos trabajando.

Uno de los principales motivos por el que desactivamos la carga diferida es que exige conocer exactamente cómo se comportará Entity Framework, o sino dará lugar a escenarios descontrolados donde el acceso a propiedades de navegación de las entidades puede derivar en un exceso de consultas a la base de datos, sin que el programador sea plenamente consciente de ello.

En cualquier caso, la carga diferida no es mala por sí misma, simplemente un mal uso puede derivar en un pésimo rendimiento pero, por el contrario, un buen uso puede ayudarnos enormemente a la hora de realizar nuestros programas.

Lo primero es saber cómo activar o desactivar la carga diferida. Para que funcione la carga diferida, Entity Framework tiene que poder crear proxys dinámicos en tiempo de ejecución que heredarán de nuestras clases POCO y sobrescribirán las propiedades de navegación (de referencia y de colección) para cargar automáticamente los datos relacionados en el primer acceso a la propiedad. Para ello, una clase de entidad tiene que cumplir los siguientes requerimientos:

  • Ser pública y no sellada.
  • Alguna propiedad de navegación tienen que ser marcada como virtual.
    • Aquí he dicho “alguna” con todo el conocimiento de causa, es decir, si al menos una propiedad de navegación es virtual se creará el proxy dinámico, sino no. Esto también significa que podemos tener en la misma entidad propiedades de navegación que funcionarán con la carga diferida y otras no.

Además también es necesario establecer a true las propiedades LazyLoadingEnabled y ProxyCreationEnabled de la configuración del contexto (que por defecto ya están activas).

Como lo más sencillo para todos es ver código, aquí va el necesario para crear una base de datos de Equipos y Jugadores con algunos datos iniciales y sobre la que trabajaremos para todos los ejemplos:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Linq;
 
namespace LazyLoading
{
    class Program
    {
        static void Main(string[] args)
        {
            const string nameOrConnectionString =
                "Data Source=(local);Initial Catalog=Prueba;Integrated Security=SSPI;MultipleActiveResultSets=True";
            Database.SetInitializer(new CreateDatabaseIfNotExistsWithSeedData());
            using (var context = new PruebaContext(nameOrConnectionString))
            {
                context.Database.Initialize(false);
                Console.WriteLine("Pulse una tecla para continuar...");
                Console.ReadKey();
            }
        }
    }
 
    class CreateDatabaseIfNotExistsWithSeedData : CreateDatabaseIfNotExists<PruebaContext>
    {
        protected override void Seed(PruebaContext context)
        {
            var realMadrid = new Equipo();
            realMadrid.Nombre = "Real Madrid";
            realMadrid.Jugadores = new HashSet<Jugador>();
            realMadrid.Jugadores.Add(new Jugador() { Nombre = "Cristiano" });
            realMadrid.Jugadores.Add(new Jugador() { Nombre = "Iker" });
            context.Equipos.Add(realMadrid);
 
            var barcelona = new Equipo();
            barcelona.Nombre = "Barcelona";
            barcelona.Jugadores = new HashSet<Jugador>();
            barcelona.Jugadores.Add(new Jugador() { Nombre = "Messi" });
            context.Equipos.Add(barcelona);
 
            var atleticoMadrid = new Equipo() { Nombre = "Atlético de Madrid" };
            context.Equipos.Add(atleticoMadrid);
        }
    }
 
    class PruebaContext : DbContext
    {
        public DbSet<Equipo> Equipos { get; set; }
        public DbSet<Jugador> Jugadores { get; set; }
 
        public PruebaContext(string nameOrConnectionString)
            : base(nameOrConnectionString)
        {
        }
    }
 
    [Table("Jugadores")]
    public class Jugador
    {
        public int JugadorId { get; set; }
        public string Nombre { get; set; }
        public int EquipoId { get; set; }
        public virtual Equipo Equipo { get; set; }
    }
 
    [Table("Equipos")]
    public class Equipo
    {
        public int EquipoId { get; set; }
        public string Nombre { get; set; }
        public virtual ICollection<Jugador> Jugadores { get; set; }
    }
}

Algo importante que hay que conocer es cuando los proxies serán creados y cuando no. Entity Framework sólo creará proxys dinámicos para los objetos materializados en el contexto a través de una consulta y para objetos creados manualmente a través del método DbSet.Create.

Una vez ya tenemos la base de datos creada y con datos y hemos cumplido los requerimientos para la creación de proxies, veamos como sucede la magia de LazyLoading y algunos tips al respecto:

   1: var realMadrid = context.Equipos.Single(p => p.Nombre.Equals("Real Madrid"));
   2: foreach (var jugador in realMadrid.Jugadores)
   3: {
   4:     Console.WriteLine(jugador.Nombre);
   5: }

En este código, primero recuperamos un equipo y después iteramos sobre los jugadores del mismo. Lo que permite LazyLoading es precisamente iterar sobre los jugadores del equipo sin tener que preocuparnos de si están o no cargados en memoria. Es decir, en el primer acceso a la propiedad de navegación Jugadores, EF cargará automáticamente los datos desde la base de datos. Esto significa que se habrán ejecutado 2 SELECT, la primera estaba clara y sucedió al recuperar el equipo, sin embargo la segunda fue transparente para nosotros y es justamente esto la bondad y la maldad de LazyLoading ¿Sabías que esto iba a suceder? Entonces no hay problema, lo tenías previsto ¿No sabías que esto iba a suceder? Pues entonces hay un acceso a base de datos en tu código que podría arruinar el rendimiento de tu aplicación, depende de en que escenarios.

Asumiendo que todos conocemos ya cómo funciona LazyLoading y sabremos reconocer escenarios malignos SELECT N+1, quedan algunos trucos que podemos utilizar para paliar esta situación si es necesario:

   1: var realMadrid = context.Equipos.Single(p => p.Nombre.Equals("Real Madrid"));
   2: var entry = context.Entry(realMadrid);
   3: if (!entry.Collection(p => p.Jugadores).IsLoaded)
   4: {
   5:     entry.Collection(p => p.Jugadores).Load();
   6: }
   7: foreach (var jugador in realMadrid.Jugadores)
   8: {
   9:     Console.WriteLine(jugador.Nombre);
  10: }

En este código, en la línea 3 investigamos si está o no cargada la colección de Jugadores. En caso de no estarlo, la cargamos de forma explícita en la línea 5. Ahora volvemos a tener controlado el escenario.

Lógicamente también sería válido utilizar carga temprana (eager loading) y así recuperar todos los datos con una sola consulta en base de datos. Aunque la carga temprana podría parecer la solución definitiva, más te valdría echar un vistazo al profiler de SQL y confirmar que la sentencia ejecutada no es un SELECT con chorrocientos JOIN que más que una consulta, podría ser una aberración que matara igualmente tu aplicación.

   1: var realMadrid = context.Equipos.
   2:     Include(p=>p.Jugadores).
   3:     Single(p => p.Nombre.Equals("Real Madrid"));
   4: foreach (var jugador in realMadrid.Jugadores)
   5: {
   6:     Console.WriteLine(jugador.Nombre);
   7: }

Otra situación que podría darse es que no siempre podemos tener la certeza de que las propiedades de navegación serán referencias válidas. Veamos el siguiente ejemplo:

   1: var equipo = context.Equipos.Create();
   2: equipo.Nombre = "Sevilla";
   3: context.Equipos.Add(equipo);
   4: var jugador = context.Jugadores.Create();
   5: jugador.Nombre = "Negredo";
   6: equipo.Jugadores.Add(jugador);
   7: context.SaveChanges();

En la línea 6 asumimos que Jugadores es una referencia válida, pero sin embargo al ejecutar el código tenemos la siguiente excepción:

clip_image001[4]

Esto ocurre porque la entidad equipo aún no está grabado en la base de datos.Para solucionar esto tenemos 2 posibilidades:

  • Grabar el contexto antes de la línea 6
  • Igualmente antes de la línea 6 crear manualmente la colección de Jugadores.

Yo personalmente me inclino siempre por la segunda solución que implica un menor número de accesos a la base de datos:

equipo.Jugadores = new HashSet<Jugador>();
var jugador = context.Jugadores.Create();

Estoy seguro que utilizar LazyLoading será un problema para algunos e incluso lo demonizarán, pero yo creo que al fin y al cabo es una característica más de Entity Framework, que depende de en qué escenarios podría resultarnos de mucha ayuda. La decisión queda en tus manos.

Un saludo!

No hay comentarios:

Publicar un comentario