该 join
子句可用于将不同源序列中的元素关联到对象模型中没有直接关系的元素。 唯一的要求是每个源中的元素共享一些可比较相等的值。 例如,食品经销商可能具有特定产品的供应商列表和买家列表。 例如,可以使用子 join
句创建同一指定区域中该产品的供应商和买家的列表。
join
子句将 2 个源序列作为输入。 每个序列中的元素必须是或包含一个属性,该属性可以与其他序列中的相应属性进行比较。 该 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
有关详细信息,请参阅 “执行内部联接”。
分组联接
含有 into
表达式的 join
子句称为分组联接。
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
使用 equals
运算符的计算结果为 false,而不使用计算结果为 true 的 ==
运算符。 最后,范围行为不同:对于 equals
,左键使用外部源序列,而右键使用内部源。 外部源仅在左侧 equals
的作用域中,并且内部源序列仅在右侧的作用域内。
非同等联接
可以使用多个 from
子句将新序列独立引入到查询中,以执行非等值连接、交叉连接和其他自定义连接操作。 有关详细信息,请参阅 执行自定义联接作。
对象集合联接与关系表
在 LINQ 查询表达式中,对对象集合执行联接作。 不能以与两个关系表完全相同的方式“联接”对象集合。 在 LINQ 中,仅当两个源序列不受任何关系绑定时,才需要显式 join
子句。 使用 LINQ to SQL 时,外键表在对象模型中表示为主表的属性。 例如,在 Northwind 数据库中,Customer 表与 Orders 表具有外键关系。 将表映射到对象模型时,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: {innerJoinQuery.Count()} items in 1 group.");
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: {totalItems} items in {groupJoinQuery.Count()} unnamed groups");
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: {totalItems} items in {groupJoinQuery2.Count()} named groups");
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($" {item.ProductName}:{item.Category}");
}
Console.WriteLine($"GroupJoin3: {totalItems} items in 1 group");
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: {totalItems} items in {leftOuterQuery.Count()} groups");
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: {leftOuterQuery2.Count()} items in 1 group");
// 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: {totalItems} items in 1 group");
}
}
/*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.
*/
注解
后面未跟 into
的 join
子句转换为 Join 方法调用。 后面跟 into
的 join
子句转换为 GroupJoin 方法调用。