Поделиться через


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

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

Подсказка

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

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

Оператор LINQ Join позволяет подключать два источника данных на основе селектора ключей для каждого источника, создавая кортеж значений при совпадении ключа. Она естественно преобразуется в INNER 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'))

Присоединение к группе

Оператор LINQ GroupJoin позволяет подключать два источника данных, аналогичные 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

Оператор LINQ SelectMany позволяет перечислять селектор коллекции для каждого внешнего элемента и создавать кортежи значений из каждого источника данных. Таким образом, это соединение, но без каких-либо условий, поэтому каждый внешний элемент связан с элементом из источника коллекции. В зависимости от того, как селектор коллекции связан с внешним источником данных, 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 (как в случае выше), он не переводится в соединение базы данных. Поэтому необходимо проверить селектор коллекции для каждого из внешних элементов. Он преобразуется в 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]

Группировка по

Операторы LINQ GroupBy создают результат типа 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 (или других операторах упорядочивания). Он использует HAVING условие в SQL для условия WHERE. Часть запроса перед применением оператора 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, следующие:

.СЕТЬ SQL
Среднее(x => x.Property) AVG(Свойство)
Count() COUNT(*)
LongCount() COUNT(*)
Max(x => x.Property) МАКС(Property)
Min(x => x.Свойство) MIN(Свойство)
Sum(x => x.Property) СУММ(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 используется, но в другой конструкции, мы не можем определить это как левое соединение.