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.