27 de diciembre de 2012

Problemas de performance con relaciones lazy en Entity Framework

La versión en inglés de este post puede ser encontrada en http://blogs.southworks.net/jrowies/2012/12/27/entity-framework-performance-issues-with-lazy-relationships/

Durante las últimas semanas estuve trabajando en una aplicación que usa Entity Framework (code-first) y tuve algunos problemas de performance que pensé que podría ser útil compartirlos.

Luego de implementar algunos features en la aplicación noté ciertos problemas de performance, así que decidí agregar MiniProfiler y ver qué estaba pasando bajo el capó. La causa de los problemas de performance era que EF estaba ejecutando muchas queries innecesarias (o al menos eso pensaba) contra la BD. Así que, luego de investigar un poco y hacer algunas preguntas, descubrí que EF tiene una forma muy particular de manejar relaciones lazy "muchos a uno".

Déjenme explicar el problema con un ejemplo, supongamos que tenemos el siguiente modelo de dominio:

public class Order
{
    public Guid Id { get; set; }
 
    public string Description { get; set; }
 
    public virtual Customer Customer { get; set; }
}
 
public class Customer
{
    public Guid Id { get; set; }
 
    public string Name { get; set; }
}

Mapeado a las siguientes tablas:

 

En este modelo, si necesito acceder al ID del customer desde una order, es necesario pasar a través de la propiedad Customer, por ejemplo order.Customer.Id

El problema con esto es que EF va a cargar el objeto Customer desde la BD en cuanto tratemos de acceder a cualquier propiedad del objeto Customer. Ya que todo lo que necesitamos es el ID del customer, uno podría asumir que EF podría obtenerlo desde la FK Customer_Id en la tabla Orders sin necesidad de ejecutar queries extra contra la BD, pero desafortunadamente este no es el caso ...

Código como este:

foreach (var order in context.Orders)
{
    Console.WriteLine("Order {0}, Customer {1}", order.Description, order.Customer.Id);
}

Terminará ejecutando una gran cantidad de queries contra la BD, sólo para recuperar el ID del customer:

SELECT 
1 AS [C1], 
[Extent1].[Id] AS [Id], 
[Extent1].[Description] AS [Description], 
[Extent1].[Customer_Id] AS [Customer_Id]
FROM [dbo].[Orders] AS [Extent1]

exec sp_executesql N'SELECT 
[Extent2].[Id] AS [Id], 
[Extent2].[Name] AS [Name]
FROM  [dbo].[Orders] AS [Extent1]
INNER JOIN [dbo].[Customers] AS [Extent2] ON [Extent1].[Customer_Id] = [Extent2].[Id]
WHERE ([Extent1].[Customer_Id] IS NOT NULL) AND 
    ([Extent1].[Id] = @EntityKeyValue1)',N'@EntityKeyValue1 uniqueidentifier',@EntityKeyValue1='FF947EF3-5A3F-4A26-BDB9-039C49F559A7'

exec sp_executesql N'SELECT 
[Extent2].[Id] AS [Id], 
[Extent2].[Name] AS [Name]
FROM  [dbo].[Orders] AS [Extent1]
INNER JOIN [dbo].[Customers] AS [Extent2] ON [Extent1].[Customer_Id] = [Extent2].[Id]
WHERE ([Extent1].[Customer_Id] IS NOT NULL) AND 
    ([Extent1].[Id] = @EntityKeyValue1)',N'@EntityKeyValue1 uniqueidentifier',@EntityKeyValue1='BDC1430B-A46B-4486-A946-2F3DAC3B69F5'

// ... and so on ...


Como en el pasado he estado trabajando con NHibernate (quien maneja este caso como es esperado - sin ejecutar queries innecesarias), para mí fue muy sorprendente encontrarme con este comportamiento.

Luego de investigar un poco encontré que la estrategia recomendada para estos casos es agregar una propiedad FK en el modelo de dominio.

public class Order
{
    public Guid Id { get; set; }
 
    public string Description { get; set; }
 
    public Guid CustomerID { get; set; }
 
    [ForeignKey("CustomerID")]
    public virtual Customer Customer { get; set; }
}

Realmente no es algo que me guste ya que el modelo de dominio se va a contaminar con propiedades que no deberían estar allí, pero mientras tanto no encuentre una forma mas elegante de solucionar este problema, creo que voy a tener que vivir con esto ;)

Aquí pueden encontrar una aplicación simple de consola que compara los diferentes comportamientos de Entity Framework y NHibernate en cuanto al manejo de relaciones lazy "muchos a uno"

No hay comentarios:

Publicar un comentario