Предложение 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

Дополнительные сведения см. в разделе Практическое руководство. Выполнение внутренних соединений (Руководство по программированию на C#).

Group Join

Предложение 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;

Дополнительные сведения см. в разделе Практическое руководство. Выполнение групповых соединений (Руководство по программированию на 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#).

Оператор equals

Предложение join выполняет уравнивающее соединение. Другими словами, совпадения могут основываться только на равенстве двух ключей. Другие типы сравнений, такие как "больше" или "не равно" не поддерживаются. Чтобы гарантировать, что все соединения являются уравнивающими соединениями, предложение join вместо оператора == использует ключевое слово equals. Ключевое слово equals можно использовать только в предложении join и оно отличается от оператора == одним очень важным моментом. С equals левый ключ потребляет последовательность внешних источников, а правый ключ потребляет внутренний источник. Внешний источник попадает в область слева от equals, а последовательность внутренних источников попадает в область справа.

Не уравнивающие соединения

Соединения, не являющиеся уравнивающими, перекрестные соединения и другие операции собственных соединений можно выполнять при помощи нескольких предложений from для независимого включения новых последовательностей в запрос. Дополнительные сведения см. в разделе Практическое руководство. Выполнение пользовательских операций соединения (Руководство по программированию на C#).

Соединения коллекций объектов иреляционных таблиц

В выражении запроса LINQ операции соединения выполняются с коллекциями объектов. Коллекции объектов можно "соединить" в точности так же, как это делается с двумя реляционными таблицами. В LINQ явные предложения join требуются только когда две последовательности не связаны каким-либо отношением. При работе с LINQ to SQL таблицы внешних ключей представлены в объектной модели как свойства основной таблицы. Например, в базе данных “Northwind” таблица “Customer” имеет связь внешнего ключа с таблицей “Orders”. При сопоставлении таблиц с объектной моделью, класс “Customer” имеет свойство “Orders” с коллекцией “Orders”, связанной с таким классом “Customer”. Фактически, соединение уже было выполнено.

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

См. также

Задачи

Практическое руководство. Выполнение левых внешних соединений (Руководство по программированию на C#)

Практическое руководство. Выполнение внутренних соединений (Руководство по программированию на C#)

Практическое руководство. Выполнение групповых соединений (Руководство по программированию на C#)

Практическое руководство. Упорядочение результатов предложения соединения (Руководство по программированию на C#)

Практическое руководство. Соединение с помощью составных ключей (Руководство по программированию в C#)

Практическое руководство. Установка образцов баз данных

Ссылки

Предложение group (Справочник по C#)

Основные понятия

Выражения запросов LINQ (Руководство по программированию на C#)

Операции соединения

Другие ресурсы

Ключевые слова запроса (Справочник по C#)