Предложение 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, содержащий коллекцию заказов, связанных с клиентом. Фактически соединение уже выполнено автоматически.

Дополнительные сведения о запросах между связанными таблицами в контексте 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.

См. также