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 o LINQ Join 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 o componente de igualdade.

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. Executar uma consulta como o exemplo a seguir gera um resultado de Blog&IEnumerable<Post>. Como os 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). Mas 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 que você enumere em um seletor de coleção para cada elemento externo e gere tuplas de valores de cada fonte de dados. De certa forma, é uma junção, mas sem nenhuma condição para que cada elemento externo esteja conectado com um elemento da fonte da coleção. Dependendo de como o seletor de coleta 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. Ele se traduz em bancos CROSS JOIN 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]

O seletor de coleção faz referência externa 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 o 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]

O seletor de coleção faz referência externa em um caso diferente de onde

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 o SQLite, não dão suporte APPLY a operadores, portanto, esse tipo de consulta pode não ser traduzido.

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 IGroupingoperador GroupBy, 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 traduzido para 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 de agregação no agrupamento aparece em um operador LINQ Where ou OrderBy (ou em outra ordenação). Ele usa 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) SOMA(Propriedade)

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

Junção à Esquerda

Embora Left Join não seja um operador LINQ, os bancos de dados relacionais têm o conceito de um Left Join que é frequentemente usado 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 duas fontes de dados e, em seguida, o 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ê nivele 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.