Operadores de consulta complejos

Language Integrated Query (LINQ) contiene muchos operadores complejos, que combinan varios orígenes de datos o realizan procesamientos complejos. No todos los operadores de LINQ tienen traducciones adecuadas en el lado servidor. En ocasiones, una consulta en un formato se traduce en el servidor, pero si se escribe en otro formato, no se traduce aunque el resultado sea el mismo. En esta página se describen algunos de los operadores complejos y sus variaciones admitidas. En futuras versiones, es posible que se reconozcan más patrones y se agreguen sus correspondientes traducciones. También es importante tener en cuenta que la compatibilidad con la traducción varía entre proveedores. Es posible que una consulta determinada, que se traduzca en SqlServer, no funcione para bases de datos SQLite.

Sugerencia

Puede ver un ejemplo de este artículo en GitHub.

Join

El operador Join de LINQ permite conectar dos orígenes de datos en función del selector de claves de cada origen, lo que genera una tupla de valores cuando la clave coincide. Se traduce de forma natural a INNER JOIN en las bases de datos relacionales. Aunque Join de LINQ tiene selectores de clave externa e interna, la base de datos requiere una única condición de combinación. Por tanto, EF Core genera una condición de combinación que compara el selector de clave externa con el selector de clave interna para determinar si son iguales.

var query = from photo in context.Set<PersonPhoto>()
            join person in context.Set<Person>()
                on photo.PersonPhotoId equals person.PhotoId
            select new { person, photo };
SELECT [p].[PersonId], [p].[Name], [p].[PhotoId], [p0].[PersonPhotoId], [p0].[Caption], [p0].[Photo]
FROM [PersonPhoto] AS [p0]
INNER JOIN [Person] AS [p] ON [p0].[PersonPhotoId] = [p].[PhotoId]

Además, si los selectores de claves son tipos anónimos, EF Core genera una condición de combinación para comparar los componentes de igualdad.

var query = from photo in context.Set<PersonPhoto>()
            join person in context.Set<Person>()
                on new { Id = (int?)photo.PersonPhotoId, photo.Caption }
                equals new { Id = person.PhotoId, Caption = "SN" }
            select new { person, photo };
SELECT [p].[PersonId], [p].[Name], [p].[PhotoId], [p0].[PersonPhotoId], [p0].[Caption], [p0].[Photo]
FROM [PersonPhoto] AS [p0]
INNER JOIN [Person] AS [p] ON ([p0].[PersonPhotoId] = [p].[PhotoId] AND ([p0].[Caption] = N'SN'))

GroupJoin

El operador GroupJoin de LINQ permite conectar dos orígenes de datos de forma similar a Join, pero crea un grupo de valores internos para los elementos externos coincidentes. Al ejecutar una consulta como la del ejemplo siguiente se genera un resultado de Blogy IEnumerable<Post>. Como las bases de datos (especialmente las relacionales) no tienen una manera de representar una colección de objetos del lado cliente, en muchos casos GroupJoin no se traduce en el servidor. Requiere que se obtengan todos los datos del servidor para ejecutar GroupJoin sin un selector especial (la primera de las consultas siguientes). Pero si el selector limita los datos que se seleccionan, la captura de todos los datos del servidor puede causar problemas de rendimiento (la segunda de las consultas siguientes). Por eso EF Core no traduce GroupJoin.

var query = from b in context.Set<Blog>()
            join p in context.Set<Post>()
                on b.BlogId equals p.BlogId into grouping
            select new { b, grouping };
var query = from b in context.Set<Blog>()
            join p in context.Set<Post>()
                on b.BlogId equals p.BlogId into grouping
            select new { b, Posts = grouping.Where(p => p.Content.Contains("EF")).ToList() };

SelectMany

El operador SelectMany de LINQ permite enumerar un selector de colecciones para cada elemento externo y generar tuplas de valores de cada origen de datos. En cierto modo es una combinación, pero sin ninguna condición, por lo que todos los elementos externos se conectan con un elemento del origen de la colección. En función de cómo se relacione el selector de colecciones con el origen de datos externo, SelectMany puede traducirse en varias consultas diferentes en el lado servidor.

