Соединения

80

»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ

Многие отношения в базе данных определены как ассоциации, и можно получить доступ к ассоциированным объектам, просто обращаясь к членам класса. Однако отображаться подобным образом могут только те отношения, которые определены с использованием внешних ключей. Поскольку не каждый тип отношения определен с применением внешних ключей, иногда приходится соединять таблицы явно.

Внутренние соединения

Внутреннее соединение по эквивалентности можно выполнить с помощью операции join. Как это принято при внутреннем соединении, любые записи во внешнем результирующем наборе исключаются, если не существуют связанные с ними записи во внутреннем результирующем наборе. Ниже приведен пример:

// Используйте свою строку подключения
            Northwind db = new Northwind(@"Data Source=MICROSOF-1EA29E\SQLEXPRESS;
                                           AttachDbFilename=C:\Northwind.mdf;
                                           Integrated Security=True");

            var entities = from s in db.Suppliers
                           join c in db.Customers on s.City equals c.City
                           select new
                           {
                               SupplierName = s.CompanyName,
                               CustomerName = c.CompanyName,
                               City = c.City
                           };

            string str = "Поставщики \n";
            foreach (var e in entities)
                str += "\n" + e.City + ": " + e.SupplierName + " - " + e.CustomerName;

            MessageBox.Show(str);

В данном коде выполняется внутреннее соединение поставщиков и потребителей (заказчиков). Если запись о заказчике из того же города, что и у поставщика, не существует, то запись о поставщике будет исключена из результирующего набора. Ниже показан результат работы этого кода:

Выполнение внутреннего соединения

Как видите, несмотря на тот факт что некоторым поставщикам в выводе соответствует по нескольку заказчиков, ряд поставщиков вообще в списке отсутствует. Причина в том, что не найдено поставщиков в тех городах, откуда эти пропущенные заказчики. Если нужно видеть поставщиков независимо от того, есть ли в том же городе заказчики или нет; то понадобится внешнее соединение.

Внешние соединения

В одной из статей по LINQ to Objects при рассмотрении стандартной операции запроса DefaultIfEmpty упоминалось, что эта операция может быть использована для выполнения соединений. В примере ниже конструкция into применяется для направления соответствующих результатов join во временную последовательность, на которой затем вызываются операции DefaultIfEmpty. Таким образом, если запись опущена в результате соединения, то будет подставлено значение по умолчанию. Чтобы можно было увидеть сгенерированный оператор SQL, используется средство протоколирования DataContext:

// Используйте свою строку подключения
            Northwind db = new Northwind(@"Data Source=MICROSOF-1EA29E\SQLEXPRESS;
                                           AttachDbFilename=C:\Northwind.mdf;
                                           Integrated Security=True");

            // Используем протоколирование
            TextBoxWriter txb = new TextBoxWriter(txt_log);
            db.Log = txb;

            var entities = from s in db.Suppliers
                           join c in db.Customers on s.City equals c.City into temp
                           from t in temp.DefaultIfEmpty()
                           select new
                           {
                               SupplierName = s.CompanyName,
                               CustomerName = t.CompanyName,
                               City = s.City
                           };

            string str = "Поставщики \n";
            foreach (var e in entities)
                str += "\n" + e.City + ": " + e.SupplierName + " - " + e.CustomerName;

            MessageBox.Show(str);

Обратите внимание, что в конструкции join результаты соединения направлены во временную последовательность по имени temp. Имя временной последовательности может быть любым до тех пор, пока оно не конфликтует ни с одним другим именем или ключевым словом.

Затем выполняется последующий запрос результатов на последовательности temp, передавая ее операции DefaultIfEmpty. Хотя она еще не рассматривалась, но операция DefaultIfEmpty, вызванная в этом коде — это не та же самая операция, о которой шла речь ранее. Запросы LINQ to SQL транслируются в операторы SQL, и эти операторы SQL выполняются базой данных. SQL Server не имеет никакой возможности вызвать стандартную операцию запроса DefaultEmpty. Вместо этого вызов операции транслируется в соответствующий оператор SQL. Вот почему мне понадобилось включить средство протоколирования DataContext.

К тому же доступ к названию города производится через таблицу Suppliers, а не коллекцию temp. Это сделано потому, что известно, что всегда должна существовать запись о поставщике, но для поставщиков, не имеющих соответствующих заказчиков, не будет значения города в присоединенном результате из коллекции temp. Это отличается от предыдущего примера внутреннего соединения, где был получен город из присоединенной таблицы. В том примере не имело значения, из какой таблицы брать город, потому что если соответствующий заказчик не был найдено, то не было бы и соответствующей результирующей записи, поскольку соединение было внутренним.

Взглянем на результат работы этого примера:

Выполнение внешнего соединения

В этом выводе видно, что получается как минимум одна запись для каждого поставщика, причем некоторые поставщики не имеют соответствующего заказчика, поскольку выполнено внешнее соединение. Но если возникнут какие-то сомнения, можете взглянуть на сгенерированный оператор SQL, который ясно указывает на выполнение внешнего соединения.

В примерах, которые представлены выше, результаты запроса проектируются на плоскую структуру. Под этим понимается объект анонимного класса, в котором каждое запрошенное поле становится членом этого класса. Вместо создания единственного анонимного класса, содержащего все нужные поля, можно было бы создать анонимный класс, состоящий из объекта Supplier и соответствующего ему объекта Customer. В этом случае существовал бы верхний уровень анонимного класса и нижний уровень, состоящий из объекта Supplier и либо соответствующего ему объекта Customer, либо объекта по умолчанию, предоставленного операцией DefaultIfEmpty, который был бы равен null.Б.

Если принять "плоский" подход, как делалось в предыдущих примерах из-за того, что спроектированный выходной класс не был сущностным классом, то не будет возможности выполнять обновления выходных объектов через объект DataContext, который управляет хранением изменений в базе данных. Это хорошо для данных, которые не подвергаются изменениям. Однако иногда может понадобиться возможность изменения извлеченных объектов с сохранением изменений через DataContext.

Давайте взглянем на следующий пример, который содержит "не плоский" код:

// Используйте свою строку подключения
            Northwind db = new Northwind(@"Data Source=MICROSOF-1EA29E\SQLEXPRESS;
                                           AttachDbFilename=C:\Northwind.mdf;
                                           Integrated Security=True");

            var entities = from s in db.Suppliers
                           join c in db.Customers on s.City equals c.City into temp
                           from t in temp.DefaultIfEmpty()
                           select new
                           { s, t};

            string str = "Поставщики \n";
            foreach (var e in entities)
                str += String.Format("\n{0}: {1} - {2}",e.s.City, e.s.CompanyName,e.t != null ? e.t.CompanyName : "");

            MessageBox.Show(str);

В этом коде вместо возврата результатов запроса в плоский анонимный объект, состоящий из всех нужных полей, результаты запроса возвращаются в анонимном объекте, состоящем из объекта Supplier и, потенциально, из объекта Customer. Также обратите внимание, что в вызове метода Console.WriteLine учитывается, что временный результат может быть null, если нет соответствующего объекта Customer. Вывод получается анологичный предыдущему.

Пройди тесты
Лучший чат для C# программистов