Lazy Load. El bueno de Martin ya lo advirtio.

Published 1/5/2012 0:59 | Pedro Hurtado

Para que vayáis tomando el aperitivo de lo que viene os recomiendo primero esta lectura Lazy Load “segundo parrafo” y no esta Lazy loading. Bueno es un patrón o bien un antipatrón, para mi sencillo un antipatrón y mira que he tenido discusiones con grandes amigos al respecto, pero claro siguiendo mi línea quiero demostrar porque es un antipatrón.

Hace años y no pocos un grupo de amigos con más de una cerveza en la mano creamos un patrón para trabajar con bb.dd, aquel día no se nos ocurrió otra cosa que llamar a este “engaña bobos”. El nombre, lógicamente, vino definido por el momento.

Porque “engaña bobos”, pues sencillo el usuario nos pedía unos datos que era incapaz de ver y todos pensamos. Pues vamos a darle a sus ojos exclusivamente lo que puede ver.

  • 10 Registro por página.
  • Cinco Campos.

En definitiva este señor se quejaba de la lentitud de las consultas y después de aplicar este patrón a nuestro desarrollo, el fue feliz y nosotros no te digo nada. Jo con el Pepe que no se queja:).

De verdad  pensáis que estoy de broma, pues no, voy a demostrar el efecto del “engaña bobos” con Entity Framework 4.3.

Hace unos días y en una conversación de twitter me encontré con unos amigos a los que les dije lo siguiente Lazy Load el Antipatrón. Engaña bobos el patrón.

Lo demostramos, venga vamos a ello.

Primer paso. Un modelo sencillo, pero con bastante ocultismo, puesto que eso no representa ni la mitad de la vida real . Una factura y un cliente y hoy con DataAnotations, por cambiar de tercio:).

   1: public class ModelFacturas:DbContext
   2:    {
   3:        public ModelFacturas()
   4:        {
   5:            this.Configuration.LazyLoadingEnabled = false;
   6:        }
   7:        public DbSet<Cliente> Clientes { get; set; }
   8:        public DbSet<Factura> Facturas { get; set; }
   9:    }
  10: [Table("Clientes")]
  11: public class Cliente
  12: {
  13:    [Key]
  14:    public int Id { get; set; }
  15:    public string Nombre { get; set; }
  16: }
  17: [Table("Facturas")]
  18: public class Factura
  19: {
  20:    [Key]
  21:    public int Id { get; set; }        
  22:    public int ClienteId { get; set; }
  23:    [ForeignKey("ClienteId")]
  24:    public virtual Cliente Cliente { get; set; }
  25:    public Decimal Importe { get; set; }
  26:    public DateTime FechaFactura { get; set; }
  27:  
  28:  
  29: }

A que todos sabemos que oculta algo, por lo menos 50 propiedades más por entidad que cuesta leerlas:).

Segundo Paso. Pepe nos solicita una consulta con las facturas en la que tenemos que mostrar el importe, fecha de factura y nombre del cliente.

Tenemos muchas formas de hacerlo y algunas nos puede hacer que nos duela la cabeza, eso sí en desarrollo perfecto, ahora en producción Valium 10:).

1. Hemos leído  muchas veces que existe un método AsNoTracking() en el objeto DbQuery que hace entre otras maravillas ganar en performace.

   1: using (ModelFacturas db = new ModelFacturas())
   2: {
   3:    var resultado = from factura in db.Facturas.AsNoTracking()
   4:                    select factura;
   5:  
   6:    foreach (var item in resultado)
   7:    {
   8:        Console.WriteLine(item.Cliente.Nombre);
   9:    }
  10: }

La verdad que sí, pero en este caso no. Recupera las facturas y por cada factura recuperada lanza la siguiente sentencia.

   1: exec sp_executesql N'SELECT 
   2: [Extent1].[Id] AS [Id], 
   3: [Extent1].[Nombre] AS [Nombre]
   4: FROM [dbo].[Clientes] AS [Extent1]
   5: WHERE [Extent1].[Id] = @EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=1