El selector de colecciones no hace referencia al elemento externo

Cuando el selector de colecciones no hace referencia a nada del origen externo, el resultado es un producto cartesiano de ambos orígenes de datos. Se traduce a CROSS JOIN en las bases de datos relacionales.

var query = from b in context.Set<Blog>()
            from p in context.Set<Post>()
            select new { b, p };
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
CROSS JOIN [Posts] AS [p]

El selector de colecciones hace referencia al elemento externo en una cláusula WHERE

Cuando el selector de colecciones tiene una cláusula WHERE, que hace referencia al elemento exterior, EF Core lo traduce a una combinación de base de datos y usa el predicado como condición de combinación. Normalmente, este caso se produce cuando se usa la navegación de colección en el elemento exterior como selector de colecciones. Si la colección está vacía para un elemento externo, no se generarán resultados para ese elemento externo. Pero si se aplica DefaultIfEmpty en el selector de colecciones, el elemento exterior se conectará con un valor predeterminado del elemento interno. Debido a esta distinción, este tipo de consultas se traduce a INNER JOIN en ausencia de DefaultIfEmpty y LEFT JOIN cuando se aplica DefaultIfEmpty.

var query = from b in context.Set<Blog>()
            from p in context.Set<Post>().Where(p => b.BlogId == p.BlogId)
            select new { b, p };

var query2 = from b in context.Set<Blog>()
             from p in context.Set<Post>().Where(p => b.BlogId == p.BlogId).DefaultIfEmpty()
             select new { b, p };
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
INNER JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]

El selector de colecciones hace referencia al elemento externo en una cláusula distinta de WHERE

Cuando el selector de colecciones hace referencia al elemento exterior, que no está en una cláusula WHERE (como en el caso anterior), no se traduce a una combinación de base de datos. Por este motivo es necesario evaluar el selector de colecciones para cada elemento exterior. Se traduce a operaciones APPLY en muchas bases de datos relacionales. Si la colección está vacía para un elemento externo, no se generarán resultados para ese elemento externo. Pero si se aplica DefaultIfEmpty en el selector de colecciones, el elemento exterior se conectará con un valor predeterminado del elemento interno. Debido a esta distinción, este tipo de consultas se traduce a CROSS APPLY en ausencia de DefaultIfEmpty y OUTER APPLY cuando se aplica DefaultIfEmpty. Algunas bases de datos como SQLite no admiten los operadores APPLY, por lo que este tipo de consulta no se puede traducir.

var query = from b in context.Set<Blog>()
            from p in context.Set<Post>().Select(p => b.Url + "=>" + p.Title)
            select new { b, p };

var query2 = from b in context.Set<Blog>()
             from p in context.Set<Post>().Select(p => b.Url + "=>" + p.Title).DefaultIfEmpty()
             select new { b, p };
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], ([b].[Url] + N'=>') + [p].[Title] AS [p]
FROM [Blogs] AS [b]
CROSS APPLY [Posts] AS [p]

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], ([b].[Url] + N'=>') + [p].[Title] AS [p]
FROM [Blogs] AS [b]
OUTER APPLY [Posts] AS [p]

GroupBy

Los operadores GroupBy de LINQ crean un resultado de tipo IGrouping<TKey, TElement>, donde TKey y TElement podrían ser cualquier tipo arbitrario. Además, IGrouping implementa IEnumerable<TElement>, lo que significa que se puede redactar sobre este elemento con cualquier operador de LINQ después de la agrupación. Como ninguna estructura de base de datos puede representar una instancia de IGrouping, en la mayoría de los casos los operadores GroupBy no tienen ninguna traducción. Cuando se aplica un operador de agregado a cada grupo, lo que devuelve un valor escalar, se puede traducir a GROUP BY de SQL en las bases de datos relacionales. GROUP BY de SQL también es restrictivo. Requiere que se agrupe solo por valores escalares. La proyección solo puede contener columnas de clave de agrupación o cualquier agregado aplicado en una columna. EF Core identifica este patrón y lo traduce al servidor, como en el ejemplo siguiente:

