Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

18 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 21 de mayo de 2013
Sabemos que el uso descontrolado de la carga diferida o lazy loading puede echar abajo el rendimiento de nuestra aplicación, puesto que se generan peticiones al motor de datos al intentar recuperar entidades relacionadas cuando intentamos acceder a ellas. Sin embargo, no son pocos los casos en los que me encuentro que no se está usando apropiadamente, provocando un rendimiento terrible en el acceso a datos de las aplicaciones.


Imaginemos, por ejemplo, la siguiente estructura de datos, que podríamos describir diciendo que un usuario puede tener un número indeterminado de blogs, y en cada uno de ellos existirán un número indeterminado de posts:
Estructura de datos
E imaginemos ahora el siguiente código, que realiza un recorrido en profundidad de estos datos para mostrar los posts de cada blog de cada uno de los usuarios, generando la salida mostrada justo después:
using (var ctx = new MyDataContext())
{
    var query = ctx.Users;
    foreach (var person in query)
    {
        Console.WriteLine("User " + person.Name + ":");
        foreach (var blog in person.Blogs)
        {
            Console.WriteLine("  Blog " + blog.Name + ":");
            foreach (var post in blog.Posts)
            {
                Console.WriteLine("      Post: " + post.Title);
            }
        }
    }
}

Salida por consola

Por defecto, con lazy loading activado, la ejecución de esta pequeña porción de código habría generado seis consultas a la base de datos: 
  1. Obtener todos los usuarios. Se obtienen dos instancias de User.
  2. Obtener todos los blogs del primer usuario (jmaguilar). Se obtienen dos instancias de Blog.
  3. Obtener todos los posts del primer blog de jmaguilar. Se obtienen dos instancias de Post.
  4. Obtener todos los posts del segundo blog de jmaguilar. Se obtienen dos instancias de Post.
  5. Obtener todos los blogs del segundo usuario (johnresig). Se obtiene una instancia de Blog.
  6. Obtener todos los posts del único blog del usuario. Se obtiene una instancia de Post.
Este problema se denomina habitualmente el “efecto SELECT N+1” dado que para recorrer una secuencia de tipo maestro-detalle se realiza una consulta para obtener las entidades principales y después N adicionales para obtener las entidades relacionadas con cada una de ellas. Además, el caso que hemos visto es especialmente grave, puesto que involucra una relación uno a muchos en dos niveles, lo cual puede ser terrible para el rendimiento cuando ataquemos colecciones de datos de cierto volumen.

La solución más frecuente es adelantar la carga de las entidades relacionadas, es decir, utilizar eager loading para traerse la estructura completa en una única consulta.

En una estructura como la que hemos visto pero con dos únicos niveles, sería algo así:
using (var ctx = new MyDataContext())
{
    var query = ctx.Users.Include(u => u.Blogs);
    foreach (var person in query)
    {
       // Code omitted
    }
}
La cláusula Include() indicará a Entity Framework que cuando realice la consulta a la colección de usuarios debe traerse también la colección de blogs relacionados con cada uno de ellos. Si ejecutamos el ejemplo anterior, podremos comprobar que el número de consultas se ha reducido a cuatro:
  1. Obtener todos los usuarios junto con sus blogs.
  2. Obtener todos los posts del primer blog del usuario.
  3. Obtener todos los posts del segundo blog del usuario jmaguilar.
  4. Obtener todos los posts del blog del usuario johnresig.
Es obvio que la mejora definitiva vendrá haciendo que en la primera consulta se obtengan también las instancias de Post asociados con los blogs. Sin embargo, la forma de hacerlo no es tan obvia ni intuitiva como la anterior. No podemos especificarlo en el Include(), puesto que no es posible referenciar la colección como u.Blogs.Posts, dado que Posts no es una propiedad de la colección Blogs del usuario y fallaría en compilación.

La fórmula a emplear en este caso es la siguiente:
using (var ctx = new MyDataContext())
{
    var query = ctx.Users.Include(u => u.Blogs.Select(b => b.Posts));
    foreach (var person in query)
    {
        // Code omitted
    }
}
Y de esta forma tan simple estaremos obteniendo la estructura de datos completa realizando una única consulta a la base de datos, con el beneficio de rendimiento que esto conlleva.

Publicado en: Variable not found.

6 Comentarios:

Luis Ruiz Pavón dijo...

Hola José María,

La verdad es que ambas cosas tienen pros y cons, por ejemplo:

Lazy Loading:

Pros:

-Bajo consumo de memoria
-Queries más rápidas

Cons:

-Mutiple roundtrips

Eager Loading:

Pros:

-Con un roundtrip podemos traernos todos los datos

Cons:

-Mayor consumo de memoria
-Queries más complicadas (Multiples joins)

Yo creo que lo bueno es detectar cuando es buena una cosa y cuando otra, pero como siempre en este mundo no hay balas de plata.

Buen artículo maestro.

josé M. Aguilar dijo...

Hola, amigo!

Efectivamente, no hay balas de plata. Lo importante es saber de antemano qué uso se va a dar a la información de una consulta y optar por el enfoque más apropiado.

El problema es a veces el no saber que existen distintas opciones, y usar siempre la misma (ya sea eager o lazy).

Gracias por tu aportación!

Un abrazo

Reynier dijo...

Hola Jose Maria,

Como siempre dando en el clavo, hay que tener muy en cuenta esto, porque hace que la carga de unos datos puedan bajar de 100 segundos a uno, hace relativamente poco tiempo encontre un helper en System.Data.Entity.DbExtensions Include que permite anidar includesy de esta manera podia hacer consultas mas complejas para cargar datos de una vez evitandome el lazy load, claro todo a su medida y cuando sea necesario.

Saludos y gracias por mantenerte activo

Jaume dijo...

Hola,
Pero Include es un método cuyo parámetro de entrada es un string.

Estoy utilizando EF 5.

Gracias

Jaume dijo...

Es que hay que hacer un using de:

using System.Data.Entity;

En caso contrario solo aparece el método Include con parámetro de entrada string

josé M. Aguilar dijo...

Eso es :)

Un saludo y gracias por comentarlo.