Сложные операторы запросов

В языке LINQ есть множество сложных операторов, которые объединяют несколько источников данных или производят сложную обработку. Не для всех операторов LINQ есть подходящие преобразования на стороне сервера. Иногда запрос в одной форме преобразуется на сервере, но в другой — не преобразуется, даже если результат совпадает. На этой странице описываются некоторые сложные операторы и их поддерживаемые варианты. В будущих выпусках, возможно, будет поддерживаться больше шаблонов и будут добавлены соответствующие преобразования. Также важно иметь в виду, что поддержка преобразований зависит от поставщика. Запрос, который преобразуется в SqlServer, может не работать в базах данных SQLite.

Совет

Вы можете скачать используемый в этой статье пример из репозитория GitHub.

Присоединиться

Оператор Join в LINQ позволяет соединять два источника данных на основе селектора ключа для каждого источника, создавая кортеж значений при совпадении ключей. В реляционных базах данных он естественным образом преобразуется в INNER JOIN. Если оператор Join в LINQ имеет внешний и внутренний селекторы ключей, база данных требует одного условия соединения. Таким образом, EF Core создает условие соединения, сравнивая внешний и внутренний селекторы ключей на равенство.

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]

Кроме того, если селекторы ключей являются анонимными типами, EF Core создает условие соединения для покомпонентного сравнения на равенство.

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

Оператор GroupJoin в LINQ позволяет соединять два источника данных, так же как оператор JOIN, но создает группу внутренних значений для соответствующих внешних элементов. Выполнение приведенного ниже запроса дает результат Blog и IEnumerable<Post>. Так как базы данных (особенно реляционные) обычно не позволяют представлять коллекцию объектов на стороне клиента, GroupJoin во многих случаях не преобразуется на сервере. Для выполнения GroupJoin без специального селектора требуется получить все данные с сервера (первый запрос ниже). Но если селектор ограничивает выбор данных, то получение всех данных с сервера может вызвать проблемы с производительностью (второй запрос ниже). Вот почему EF Core не преобразует оператор 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

Оператор SelectMany в LINQ позволяет перечислять селектор коллекции для каждого внешнего элемента и создавать кортежи значений из каждого источника данных. Таким образом, это соединение, но без каких-либо условий, поэтому каждый внешний элемент соединяется с элементом из источника коллекции. В зависимости от того, как селектор коллекции связан с внешним источником данных, оператор SelectMany может преобразовываться в различные запросы на стороне сервера.

Селектор коллекции не ссылается на внешний источник

Если селектор коллекции не ссылается на элементы во внешнем источнике, результатом является декартово произведение обоих источников данных. В реляционных базах данных такой оператор преобразуется в 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]

Селектор коллекции ссылается на внешний источник в предложении WHERE

Если в селекторе коллекции есть предложение WHERE, которое ссылается на внешний элемент, то EF Core преобразует его в соединение базы данных и использует предикат в качестве условия соединения. Обычно такое бывает при использовании свойства навигации по коллекции для внешнего элемента в качестве селектора коллекции. Если коллекция для внешнего элемента пуста, то для него результаты отсутствуют. Но если к селектору коллекции применяется DefaultIfEmpty, внешний элемент соединяется со значением внутреннего элемента по умолчанию. В связи с этим различием запросы такого типа преобразуются в INNER JOIN при отсутствии DefaultIfEmpty и в LEFT JOIN, если 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]

Селектор коллекции ссылается на внешний источник не в предложении WHERE

Если селектор коллекции ссылается на внешний элемент, который не входит в предложение WHERE (как в случае выше), он не преобразуется в соединение базы данных. Поэтому необходимо оценить селектор коллекции для каждого внешнего элемента. Во многих реляционных базах данных такой оператор преобразуется в операции APPLY. Если коллекция для внешнего элемента пуста, то для него результаты отсутствуют. Но если к селектору коллекции применяется DefaultIfEmpty, внешний элемент соединяется со значением внутреннего элемента по умолчанию. В связи с этим различием запросы такого типа преобразуются в CROSS APPLY при отсутствии DefaultIfEmpty и в OUTER APPLY, если DefaultIfEmpty применяется. Некоторые базы данных, такие как SQLite, не поддерживают операторы APPLY, поэтому запросы такого типа могут не преобразовываться.

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

Операторы GroupBy в LINQ создают результат типа IGrouping<TKey, TElement>, где TKey и TElement могут быть произвольного типа. Кроме того, IGrouping реализует интерфейс IEnumerable<TElement>. Это означает, что после группирования можно производить композицию с помощью любого оператора LINQ. Так как ни одна структура базы данных не может представлять IGrouping, в большинстве случаев операторы GroupBy не преобразуются. Если к каждой группе применяется статистический оператор, возвращающий скалярное значение, в реляционных базах данных его можно преобразовать в оператор SQL GROUP BY. Применение оператора SQL GROUP BY также ограничено. Он требует выполнять группирование только по скалярным значениям. Проекция может содержать только ключевые столбцы группирования или любое статистическое выражение, примененное к столбцу. EF Core распознает этот шаблон и преобразует его на стороне сервера, как показано в следующем примере:

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 также преобразует запросы, в которых статистический оператор, выполняющий действия над группированием, включен в оператор LINQ Where или OrderBy (либо другое упорядочение). Для предложения WHERE в SQL используется предложение HAVING. Часть запроса перед применением оператора GroupBy может быть любым сложным запросом, который может быть преобразован на стороне сервера. Кроме того, после применения статистических операторов к запросу группирования для удаления группирований из результирующего источника этот запрос можно использовать для композиции, как и любой другой запрос.

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]

EF Core поддерживает следующие статистические операторы:

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

Могут поддерживаться дополнительные статистические операторы. Проверьте документы поставщика на наличие дополнительных сопоставлений функций.

Несмотря на отсутствие структуры базы данных для представления IGrouping, в некоторых случаях EF Core 7.0 и более поздней версии могут создавать группировки после возврата результатов из базы данных. Это аналогично тому, как Include работает оператор при включении связанных коллекций. Следующий запрос LINQ использует оператор GroupBy для группировки результатов по значению свойства Price.

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

В этом случае оператор GroupBy не преобразуется непосредственно GROUP BY в предложение в SQL, но вместо этого EF Core создает группы после возврата результатов с сервера.

Левое соединение

Хотя в LINQ нет оператора левого соединения, в запросах к реляционным базам данных левое соединение используется часто. Определенный шаблон в запросах LINQ дает тот же результат, что и оператор LEFT JOIN на сервере. EF Core распознает такой шаблон и создает эквивалентный оператор LEFT JOIN на стороне сервера. Шаблон предполагает создание соединения GroupJoin между двумя источниками данных и последующее преобразование группирования в плоскую структуру с помощью оператора SelectMany с DefaultIfEmpty, применяемого к источнику группирования, для сопоставления со значением NULL, когда во внутреннем источнике нет соответствующего элемента. В приведенном ниже примере показано, как выглядит этот шаблон и какой результат он дает.

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]

Приведенный выше шаблон создает сложную структуру в дереве выражения. По этой причине EF Core требует преобразовать результаты группирования с помощью оператора GroupJoin в плоскую структуру сразу после этого оператора. Если конструкция GroupJoin-DefaultIfEmpty-SelectMany используется в рамках другого шаблона, она может не распознаваться как левое соединение.