var query = from p in context.Set<Post>()
            group p by p.AuthorId
            into g
            select new { g.Key, Count = g.Count() };
SELECT [p].[AuthorId] AS [Key], COUNT(*) AS [Count]
FROM [Posts] AS [p]
GROUP BY [p].[AuthorId]

EF Core también traduce las consultas en las que un operador de agregado en la agrupación aparece en un operador Where o OrderBy (u otro orden) de LINQ. Usa la cláusula HAVING en SQL para la cláusula WHERE. La parte de la consulta antes de aplicar el operador GroupBy puede ser cualquier consulta compleja, siempre que se pueda traducir al servidor. Además, una vez que se aplican operadores de agregado en una consulta de agrupación para quitar agrupaciones del origen resultante, se puede redactar sobre ella como cualquier otra consulta.

var query = from p in context.Set<Post>()
            group p by p.AuthorId
            into g
            where g.Count() > 0
            orderby g.Key
            select new { g.Key, Count = g.Count() };
SELECT [p].[AuthorId] AS [Key], COUNT(*) AS [Count]
FROM [Posts] AS [p]
GROUP BY [p].[AuthorId]
HAVING COUNT(*) > 0
ORDER BY [p].[AuthorId]

Los operadores de agregado que admite EF Core son los siguientes

.NET SQL
Average(x => x.Property) AVG(Property)
Count() COUNT(*)
LongCount() COUNT(*)
Max(x => x.Property) MAX(Property)
Min(x => x.Property) MIN(Property)
Sum(x => x.Property) SUM(Property)

Se pueden admitir operadores de agregado adicionales. Compruebe los documentos del proveedor para obtener más asignaciones de funciones.

Incluso si no hay ninguna estructura de base de datos para representar un elemento de IGrouping, EF Core 7.0 y versiones posteriores pueden crear las agrupaciones después de devolverse los resultados de la base de datos. Esto es similar a la forma de funcionar el operador Include al incluir colecciones relacionadas. En la siguiente consulta LINQ se usa el operador GroupBy para agrupar los resultados por el valor de su propiedad Price.

var query = context.Books.GroupBy(s => s.Price);
SELECT [b].[Price], [b].[Id], [b].[AuthorId]
FROM [Books] AS [b]
ORDER BY [b].[Price]

En este caso, el operador GroupBy no se traduce directamente en una cláusula GROUP BY en SQL, sino que EF Core crea las agrupaciones después de devolverse los resultados del servidor en su lugar.

Left Join

Aunque Left Join no es un operador de LINQ, las bases de datos relacionales tienen el concepto de combinación izquierda que se usa con frecuencia en las consultas. Un patrón determinado en las consultas LINQ proporciona el mismo resultado que LEFT JOIN en el servidor. EF Core identifica estos patrones y genera la operación LEFT JOIN equivalente en el lado servidor. El patrón implica la creación de GroupJoin entre los dos orígenes de datos y, después, la reducción de la agrupación mediante el operador SelectMany con DefaultIfEmpty en el origen de agrupación para que coincida con NULL cuando el elemento interior no tiene un elemento relacionado. En el ejemplo siguiente se muestra el aspecto de este patrón y lo que genera.

var query = from b in context.Set<Blog>()
            join p in context.Set<Post>()
                on b.BlogId equals p.BlogId into grouping
            from p in grouping.DefaultIfEmpty()
            select new { b, p };
SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]

En el patrón anterior se crea una estructura compleja en el árbol de expresión. Por eso, EF Core requiere que se reduzcan los resultados de agrupación del operador GroupJoin en un paso inmediatamente después del operador. Aunque se use GroupJoin-DefaultIfEmpty-SelectMany, pero en otro patrón, es posible que no se identifique como una combinación izquierda.