En mi caso la simulación eran 100 registros, pues vamos a pensar en 100.000:(.

2. No me gusta, por lo tanto nos olvidamos de “AsNoTracking”

   1: using (ModelFacturas db = new ModelFacturas())
   2: {
   3:    var resultado = from factura in db.Facturas
   4:                    select factura;
   5:  
   6:    foreach (var item in resultado)
   7:    {
   8:        Console.WriteLine(item.Cliente.Nombre);
   9:    }
  10: }

Respuesta en la bb.dd, solo dos sentencias. una para recuperar las facturas y la otra para recuperar el cliente, pero una sola, menuda maravilla.

   1: SELECT 
   2: [Extent1].[Id] AS [Id], 
   3: [Extent1].[ClienteId] AS [ClienteId], 
   4: [Extent1].[Importe] AS [Importe], 
   5: [Extent1].[FechaFactura] AS [FechaFactura]
   6: FROM [dbo].[Facturas] AS [Extent1]
   7:  
   8:  
   9: exec sp_executesql N'SELECT 
  10: [Extent1].[Id] AS [Id], 
  11: [Extent1].[Nombre] AS [Nombre]
  12: FROM [dbo].[Clientes] AS [Extent1]
  13: WHERE [Extent1].[Id] = @EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=1

Pero claro no os he contado una cosa y es que para el ejemplo hice trampas , a todas las facturas le inserte el mismo cliente:).

   1: using (ModelFacturas db = new ModelFacturas())
   2: {
   3:     Cliente cl = new Cliente() { Nombre = "Cliente1"};
   4:  
   5:  
   6:     db.Clientes.Add(cl);
   7:  
   8:     db.SaveChanges();
   9:  
  10:     for (int i = 0; i < 100; i++)
  11:     {
  12:         db.Facturas.Add(new Factura() { ClienteId = cl.Id, FechaFactura = DateTime.Now.Date, Importe = 1 });
  13:     }
  14:  
  15:     db.SaveChanges();
  16: }

Con lo cual excepto que tu empresa tenga un solo cliente y mal tiene que funcionar , el resultado es casi igual de malo.

Tercer Paso.

Vamos a aplicar el “Engaña bobos” varias formas pero una sencilla gracias a EF.

   1: using (ModelFacturas db = new ModelFacturas())
   2: {
   3:    var resultado = from factura in db.Facturas.AsNoTracking()
   4:                    select new
   5:                    {
   6:                        FacturaId = factura.Id,
   7:                        Fecha = factura.FechaFactura,
   8:                        Importe = factura.Importe,
   9:                        Cliente = factura.Cliente.Nombre
  10:                    };
  11:  
  12:    foreach (var item in resultado)
  13:    {
  14:        Console.WriteLine(item.Cliente);
  15:    }
  16:  
  17:    
  18: }

Fijaos en detalles. Aquí si utilizo “AsNoTracking”, que pasa que tengo que definir una clase con cuatro campos, pues sí y así lo deberías de hacer o no te solicitaron eso, que pasa que da pereza, pues no. Si nos piden eso, no inventes, cual sería en problema?

Creo que ninguno siguiendo los principios No vas a necesitarlo (YAGNI). En este caso si lo necesito.

Y para acreditar el resultado solo hay que mirar esto.

   1: SELECT 
   2: [Extent1].[Id] AS [Id], 
   3: [Extent1].[FechaFactura] AS [FechaFactura], 
   4: [Extent1].[Importe] AS [Importe], 
   5: [Extent2].[Nombre] AS [Nombre]
   6: FROM  [dbo].[Facturas] AS [Extent1]
   7: INNER JOIN [dbo].[Clientes] AS [Extent2] ON [Extent1].[ClienteId] = [Extent2].[Id]

Señores, eficiente.

Conclusiones.

Que ocurre que nos da miedo hacer join o que no sabemos, por que la moda es no enseñarlo. En este caso es más eficiente un join que cualquier otra cosa, lo puedes hacer con “Incluce” con “join” o como os he mostrado, simple.Pero utilízalo.

Un detalle que he dejado pasar por alto y quizá alguno se ha dado cuenta,  en mi primer ejemplo de código escribí esto.

this.Configuration.LazyLoadingEnabled = false;

Efectivamente no utilices el Antipatrón o si lo haces conociendo lo que puede pasar, que ahorra poco, frente a una lectura explicita y cuando la necesito.

Comparte este post:

Comentarios

# El Blog de Pedro Hurtado said on May 4, 2012 1:29 AM:

En mi anterior post y con bastante ánimo de critica intente explicar mi desacuerdo con ciertas

# El Blog de Pedro Hurtado said on May 8, 2012 10:01 PM:

Hola, os acordáis de Pepe, si hombre el usuario que nos ayudo a crear el patrón “Engañabobos”. 

# Crowley said on April 11, 2013 12:23 PM:

Siento ser un poco necromancer por resucitar algo tan viejo pero al encontrar esto buscando en el google y leerlo no he podido evitar comentar.

Parece que has descubierto así por encima lo que se llama de forma pedante Command Query Responsibility Segregation (CQRS) que en su forma mas sencilla consiste en no utilizar el modelo de dominio (teniendo que  mapearlo a DTO o a modelos de la vista) para las consultas que van a la vista, si no, realizar consultas desnormalizadas directamente (1ª forma normal) que es lo que mostramos y luego utilizar el modelo de dominio (con sus reglas de negocio) para las escrituras.

# Pedro Hurtado said on April 11, 2013 12:37 PM:

Crowley gracias por el comentario:)

De todas formas soy consciente de no haber descubierto nada, simplemente es una aclaración a ciertas cosas que se producen diariamente en todos los desarrollos y como veo gente que no tiene en cuenta lo que tu comentas, pues se me ocurrió aclararlo. Pero no me digas que no es graciso el patrón engañabobos. Anda una risa te habrás dado:)