Проекции на сущностные классы

41

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

При выполнении запросов LINQ to SQL есть два выбора для проекции возвращенных результатов. Результаты можно спроецировать на сущностный класс или на несущностный класс, который может быть именованным или анонимным. Между проецированием на сущностный и несущностный класс имеется существенное отличие.

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

Ниже приведен пример запроса, проецируемого на сущностный класс:

IEnumerable<Customer> custs = from c in db.Customers select c;

После этого запроса можно было бы вносить изменения в любой из сущностных объектов Customer в последовательности custs и сохранять их вызовом метода SubmitChanges. Далее показан пример запроса, проецируемого на несущностный класс:

var custs = from c in db.Customers
                         select new { Id = c.CustomerID, Name = c.ContactName };

Выполнив проецирование результата на анонимный класс, сохранить никакие внесенные изменения в каждый объект последовательности custs вызовом метода SubmitChanges не получится.

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

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

            var cusorders = from o in db.Orders
                            where o.Customer.CustomerID == "CONSH"
                            orderby o.ShippedDate descending
                            select new { Customer = o.Customer, Order = o };

            // Захватить первый заказ
            Order firstOrder = cusorders.First().Order;

            // Теперь сохранить страну поставки первого заказа, 
            // чтобы можно было восстановить ее позже
            string shipCountry = firstOrder.ShipCountry;
            Console.WriteLine("Заказ первоначально поставлялся в {0}", shipCountry);

            // Теперь изменить страну поставки с Великобритании на США
            firstOrder.ShipCountry = "USA";
            db.SubmitChanges();

            // Запрос для проверки того, была ли страна на самом деле изменена
            string country = (from o in db.Orders
                              where o.Customer.CustomerID == "CONSH"
                              orderby o.ShippedDate descending
                              select o.ShipCountry).FirstOrDefault<string>();

            Console.WriteLine("Заказ теперь поставляется в {0}", country);

            // Восстановить состояние базы данных, чтобы пример можно было запустить снова
            firstOrder.ShipCountry = shipCountry;
            db.SubmitChanges();

В этом коде запрашиваются заказы, поступившие от заказчика "CONSH". Возвращенные заказы проецируются на анонимный тип, содержащий Customer и каждый Order. Сам анонимный класс не принимает служб DataContext, таких как отслеживание идентичности, отслеживание и обработка изменений, но его компоненты типа Customer и Order делают это, будучи сущностными классами.

Затем на результатах предыдущего выполняется другой запрос, чтобы получить первый Order. После этого сохраняется копия исходного свойства ShipCountry объекта Order, чтобы можно было восстановить его в конце примера, и это значение выводится на консоль.

Затем ShipCountry в Order изменяется с сохранением изменения с помощью метода SubmitChanges. После этого ShipCountry для этого заказа снова запрашивается из базы данных и выводится на консоль, просто чтобы продемонстрировать, что в базе данных действительно произошло изменение. Это доказывает правильность работы метода SubmitChanges, а также тот факт, что компоненты сущностного класса анонимного типа получают в свое распоряжение службы объекта DataContext. Далее ShipCountry сбрасывается в исходное значение и сохраняется, чтобы пример можно было запустить опять, и никакие последующие примеры не пострадали.

Вот результат запуска:

Проецирование на несущностный класс, содержащий сущностные классы

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

При проецировании отдавайте предпочтение инициализации объектов перед параметризацией конструирования

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

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

            db.Log = Console.Out;

            var contacts = from c in db.Customers
                           where c.City == "Buenos Aires"
                           select new { Name = c.ContactName, Phone = c.Phone } into co
                           orderby co.Name
                           select co;

            foreach (var contact in contacts)
            {
                Console.WriteLine("{0} - {1}", contact.Name, contact.Phone);
            }

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

Проецирование с использованием инициализации объектов

Здесь даже не интересует вывод результатов запроса. В действительности необходимо увидеть сгенерированный запрос SQL. Поэтому возникает вопрос, а зачем тогда нужен цикл foreach? Дело в том, что из-за отложенного выполнения без foreach запрос вообще бы не выполнился.

Существенная для обсуждения часть запроса LINQ to SQL заключена в конструкциях select и orderby. Запрос LINQ to SQL инструктируется о необходимости создания члена по имени Name в анонимном классе, который заполняется полем ContactName из таблицы Customers.

Затем запросу указывается отсортировать результат по члену Name анонимного объекта, на который выполняется проекция. Объект DataContext содержит всю переданную ему информацию. Инициализация объекта эффективно отображает исходное поле ContactName из класса Customer на целевое поле Name анонимного класса, и объект DataContext отвечает за это отображение. На основании этой информации он способен понять, что на самом деле Customers сортируется по полю ContactName, поэтому он может сгенерировать запрос SQL, который сделает это.

Теперь рассмотрим проекцию на именованный класс. Для этого сначала создадим класс CustomerContact:

class CustomerContact
    {
        public string Name;
        public string Phone;

        public CustomerContact(string name, string phone)
        {
            Name = name;
            Phone = phone;
        }
    }

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

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

            db.Log = Console.Out;

            var contacts = from c in db.Customers
                           where c.City == "Buenos Aires"
                           select new CustomerContact(c.ContactName, c.Phone) into co
                           orderby co.Name
                           select co;
            
            try
            {
                foreach (var contact in contacts)
                    Console.WriteLine("{0} - {1}", contact.Name, contact.Phone);
            }
            catch (Exception ex)
            {
                Console.WriteLine("\n  " + ex.Message);
            }

В данном примере вместо проецирования на анонимный класс осуществляется проекция на класс CustomerContact. А вместо инициализации созданных объектов применяется параметризованный конструктор. При запуске этого примера будет сгенерировано исключение:

Проецирование с использованием параметризованного конструирования

Так что же произошло? Взглянув на предшествующий запрос LINQ to SQL может возникнуть вопрос: откуда DataContext знает, какое поле класса Customer отображается на член CustomerContact.Name, которому предпринимается попытка выполнить упорядочивание? Ответ - никак, отсюда и проблемы.

Однако использовать параметризованное конструирование вполне безопасно до тех пор, пока ничего в запросе проекции не ссылается на члены именованного класса:

...
var contacts = from c in db.Customers
                           where c.City == "Buenos Aires"
                           select new CustomerContact(c.ContactName, c.Phone);

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

Проецирование с использованием параметризованного конструирования без ссылок на члены

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

Из-за этих сложностей рекомендуется, где это возможно, вместо параметризованных конструкций применять инициализацию объектов!

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