Compartilhar via


Operadores de consulta complexa

A LINQ (Consulta Integrada à Linguagem) contém muitos operadores complexos, que combinam várias fontes de dados ou fazem processamento complexo. Nem todos os operadores LINQ têm traduções adequadas no lado do servidor. Às vezes, uma consulta em um formulário é traduzida para o servidor, mas se gravada em um formulário diferente não se traduz mesmo que o resultado seja o mesmo. Esta página descreve alguns dos operadores complexos e suas variações com suporte. Em versões futuras, podemos reconhecer mais padrões e adicionar suas traduções correspondentes. Também é importante ter em mente que o suporte à tradução varia entre provedores. Uma consulta específica, que é traduzida no SqlServer, pode não funcionar para bancos de dados SQLite.

Dica

Veja o exemplo deste artigo no GitHub.

Join

O operador LINQ Join permite que você conecte duas fontes de dados com base no seletor de chave para cada fonte, gerando uma tupla de valores quando a chave corresponde. Ele se traduz naturalmente em bancos INNER JOIN de dados relacionais. Embora a Junção LINQ tenha seletores de chave externa e interna, o banco de dados requer uma única condição de junção. Portanto, o EF Core gera uma condição de junção comparando o seletor de chave externa com o seletor de chave interna para igualdade.

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]

Além disso, se os seletores de chave forem tipos anônimos, o EF Core gerará uma condição de junção para comparar a igualdade em termos de componente.

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

O operador LINQ GroupJoin permite que você conecte duas fontes de dados semelhantes à Junção, mas cria um grupo de valores internos para combinar elementos externos. A execução de uma consulta como o exemplo a seguir gera um resultado de Blog e IEnumerable<Post>. Como bancos de dados (especialmente bancos de dados relacionais) não têm uma maneira de representar uma coleção de objetos do lado do cliente, GroupJoin não se traduz para o servidor em muitos casos. Ele exige que você obtenha todos os dados do servidor para fazer GroupJoin sem um seletor especial (primeira consulta abaixo). No entanto, se o seletor estiver limitando os dados que estão sendo selecionados, buscar todos os dados do servidor poderá causar problemas de desempenho (segunda consulta abaixo). É por isso que o EF Core não converte 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

O operador LINQ SelectMany permite enumerar em um seletor de coleção para cada elemento externo e gerar tuplas de valores de cada fonte de dados. De certa forma, é uma junção, mas sem nenhuma condição para que cada elemento externo seja conectado com um elemento da fonte da coleção. Dependendo de como o seletor de coleção está relacionado à fonte de dados externa, SelectMany pode se traduzir em várias consultas diferentes no lado do servidor.

O seletor de coleção não faz referência externa

Quando o seletor de coleção não faz referência a nada da fonte externa, o resultado é um produto cartesiano de ambas as fontes de dados. Isso se traduz CROSS JOIN em bancos de dados relacionais.

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]

Referências de seletor de coleção externas em uma cláusula where

Quando o seletor de coleção tem uma cláusula where, que faz referência ao elemento externo, o EF Core converte-o em uma junção de banco de dados e usa o predicado como a condição de junção. Normalmente, esse caso surge ao usar a navegação de coleção no elemento externo como seletor de coleção. Se a coleção estiver vazia para um elemento externo, nenhum resultado será gerado para esse elemento externo. Mas se DefaultIfEmpty for aplicado no seletor de coleção, o elemento externo será conectado com um valor padrão do elemento interno. Devido a essa distinção, esse tipo de consulta se traduz INNER JOIN na ausência de DefaultIfEmpty e LEFT JOIN quando DefaultIfEmpty é aplicado.

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]

Referências de seletor de coleção externas em um caso não local

