c# - Los resultados de selección de Linq anidados de EF Core en N+1 consultas SQL
sql-server entity-framework-core (2)
Tengo un modelo de datos donde un objeto ''Top'' tiene entre 0 y N ''Sub'' objetos. En SQL esto se logra con una clave foránea dbo.Sub.TopId
.
var query = context.Top
//.Include(t => t.Sub) Doesn''t seem to do anything
.Select(t => new {
prop1 = t.C1,
prop2 = t.Sub.Select(s => new {
prop21 = s.C3 //C3 is a column in the table ''Sub''
})
//.ToArray() results in N + 1 queries
});
var res = query.ToArray();
En Entity Framework 6 (con la carga inactiva desactivada) esta consulta Linq se convertiría en una única consulta SQL. El resultado se cargaría completamente, por lo que res[0].prop2
sería un IEnumerable<SomeAnonymousType>
que ya está lleno.
Cuando se usa EntityFrameworkCore (NuGet v1.1.0), sin embargo, la subcolección aún no está cargada y es de tipo:
System.Linq.Enumerable.WhereSelectEnumerableIterator<Microsoft.EntityFrameworkCore.Storage.ValueBuffer, <>f__AnonymousType1<string>>.
Los datos no se cargarán hasta que se itere sobre ellos, lo que dará como resultado consultas N + 1. Cuando agrego .ToArray()
a la consulta (como se muestra en los comentarios), los datos se cargan completamente en var res
, usando un analizador de SQL, sin embargo, muestra que esto ya no se logra en 1 consulta de SQL. Para cada objeto ''Top'' se ejecuta una consulta en la tabla ''Sub''.
La primera especificación de .Include(t => t.Sub)
no parece cambiar nada. El uso de tipos anónimos tampoco parece ser el problema, al reemplazar los new { ... }
bloques new { ... }
con los new MyPocoClass { ... }
no cambia nada.
Mi pregunta es: ¿hay una manera de obtener un comportamiento similar al EF6, donde todos los datos se cargan inmediatamente?
Nota : me doy cuenta de que, en este ejemplo, el problema se puede solucionar creando los objetos anónimos en la memoria después de ejecutar la consulta de la siguiente manera:
var query2 = context.Top
.Include(t => t.Sub)
.ToArray()
.Select(t => new //... select what is needed, fill anonymous types
Sin embargo, esto es solo un ejemplo, realmente necesito la creación de objetos para formar parte de la consulta Linq, ya que AutoMapper usa esto para completar los DTO en mi proyecto
Actualización: Probado con el nuevo EF Core 2.0, el problema sigue presente. (21-08-2017)
El problema se rastrea en el aspnet/EntityFrameworkCore
GitHub: Issue 4007
Actualización: Un año después, este problema se ha corregido en la versión 2.1.0-preview1-final
.(2018-03-01)
Actualización: Se ha lanzado la versión 2.1 de EF, que incluye una solución. ver mi respuesta a continuación. (2018-05-31)
El problema de GitHub # 4007 se ha marcado como closed-fixed
para el hito 2.1.0-preview1
. Y ahora, la versión preliminar 2.1 está disponible en NuGet como se explica en esta publicación de .NET Blog .
También se lanza la versión 2.1 propiamente dicha, instálela con el siguiente comando:
Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 2.1.0
Luego use .ToList()
en el .ToList()
anidado. .Select(x => ...)
para indicar que el resultado se debe buscar de inmediato. Para mi pregunta original esto se ve así:
var query = context.Top
.Select(t => new {
prop1 = t.C1,
prop2 = t.Sub.Select(s => new {
prop21 = s.C3
})
.ToList() // <-- Add this
});
var res = query.ToArray(); // Execute the Linq query
Esto hace que se ejecuten 2 consultas SQL en la base de datos (en lugar de N + 1); Primero, seleccione SELECT
la tabla ''Top'' y luego SELECT
FROM
la tabla ''Sub'' con una INNER JOIN
FROM
la tabla ''Top'', según la relación Clave-Clave-Clave [Sub].[TopId] = [Top].[Id]
. Los resultados de estas consultas se combinan en la memoria.
El resultado es exactamente lo que esperaría y muy similar a lo que EF6 habría devuelto: una matriz de tipo anónimo ''a
que tiene propiedades prop1
y prop2
donde prop2
es una Lista de tipo anónimo ''b
que tiene una propiedad prop21
. ¡Lo más importante es que todo esto está completamente cargado después de la llamada .ToArray()
!
Me enfrenté al mismo problema.
La solución que propusiste no funciona para tablas relativamente grandes. Si tiene una mirada en la consulta generada, sería una unión interna sin la condición de dónde.
var query2 = context.Top .Include (t => t.Sub) .ToArray () .Seleccione (t => new // ... seleccione lo que sea necesario, complete los tipos anónimos
Lo resolví con un rediseño de la base de datos, aunque me encantaría escuchar una mejor solución.
En mi caso, tengo dos tablas A y B. La tabla A tiene uno a muchos con B. Cuando intenté resolverlo directamente con una lista como se describe, no logré hacerlo (tiempo de ejecución para .NET). LINQ fue de 0.5 segundos, mientras que .NET Core LINQ falló después de 30 segundos de tiempo de ejecución).
Como resultado, tuve que crear una clave externa para la tabla B y comenzar desde el lado de la tabla B sin una lista interna.
context.A.Where(a => a.B.ID == 1).ToArray();
Después, simplemente puede manipular los objetos .NET resultantes.