Ассоциации

46

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

Запрос ассоциированного класса в LINQ to SQL не сложнее простого обращения к переменной-члену сущностного класса. Это объясняется тем, что ассоциированный класс является переменной-членом связанного сущностного класса или хранится в коллекции объектов сущностных классов, причем коллекция является переменной-членом связанного сущностного класса.

Если ассоциированный класс представляет сторону многие в отношении "один ко многим", то объекты класса многие будут храниться в коллекции, тип которой — EntitySet<T>, а T - тип сущностного класса стороны многие. Эта коллекция будет членом класса стороны один, а ссылка на объект класса один станет членом класса стороны многие.

Например, рассмотрим случай сущностных классов Customer и Order, которые были сгенерированы для базы данных Northwind. Заказчик может иметь множество заказов, но заказ принадлежит только одному заказчику. В данном примере класс Customer находится на стороне один отношения "один ко многим" между сущностными классами Customer и Order. Класс Order находится на стороне многие того же отношения. Поэтому заказы, принадлежащие объекту Customer, могут быть доступны через переменную-член, обычно именованную Orders, типа EntitySet<Order> в классе Customer. Заказчик в объекте Order доступен через переменную-член, обычно именуемую Customer, типа EntityRef<Customer> в классе Order:

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

Ниже приведен пример, в котором запрашиваются определенные заказчики и затем отображаются вместе со всеми их заказами:

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

            IQueryable<Customer> custs = from c in db.Customers
                                         where c.Country == "UK" &&
                                               c.City == "London"
                                         orderby c.CustomerID
                                         select c;

            string s = "";
            foreach (Customer cust in custs)
            {
                s += "\n" + cust.CompanyName + " - " + cust.ContactName + "\n";
                foreach (Order order in cust.Orders)
                    s += "\n  " + order.OrderID + " " + order.OrderDate;
                s += "\n";
            }
            MessageBox.Show("Заказы из Лондона: \n" + s);

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

Использование ассоциации для доступа к связанным данным

Здесь может показаться, что это страшно неэффективно, если нигде нет обращения к заказам данного заказчика.

В действительности это не так. Причина в том, что заказы в действительности не извлекаются до тех пор, пока на них никто не ссылается. Если бы код не обращался к свойству Orders заказчика, они бы никогда и не извлекались. Это известно под названием отложенной загрузки, которую не следует путать с отложенным выполнением запроса.

Отложенная загрузка

Отложенная загрузка — это такая загрузка, при которой записи на самом деле не загружаются из базы данных до тех пор, пока это не становится абсолютно необходимым, что происходит при первом обращении к ним; таким образом, загрузка записей откладывается.

В предыдущем примере, если бы не было ни одной ссылки на переменную-член Orders, то заказы никогда бы не извлекались из базы данных. Для большинства ситуаций отложенная загрузка — вещь хорошая. Она предотвращает выполнение ненужных запросов и экономит пропускную способность сети за счет исключения пересылки ненужных данных.

Однако может возникнуть проблема. Ниже показан такой же код, как в предыдущем примере, за исключением того, что на этот раз включено средство протоколирования, обеспечиваемое объектом DataContext.Log, чтобы выявить проблему:

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

            IQueryable<Customer> custs = from c in db.Customers
                                         where c.Country == "UK" &&
                                               c.City == "London"
                                         orderby c.CustomerID
                                         select c;

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

            string s = "";
            foreach (Customer cust in custs)
            {
                s += "\n" + cust.CompanyName + " - " + cust.ContactName + "\n";
                foreach (Order order in cust.Orders)
                    s += "\n  " + order.OrderID + " " + order.OrderDate;
                s += "\n";
            }
            MessageBox.Show("Заказы из Лондона: \n" + s);

В результате запуска этого примера получается следующий вывод:

Пример, демонстрирующий отложенную загрузку

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