Quando o seletor de coleção faz referência ao elemento externo, que não está em uma cláusula where (como o caso acima), ele não se traduz em uma junção de banco de dados. É por isso que precisamos avaliar o seletor de coleção para cada elemento externo. Ele se traduz APPLY em operações em muitos bancos de dados relacionais. Se a coleção estiver vazia para um elemento externo, nenhum resultado será gerado para esse elemento externo. Mas se DefaultIfEmpty for aplicado no seletor de coleção, o elemento externo será conectado com um valor padrão do elemento interno. Devido a essa distinção, esse tipo de consulta se traduz CROSS APPLY na ausência de DefaultIfEmpty e OUTER APPLY quando DefaultIfEmpty é aplicado. Determinados bancos de dados, como SQLite, não dão suporte a operadores APPLY, portanto, esse tipo de consulta pode não ser convertido.

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

Os operadores LINQ GroupBy criam um resultado do tipo IGrouping<TKey, TElement> em que TKey e TElement podem ser qualquer tipo arbitrário. Além disso, IGrouping implementa IEnumerable<TElement>, o que significa que você pode redigir sobre ele usando qualquer operador LINQ após o agrupamento. Como nenhuma estrutura de banco de dados pode representar um IGrouping, os operadores GroupBy não têm tradução na maioria dos casos. Quando um operador de agregação é aplicado a cada grupo, que retorna um escalar, ele pode ser convertido em SQL GROUP BY em bancos de dados relacionais. O SQL GROUP BY também é restritivo. Ele exige que você agrupe somente por valores escalares. A projeção só pode conter colunas de chave de agrupamento ou qualquer agregação aplicada em uma coluna. O EF Core identifica esse padrão e o converte no servidor, como no exemplo a seguir:

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]

O EF Core também converte consultas em que um operador agregado no agrupamento aparece em um operador LINQ Where ou OrderBy (ou outra ordenação). Ele usa a cláusula HAVING no SQL para a cláusula where. A parte da consulta antes de aplicar o operador GroupBy pode ser qualquer consulta complexa, desde que possa ser traduzida para o servidor. Além disso, depois de aplicar operadores de agregação em uma consulta de agrupamento para remover agrupamentos da origem resultante, você poderá redigir sobre ela como qualquer outra 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]

Os operadores de agregação que o EF Core dá suporte são os seguintes

.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)

Podem ter suporte para operadores de agregação adicionais. Verifique os documentos do provedor para obter mais mapeamentos de funções.

Mesmo que não haja uma estrutura de dados para representar um IGrouping, em alguns casos, o EF Core 7.0 e versões mais recentes podem criar os agrupamentos depois que os resultados são retornados do banco de dados. Isso é semelhante ao funcionamento do operador Include ao incluir coleções relacionadas. A consulta LINQ a seguir usa o operador GroupBy para agrupar os resultados pelo valor de sua propriedade Price.

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

Nesse caso, o operador GroupBy não se traduz diretamente em uma cláusula GROUP BY no SQL, mas, em vez disso, o EF Core cria os agrupamentos depois que os resultados são retornados do servidor.

Junção à esquerda

Embora Left Join não seja um operador LINQ, os bancos de dados relacionais têm o conceito de uma Junção à Esquerda que é frequentemente usada em consultas. Um padrão específico em consultas LINQ fornece o mesmo resultado que um LEFT JOIN no servidor. O EF Core identifica esses padrões e gera o equivalente LEFT JOIN no lado do servidor. O padrão envolve a criação de um GroupJoin entre as fontes de dados e, em seguida, a nivelamento do agrupamento usando o operador SelectMany com DefaultIfEmpty na fonte de agrupamento para corresponder nulo quando o interior não tiver um elemento relacionado. O exemplo a seguir mostra a aparência desse padrão e o que ele gera.

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]

O padrão acima cria uma estrutura complexa na árvore de expressão. Por isso, o EF Core exige que você mesclar os resultados de agrupamento do operador GroupJoin em uma etapa imediatamente após o operador. Mesmo que o GroupJoin-DefaultIfEmpty-SelectMany seja usado, mas em um padrão diferente, talvez não o identifiquemos como uma Junção à Esquerda.