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

如需詳細資訊,請參閱執行內部聯結

群組聯結

具有 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: {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 方法呼叫。

另請參閱