Как видите, для извлечения заказов каждого заказчика выполняется отдельный запрос. Заказы не запрашиваются, а потому и не загружаются — до тех пор, пока не осуществляется обращение к переменной Orders EntityRef<T> во втором цикле foreach, который следует немедленно после отображения информации о заказчике. Так как заказы не извлекаются до тех пор, пока к ним не будет выполнено обращение, их загрузка откладывается.

Поскольку для каждого заказчика генерируется и выполняется отдельный запрос, потенциально в базу данных поступает множество SQL-запросов. Это может привести к проблемам с производительностью.

В коде точно известно, что планируется обращение к заказам извлеченных заказчиков. В данном случае можно было бы добиться лучшей производительности, если бы удалось извлекать заказы вместе с извлечением заказчиков. То, что необходимо, называется немедленной загрузкой.

Немедленная загрузка с помощью класса DataLoadOptions

Хотя отложенная загрузка является поведением по умолчанию для ассоциированных классов, можно инициировать их немедленную загрузку. Немедленная загрузка заставляет ассоциированные классы загружаться до того, как к ним выполнится обращение. Это может дать выигрыш в производительности. С помощью операции LoadWidth<T> класса DataLoadOptions можно заставить DataContext немедленно загрузить ассоциированный класс, указанный в лямбда-выражении этой операции. Благодаря операции LoadWidth<T>, когда происходит действительное выполнение запроса, то при этом извлекается не только первичный класс, но также и ассоциированный с ним класс.

В примере ниже используется тот же базовый код, но здесь создается объект DataLoadOptions; на этом объекте вызывается операция LoadWith<T> с передачей члена Orders как класса, подлежащего немедленной загрузке вместе с объектом Customer; и объект DataLoadOptions присваивается объекту Northwind типа DataContext. Кроме того, чтобы исключить любые сомнения относительного того, что ассоциированные объекты — заказы загрузятся до того, как к ним произойдет обращение, код, выполняющий перечисление заказов текущего заказчика, опущен, чтобы на них не было никаких ссылок:

Northwind db = new Northwind(@"Data Source=MICROSOF-1EA29E\SQLEXPRESS;
                                           AttachDbFilename=C:\Northwind.mdf;
                                           Integrated Security=True");

            DataLoadOptions dlo = new DataLoadOptions();
            dlo.LoadWith<Customer>(c => c.Orders);

            IQueryable<Customer> custs = from c in db.Customers
                                         where c.Country == "UK" &&
                                               c.City == "London"
                                         orderby c.CustomerID
                                         select c;

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

            string s = "";
            foreach (Customer cust in custs)
            {
                s += "\n" + cust.CompanyName + " - " + cust.ContactName + "\n";
                /*foreach (Order order in cust.Orders)
                    s += "\n  " + order.OrderID + " " + order.OrderDate;
                s += "\n";*/
            }
            MessageBox.Show("Заказы из Лондона: \n" + s);

Единственное отличие этого кода от предыдущего состоит в создании экземпляра объекта DataLoadOptions, вызове операции LoadWith<T>, присваивании объекта DataLoadOptions объекту Northwind типа DataContext и исключении ссылок на заказы каждого заказчика. В вызове операции LoadWith<T> объект DataLoadOptions инструктируется на немедленную загрузку Orders при загрузке объекта Customer. Теперь посмотрим на вывод программы:

Пример, демонстрирующий немедленную загрузку с использованием класса DataLoadOptions

Как видите, был выполнен единственный запрос SQL для извлечения заказчиков, отвечающих условию конструкции where.Несмотря на то что нигде нет обращений к заказам данного заказчика, этот единственный запрос соединяет каждого заказчика с его заказами. Поскольку заказы загружаются до обращения к ним, их загрузка не откладывается и выполняется немедленно.

Вместо множества запросов SQL, количество которых равно одному (запрос заказчиков), плюс число извлеченных заказчиков (для заказов каждого заказчика), здесь мы имеем дело с одним SQL-запросом. Если заказчиков достаточно много, разница в производительности будет огромной.

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

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