Compartilhar via


Operadores de consulta complexos

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

Você pode exibir o exemplo deste artigo no GitHub.

Participar

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. Traduz-se naturalmente em INNER JOIN em bancos 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'))

UnirGrupo

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 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 união, mas sem qualquer condição, então todo elemento externo é conectado a um elemento da fonte da coleção. Dependendo de como o seletor de coleção está relacionado à fonte de dados externa, SelectMany pode resultar em diversas 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. Em bancos de dados relacionais, isso se traduz em CROSS JOIN.

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 a seletor de coleções 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 como INNER JOIN na ausência de DefaultIfEmpty e como 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. Isso se traduz em APPLY 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 como CROSS APPLY na ausência de DefaultIfEmpty e como OUTER APPLY quando DefaultIfEmpty é aplicado. Determinados bancos de dados, como SQLite, não dão suporte APPLY a operadores, 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]

AgruparPor

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 compor nele usando qualquer operador LINQ após o agrupamento. Como nenhuma estrutura de banco de dados pode representar um IGrouping, 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 traduz consultas em que um operador de agregação no contexto do agrupamento aparece em um operador LINQ, como Where ou OrderBy (ou outra forma de 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

.REDE SQL
Average(x => x.Property) AVG(Property)
Count() COUNT(*)
LongCount() COUNT(*)
Max(x => x.Property) MAX(Propriedade)
Min(x => x.Property) MIN(Propriedade)
Sum(x => x.Property) SOMA(Propriedade)

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

Embora não haja nenhuma estrutura de banco de dados para representar um IGrouping, em alguns casos, o EF Core 7.0 e versões mais recentes pode criar os agrupamentos depois que os resultados são retornados do banco de dados. Isso é semelhante a como o Include operador funciona 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 GROUP BY cláusula 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, ao nivelar o agrupamento usando o operador SelectMany com DefaultIfEmpty na fonte de agrupamento para corresponder a nulo quando não houver um elemento relacionado no elemento interno. 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ê acachate os resultados de agrupamento do operador GroupJoin em uma etapa imediatamente após o operador. Mesmo que o GroupJoin-DefaultIfEmpty-SelectMany seja usado de uma forma diferente, talvez não o identifiquemos como um Left Join.