Предложение join (Справочник по C#)
Предложение join
удобно для связывания элементов из разных исходных последовательностей, не имеющих прямых связей в объектной модели. Единственное требование заключается в том, что у элементов в каждом источнике должно быть общим некоторое значение, которое может быть проверено на равенство. Например, у дистрибьютора может быть список поставщиков определенного продукта и список покупателей. Предложение join
может использоваться, например, для создания списка поставщиков и покупателей этого продукта, которые находятся в одном заданном регионе.
Предложение join
принимает две исходные последовательности в качестве входных данных. Элементы в каждой последовательности должны являться свойством (или содержать свойство), которое можно сравнить с соответствующим свойством в другой последовательности. Предложение join
сравнивает указанные ключи на равенство с помощью специального ключевого слова equals
. Все соединения, выполняемые предложением join
, являются эквисоединениями. Форма выходных данных предложения join
зависит от конкретного типа выполняемого соединения. Ниже приведены три наиболее распространенных типа соединения:
внутреннее соединение,
групповое соединение,
левое внешнее соединение.
внутреннее соединение,
В следующем примере показано простое внутреннее эквисоединение. По этому запросу создается прямая последовательность пар "название продукта/категория". Одна и та же строка, обозначающая категорию, будет присутствовать в нескольких элементах. Если для элемента из categories
нет соответствующего элемента в products
, эта категория не будет отображаться в результатах.
var innerJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID
select new { ProductName = prod.Name, Category = category.Name }; //produces flat sequence
Дополнительные сведения см. в разделе Выполнение внутренних соединений.
групповое соединение,
Предложение join
с выражением into
называется групповым соединением.
var innerGroupJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
select new { CategoryName = category.Name, Products = prodGroup };
Групповое соединение создает иерархическую последовательность результатов, которая связывает элементы в левой исходной последовательности с одним или несколькими соответствующими элементами в правой исходной последовательности. Групповое соединение не имеет эквивалента в терминах реляционной базы данных. По сути это последовательность массивов объектов.
Если для элементов в левом источнике не удается найти элементы из правой исходной последовательности, предложение join
создаст пустой массив для этого элемента. Таким образом, групповое соединение — это такое же внутреннее эквисоединение, за исключением того что последовательность результатов организуется в группы.
Если просто выбрать результаты группового соединения, можно получить доступ к элементам, но не определить ключ, по которому они совпадают. Таким образом, удобнее будет выбирать результаты группового соединения в новый тип, который также имеет имя ключа, как показано в предыдущем примере.
Кроме того, результаты группового соединения можно использовать как генераторы других вложенных запросов:
var innerGroupJoinQuery2 =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
from prod2 in prodGroup
where prod2.UnitPrice > 2.50M
select prod2;
Дополнительные сведения см. в разделе Выполнение групповых соединений.
левое внешнее соединение.
В левом внешнем соединении возвращаются все элементы в левой исходной последовательности, даже если в правой последовательности нет соответствующих элементов. Для выполнения левого внешнего соединения в LINQ используйте метод DefaultIfEmpty
в сочетании с групповым соединением, чтобы указать правый элемент по умолчанию, создаваемый, если левый элемент не имеет совпадений. null
можно использовать как значение по умолчанию для любого ссылочного типа, кроме того, можно указать определенный пользователем тип по умолчанию. В следующем примере показан определяемый пользователем тип по умолчанию:
var leftOuterJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
from item in prodGroup.DefaultIfEmpty(new Product { Name = String.Empty, CategoryID = 0 })
select new { CatName = category.Name, ProdName = item.Name };
Дополнительные сведения см. в разделе Выполнение левых внешних соединений.
Оператор равенства
Предложение join
выполняет эквисоединение. Другими словами совпадения могут основываться только на равенстве двух ключей. Другие типы сравнений, такие как "больше чем" или "не равно" не поддерживаются. Чтобы гарантировать, что все соединения являются эквисоединениями, предложение join
использует ключевое слово equals
оператора ==
. Ключевое слово equals
может использоваться только в предложении join
, и оно имеет важные отличия от оператора ==
. При сравнении строк equals
имеет перегрузку для сравнения по значению, а оператор ==
использует равенство ссылок. Если обе стороны сравнения имеют одинаковые строковые переменные и equals
==
достигают одного результата: true. Это связано с тем, что, когда программа объявляет две или более эквивалентные строковые переменные, компилятор сохраняет все из них в одном расположении. Это называется интернингом. Другим важным отличием является сравнение со значением NULL: null equals null
вычисляется как false с оператором equals
, а оператор ==
вычисляет его как true. Наконец, отличается поведения включения в область: с equals
левый ключ использует внешнюю исходную последовательность, а правый ключ — внутренний источник. Внешний источник попадает в область только левой части equals
, а внутренняя исходная последовательность — только в область правой части.
Соединения, не являющиеся уравнивающими
Соединения, не являющиеся эквисоединениями, перекрестные соединения и другие настраиваемые операции соединения можно выполнять с помощью нескольких предложений from
для независимого ввода новых последовательностей в запрос. Дополнительные сведения см. в разделе Выполнение пользовательских операций соединения.
Соединения коллекций объектов и реляционных таблиц
В выражении запроса LINQ операции соединения выполняются для коллекций объектов. Коллекции объектов нельзя "соединять" точно так же, как две реляционные таблицы. В LINQ явные предложения join
требуются, только если две исходные последовательности не связаны каким-либо отношением. При работе с LINQ to SQL внешние таблицы ключей представлены в объектной модели как свойства первичной таблицы. Например, в базе данных Northwind таблица Customer имеет связь типа "внешний ключ" с таблицей Orders. При сопоставлении таблиц с объектной моделью класс Customer имеет свойство Orders, содержащий коллекцию заказов, связанных с клиентом. Фактически соединение уже выполнено автоматически.
Дополнительные сведения о запросах между связанными таблицами в контексте LINQ to SQL см. в разделе "Практическое руководство. Сопоставление связей базы данных".
Составные ключи
Равенство нескольких значений можно проверить с помощью составного ключа. Дополнительные сведения см. в разделе Соединение с помощью составных ключей. Составные ключи можно также использовать в предложении group
.
Пример
В следующем примере сравниваются результаты внутреннего соединения, группового соединения и левого внешнего соединения для одних и тех же исходных данных с использованием одинаковых совпадающих ключей. В эти примеры для уточнения результатов в окне консоли добавлен дополнительный код.
class JoinDemonstration
{
#region Data
class Product
{
public required string Name { get; init; }
public required int CategoryID { get; init; }
}
class Category
{
public required string Name { get; init; }
public required int ID { get; init; }
}
// Specify the first data source.
List<Category> categories =
[
new Category {Name="Beverages", ID=001},
new Category {Name="Condiments", ID=002},
new Category {Name="Vegetables", ID=003},
new Category {Name="Grains", ID=004},
new Category {Name="Fruit", ID=005}
];
// Specify the second data source.
List<Product> products =
[
new Product {Name="Cola", CategoryID=001},
new Product {Name="Tea", CategoryID=001},
new Product {Name="Mustard", CategoryID=002},
new Product {Name="Pickles", CategoryID=002},
new Product {Name="Carrots", CategoryID=003},
new Product {Name="Bok Choy", CategoryID=003},
new Product {Name="Peaches", CategoryID=005},
new Product {Name="Melons", CategoryID=005},
];
#endregion
static void Main(string[] args)
{
JoinDemonstration app = new JoinDemonstration();
app.InnerJoin();
app.GroupJoin();
app.GroupInnerJoin();
app.GroupJoin3();
app.LeftOuterJoin();
app.LeftOuterJoin2();
}
void InnerJoin()
{
// Create the query that selects
// a property from each element.
var innerJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID
select new { Category = category.ID, Product = prod.Name };
Console.WriteLine("InnerJoin:");
// Execute the query. Access results
// with a simple foreach statement.
foreach (var item in innerJoinQuery)
{
Console.WriteLine("{0,-10}{1}", item.Product, item.Category);
}
Console.WriteLine("InnerJoin: {0} items in 1 group.", innerJoinQuery.Count());
Console.WriteLine(System.Environment.NewLine);
}
void GroupJoin()
{
// This is a demonstration query to show the output
// of a "raw" group join. A more typical group join
// is shown in the GroupInnerJoin method.
var groupJoinQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
select prodGroup;
// Store the count of total items (for demonstration only).
int totalItems = 0;
Console.WriteLine("Simple GroupJoin:");
// A nested foreach statement is required to access group items.
foreach (var prodGrouping in groupJoinQuery)
{
Console.WriteLine("Group:");
foreach (var item in prodGrouping)
{
totalItems++;
Console.WriteLine(" {0,-10}{1}", item.Name, item.CategoryID);
}
}
Console.WriteLine("Unshaped GroupJoin: {0} items in {1} unnamed groups", totalItems, groupJoinQuery.Count());
Console.WriteLine(System.Environment.NewLine);
}
void GroupInnerJoin()
{
var groupJoinQuery2 =
from category in categories
orderby category.ID
join prod in products on category.ID equals prod.CategoryID into prodGroup
select new
{
Category = category.Name,
Products = from prod2 in prodGroup
orderby prod2.Name
select prod2
};
//Console.WriteLine("GroupInnerJoin:");
int totalItems = 0;
Console.WriteLine("GroupInnerJoin:");
foreach (var productGroup in groupJoinQuery2)
{
Console.WriteLine(productGroup.Category);
foreach (var prodItem in productGroup.Products)
{
totalItems++;
Console.WriteLine(" {0,-10} {1}", prodItem.Name, prodItem.CategoryID);
}
}
Console.WriteLine("GroupInnerJoin: {0} items in {1} named groups", totalItems, groupJoinQuery2.Count());
Console.WriteLine(System.Environment.NewLine);
}
void GroupJoin3()
{
var groupJoinQuery3 =
from category in categories
join product in products on category.ID equals product.CategoryID into prodGroup
from prod in prodGroup
orderby prod.CategoryID
select new { Category = prod.CategoryID, ProductName = prod.Name };
//Console.WriteLine("GroupInnerJoin:");
int totalItems = 0;
Console.WriteLine("GroupJoin3:");
foreach (var item in groupJoinQuery3)
{
totalItems++;
Console.WriteLine(" {0}:{1}", item.ProductName, item.Category);
}
Console.WriteLine("GroupJoin3: {0} items in 1 group", totalItems);
Console.WriteLine(System.Environment.NewLine);
}
void LeftOuterJoin()
{
// Create the query.
var leftOuterQuery =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
select prodGroup.DefaultIfEmpty(new Product() { Name = "Nothing!", CategoryID = category.ID });
// Store the count of total items (for demonstration only).
int totalItems = 0;
Console.WriteLine("Left Outer Join:");
// A nested foreach statement is required to access group items
foreach (var prodGrouping in leftOuterQuery)
{
Console.WriteLine("Group:");
foreach (var item in prodGrouping)
{
totalItems++;
Console.WriteLine(" {0,-10}{1}", item.Name, item.CategoryID);
}
}
Console.WriteLine("LeftOuterJoin: {0} items in {1} groups", totalItems, leftOuterQuery.Count());
Console.WriteLine(System.Environment.NewLine);
}
void LeftOuterJoin2()
{
// Create the query.
var leftOuterQuery2 =
from category in categories
join prod in products on category.ID equals prod.CategoryID into prodGroup
from item in prodGroup.DefaultIfEmpty()
select new { Name = item == null ? "Nothing!" : item.Name, CategoryID = category.ID };
Console.WriteLine("LeftOuterJoin2: {0} items in 1 group", leftOuterQuery2.Count());
// Store the count of total items
int totalItems = 0;
Console.WriteLine("Left Outer Join 2:");
// Groups have been flattened.
foreach (var item in leftOuterQuery2)
{
totalItems++;
Console.WriteLine("{0,-10}{1}", item.Name, item.CategoryID);
}
Console.WriteLine("LeftOuterJoin2: {0} items in 1 group", totalItems);
}
}
/*Output:
InnerJoin:
Cola 1
Tea 1
Mustard 2
Pickles 2
Carrots 3
Bok Choy 3
Peaches 5
Melons 5
InnerJoin: 8 items in 1 group.
Unshaped GroupJoin:
Group:
Cola 1
Tea 1
Group:
Mustard 2
Pickles 2
Group:
Carrots 3
Bok Choy 3
Group:
Group:
Peaches 5
Melons 5
Unshaped GroupJoin: 8 items in 5 unnamed groups
GroupInnerJoin:
Beverages
Cola 1
Tea 1
Condiments
Mustard 2
Pickles 2
Vegetables
Bok Choy 3
Carrots 3
Grains
Fruit
Melons 5
Peaches 5
GroupInnerJoin: 8 items in 5 named groups
GroupJoin3:
Cola:1
Tea:1
Mustard:2
Pickles:2
Carrots:3
Bok Choy:3
Peaches:5
Melons:5
GroupJoin3: 8 items in 1 group
Left Outer Join:
Group:
Cola 1
Tea 1
Group:
Mustard 2
Pickles 2
Group:
Carrots 3
Bok Choy 3
Group:
Nothing! 4
Group:
Peaches 5
Melons 5
LeftOuterJoin: 9 items in 5 groups
LeftOuterJoin2: 9 items in 1 group
Left Outer Join 2:
Cola 1
Tea 1
Mustard 2
Pickles 2
Carrots 3
Bok Choy 3
Nothing! 4
Peaches 5
Melons 5
LeftOuterJoin2: 9 items in 1 group
Press any key to exit.
*/
Замечания
Предложение join
, за которым не следует into
, преобразуется в вызов метода Join. Предложение join
, за которым следует into
, преобразуется в вызов метода GroupJoin.
См. также
- Ключевые слова запроса (LINQ)
- LINQ
- Операции соединения
- Предложение group
- Выполнение левых внешних соединений
- Выполнение внутренних соединений
- Выполнение групповых соединений
- Упорядочение результатов предложения соединения
- Соединение с помощью составных ключей
- Совместимые системы баз данных для Visual Studio