Поделиться через


оператор 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, содержащее коллекцию 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 вызов метода.

См. также