LeftJoin и RightJoin в EF LINQ
Если вы когда-либо работали с базами данных, вы знаете о LEFT JOIN (RIGHT JOIN). Они используются довольно часто, но в Entity Framework Core выполнение левого объединения всегда было… довольно замороченным. Приходилось проделывать сложные трюки с GroupJoin и DefaultIfEmpty, что затрудняло чтение и поддержку кода. Но в .NET 10 это наконец-то исправлено с помощью новых методов LeftJoin и RightJoin.
LEFT JOIN возвращает все строки из левой части и соответствующие им строки из правой. Если совпадений нет, правая часть равна null. Это используется, чтобы сохранить «владельцев», даже если у них нет связанных строк (например, показать все товары, даже если у некоторых нет отзывов).
Старый способ
До .NET 10 для левого объединения в LINQ требовалось вызвать GroupJoin, а затем DefaultIfEmpty, чтобы оставить строки без совпадений. Это работало, но смысл был скрыт в шуме.
var query = dbContext.Products
.GroupJoin(
dbContext.Reviews,
prod => prod.Id,
review => review.ProductId,
(prod, reviews) =>
new { prod, subgroup = reviews })
.SelectMany(
j => j.subgroup.DefaultIfEmpty(),
(j, review) => new
{
ProductId = j.prod.Id,
j.prod.Name,
j.prod.Price,
ReviewId = (int?)review!.Id ?? 0,
Rating = (int?)review!.Rating ?? 0,
Comment = review!.Comment ?? "N/A"
})
.OrderBy(x => x.ProductId)
.ThenBy(x => x.ReviewId);
Вот SQL, который генерируется для этого запроса:
SELECT
p."Id" AS "ProductId",
p."Name",
p."Price",
COALESCE(r."Id", 0) AS "ReviewId",
COALESCE(r."Rating", 0) AS "Rating",
COALESCE(r."Comment", 'N/A') AS "Comment"
FROM "Products" AS p
LEFT JOIN "Reviews" AS r ON p."Id" = r."ProductId"
ORDER BY p."Id", COALESCE(r."Id", 0)
GroupJoin сопоставляет строки, DefaultIfEmpty вставляет значение по умолчанию (NULL) при отсутствии совпадений, поэтому строка из левого множества всё равно отображается. Это слишком многословно для такого распространённого явления, как левое объединение.
Новый подход в EF 10
Теперь мы можем выразить то, что хотим:
var query = dbContext.Products
.LeftJoin(
dbContext.Reviews,
prod => prod.Id,
review => review.ProductId,
(prod, review) => new
{
ProductId = prod.Id,
prod.Name,
prod.Price,
ReviewId = (int?)review.Id ?? 0,
Rating = (int?)review.Rating ?? 0,
Comment = review.Comment ?? "N/A"
})
.OrderBy(x => x.ProductId)
.ThenBy(x => x.ReviewId)
Сгенерированный SQL-код идентичен предыдущему примеру.
Здесь очевидна цель: вы видите LeftJoin, вы знаете, чего ожидать. Меньше кода, меньше изменяемых элементов (нет GroupJoin, DefaultIfEmpty, SelectMany). Тот же результат: все товары сохранены, отзывы могут отсутствовать.
RightJoin
RightJoin аналогичен - сохраняет все строки с правой стороны и только соответствующие строки с левой. EF Core преобразует это в RIGHT JOIN. Это удобно, когда необходимо сохранить вторую последовательность.
Итого
Левые объединения нужны довольно часто. Отображение всех пользователей с необязательными настройками профиля. Все товары с необязательными отзывами. Все заказы с необязательными данными о доставке. Теперь это так же просто, как и любой другой метод LINQ.
Несколько советов:
- В проекции защитите сторону, допускающую null:
review.Comment ?? "N/A";- Сохраняйте проекции небольшими, чтобы не извлекать больше столбцов, чем необходимо;
- Добавляйте индексы по ключам объединений для улучшения планов запросов.
Источник: https://www.milanjovanovic.tech/blog/whats-new-in-ef-core-10-leftjoin-and-rightjoin-operators-in-linq