join 句 (C# リファレンス)

オブジェクト モデル内で直接的な関係を持たない、異なるソース シーケンスの要素どうしを関連付けるには、join 句を使用すると便利です。 唯一の要件は、各ソースの要素が、等価比較できるような値を共有していることです。 たとえば、食品販売業者が、特定の商品についての仕入先一覧とバイヤー一覧を持っているとします。 join 句を使用すると、その商品に関して、指定した同じ地域の仕入先とバイヤーの一覧を作成できます。

join 句は、入力として 2 つのソース シーケンスを受け取ります。 各シーケンス内の要素は、対応するもう一方のシーケンスのプロパティと比較できるプロパティであるか、このプロパティを含んでいる必要があります。 join 句は、特別な equals キーワードを使用して、指定したキーが等しいかどうかを比較します。 join 句によって実行される結合は、すべて等結合です。 join 句の出力結果は、実行する結合の種類によって異なります。 最も一般的な結合の種類は、次の 3 つです。

  • 内部結合

  • グループ化結合

  • 左外部結合

内部結合

簡単な内部等結合の例を次に示します。 このクエリでは、"商品名/カテゴリ" ペアのフラットなシーケンスが生成されます。 同じカテゴリ文字列が、複数の要素に表示されます。 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

詳細については、「方法 : 内部結合を実行する (C# プログラミング ガイド)」を参照してください。

グループ化結合

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 };

グループ化結合では、階層構造の結果シーケンスが生成され、左側のソース シーケンスの要素が、右側のソース シーケンスに含まれる 1 つ以上の一致する要素に関連付けられます。 グループ化結合は基本的にオブジェクト配列のシーケンスであり、リレーショナル データベースの用語には、これに相当するものがありません。

右側のソース シーケンスに左側のソースの要素と一致する要素がない場合、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;    

詳細については、「方法 : グループ化結合を実行する (C# プログラミング ガイド)」を参照してください。

左外部結合

左外部結合では、右側のシーケンスに一致する要素がない場合でも、左側のソース シーケンスに含まれるすべての要素が返されます。 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 };

詳細については、「方法 : 左外部結合を実行する (C# プログラミング ガイド)」を参照してください。

等値演算子

join 句は、等結合を実行します。 つまり、2 つのキーが等しい場合のみ、一致が結合されます。 他の種類の比較 ("より大きい"、"等しくない" など) はサポートされません。 すべての結合が等結合であることを明確にするため、join 句では、== 演算子の代わりに equals キーワードが使用されます。 join 句では equals キーワードのみ使用できます。このキーワードと == 演算子には、重要な違いが 1 つあります。 equals の場合、左辺のキーは外部のソース シーケンスを使用し、右辺のキーは内部のソースを使用します。 外部のソースは equals の左辺のスコープにしかなく、内部のソース シーケンスは右辺のスコープにしかありません。

非等結合

複数の from 句を使用して新しいシーケンスをクエリに個別に導入すると、非等結合、クロス結合、および他のカスタム結合操作を実行できます。 詳細については、「方法 : カスタム結合操作を実行する (C# プログラミング ガイド)」を参照してください。

オブジェクト コレクションの結合とリレーショナル テーブルの結合

LINQ クエリ式では、結合操作はオブジェクト コレクションに対して実行されます。 オブジェクト コレクションは、2 つのリレーショナル テーブルを結合するのとまったく同じ方法で "結合" することはできません。 LINQ では、2 つのソース シーケンスの間に何の関連付けもない場合のみ、明示的な join 句が必要です。 LINQ to SQL を使用する場合、オブジェクト モデルの外部キー テーブルは、主テーブルのプロパティとして表されます。 たとえば、Northwind データベースの Customer テーブルと Orders テーブルとの間には、外部キー リレーションシップがあります。 この 2 つのテーブルをオブジェクト モデルにマップすると、Customer クラスは Orders プロパティを持っていて、Orders プロパティにはその Customer に関連付けられた Orders のコレクションが格納されていることになります。 つまり、結合は既に自動的に行われています。

LINQ to SQL のコンテキストで複数の関連テーブルを照会する方法の詳細については、「方法 : データベース リレーションシップを割り当てる (LINQ to SQL)」を参照してください。

複合キー

複合キーを使用すると、複数の値が等しいかどうかをテストできます。 詳細については、「方法 : 複合キーを使用して結合する (C# プログラミング ガイド)」を参照してください。 複合キーは、group 句でも使用できます。

使用例

同じデータ ソースに対して同じ一致するキーを使用して内部結合、グループ化結合、および左外部結合を実行した結果の比較を次の例に示します。 これらの例には、コンソールでの表示結果をわかりやすくするために、余分なコードが追加されています。

    class JoinDemonstration
    {
        #region Data

        class Product
        {
            public string Name { get; set; }
            public int CategoryID { get; set; }
        }

        class Category
        {
            public string Name { get; set; }
            public int ID { get; set; }
        }

        // Specify the first data source.
        List<Category> categories = new List<Category>()
        { 
            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 List<Product>()
       {
          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();

            // Keep the console window open in debug mode.
            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
        }

        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, groupJoinQuery3.Count());
            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:", prodGrouping.Count());
                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 メソッドの呼び出しに変換されます。

参照

処理手順

方法 : 左外部結合を実行する (C# プログラミング ガイド)

方法 : 内部結合を実行する (C# プログラミング ガイド)

方法 : グループ化結合を実行する (C# プログラミング ガイド)

方法 : join 句の結果の順序を指定する (C# プログラミング ガイド)

方法 : 複合キーを使用して結合する (C# プログラミング ガイド)

参照

group 句 (C# リファレンス)

概念

LINQ クエリ式 (C# プログラミング ガイド)

結合演算

その他の技術情報

クエリ キーワード (C# リファレンス)