Соединения
80LINQ --- LINQ to DataSet и SQL --- Соединения
»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ
Многие отношения в базе данных определены как ассоциации, и можно получить доступ к ассоциированным объектам, просто обращаясь к членам класса. Однако отображаться подобным образом могут только те отношения, которые определены с использованием внешних ключей. Поскольку не каждый тип отношения определен с применением внешних ключей, иногда приходится соединять таблицы явно.
Внутренние соединения
Внутреннее соединение по эквивалентности можно выполнить с помощью операции 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. Вывод получается анологичный предыдущему.