子句 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 };
如需詳細資訊,請參閱 執行左外連接。
equals 運算符
條款 join 會執行等聯結。 換句話說,您只能根據兩個鍵值的相等性進行匹配。 不支援其他類型的比較,例如「大於」或「不相等」。 為了明確表示所有聯結都是等聯結, join 子句會使用 equals 關鍵詞,而不是 == 運算符。
equals 關鍵詞只能在 join 子句中使用,且在某些重要方面與 == 運算符不同。 比較字串時, equals 具有要依值比較的多載,而運算符 == 會使用參考相等。 當比較的兩端都有相同的字串變數, equals 並 == 達到相同的結果:true。 這是因為,當程式宣告兩個以上的對等字串變數時,編譯程式會將所有變數儲存在相同的位置。 這稱為駐留。 另一個重要差異是 Null 比較: null equals null 會使用 equals 運算符評估為 false,而非將它評估為 true 的 == 運算符。 最後,範圍行為不同:使用 equals時,左鍵會取用外部來源序列,而右鍵會取用內部來源。 外部來源只位於 左側 equals 的範圍中,而內部來源序列只位於右側的範圍中。
Non-equijoins
您可以使用多個 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.
*/
備註
join後面未接著into的子句會轉譯成Join方法呼叫。
join後面接著into的子句會被轉換成GroupJoin方法呼叫。