Entity Framework FAQ

Понимание моделирования сущностей, отображение таких моделей на реляционные базы данных, а также проектирование сущностных моделей данных (Entity Data Model, EDM) являются первыми шагами к пониманию Entity Framework. Я начну свою статью с ответов на вопросы об основах Entity Framework, в том числе о классе ObjectContext, а затем отвечу на вопросы о том, когда и где стоит использовать Entity Client с Entity SQL. Кроме того, я планирую объяснить разницу между EntityClient и службами Object Services, а также последствия использования запросов LINQ и Entity SQL вместе с этими службами.

Важной частью Entity Framework является анализ запросов, как созданных в коде для платформы Microsoft® .NET Framework, так и на языке SQL, поэтому явная и упреждающая загрузка в статье будет рассмотрена на примере создаваемых запросов. Все примеры кода и образец базы данных NorthwindEF вместе с самой этой статьей доступны для загрузки на веб-узле журнала MSDN Magazine.

Зачем использовать запросы на Entity SQL, если сущности можно получить запросами LINQ?

На каждой презентации, посвященной использованию Entity SQL с EntityClient или службами Object Services, кто-нибудь задает этот вопрос. (И я не могу их осуждать, потому что это один из первых вопросов, которые я задавал сам себе, начав освоение Entity Framework!) Строгий контроль типов и синтаксис запросов LINQ настолько притягательны, что разработчики могут только удивляться, зачем им нужен новый язык для взаимодействия с сущностями.

Чтобы ответить на этот вопрос наиболее полно, я хотел бы прежде всего обсудить три главных приема работы с EDM.

  • Составление запросов Entity SQL с помощью поставщика EntityClient
  • Составление запросов Entity SQL с помощью служб Object Services
  • Составление запросов LINQ с помощью служб Object Services

У всех этих методов есть общие характеристики, например прямое или косвенное использование поставщика EntityClient. Однако различаются как результаты применения данных приемов, так и способы получения этих результатов.

Поставщик EntityClient предоставляет набор объектов, который должен быть знаком тем, кто знает объектную модель ADO.NET. Для соединения с EDM используется EntityConnection, для выдачи запросов к EDM используется EntityCommand, а результаты запроса возвращаются с помощью класса DbDataReader. Используется ли EntityClient напрямую или опосредованно, через службы Object Services, в конце концов с его помощью отправляется запрос и возвращается результат.

Так почему же используется Entity SQL, если есть LINQ? Ответ заключается в преимуществах и недостатках обоих методов.

EntityClient и Entity SQL

Код, написанный с помощью EntityClient API, из всех трех методов обеспечивает наиболее точный контроль. Можно создать EntityConnection для соединения с EDM, написать запрос на Entity SQL и выполнить его с помощью EntityCommand, а потом вернуть результат через DbDataReader. За счет отсутствия некоторых полезных особенностей синтаксиса, имеющихся в LINQ и службах Object Services, этот способ является также менее ресурсоемким.

Самое большое преимущество Entity SQL — гибкость. Его основанный на строках синтаксис предполагает легкое построение динамических запросов. Это очень полезно для создания оперативных запросов.

Однако другой стороной гибкости и легкости является то, что данные можно вернуть только через DbDataReader. С помощью EntityClient с Entity SQL нельзя возвращать настоящие сущности. Для извлечения данных и обхода коллекции записей, удовлетворяющих запросу Entity SQL, используется класс DbDataReader. Из кода на рис. 1 видно, что через DbDataReader можно перебирать только записи о клиентах, а не сущности Клиент.

Рис. 1 Обход по строками через DbDataReader

string city = "London";
using (EntityConnection cn = new EntityConnection("Name=Entities"))
{
  cn.Open();
  EntityCommand cmd = cn.CreateCommand();
  cmd.CommandText = @"SELECT VALUE c FROM Entities.Customers AS c WHERE 
  c.Address.City = @city";
  cmd.Parameters.AddWithValue("city", city);
  DbDataReader rdr = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
  while (rdr.Read())
  Console.WriteLine(rdr["CompanyName"].ToString());
  rdr.Close();
}

Вот подсказка: в отладочном режиме можно получить такое сообщение об ошибке, в котором говорится, что содержимое колонки под номером X нельзя прочитать. Такая ошибка возникает только в режиме отладки и ее можно избежать, закрыв в режиме отладки окно «autos». Это известная ошибка третьего выпуска тестовой версии Entity Framework.

Сопоставимого языка манипулирования данными (ЯМД) для Entity SQL пока нет. Это значит, что операторы Insert, Update или Delete нельзя применять к EDM напрямую (см. рис. 2).

Рис. 2 Entity Framework. API

EntityClient и Entity SQLСлужбы Object Services и Entity SQLСлужбы Object Services и LINQ
Напрямую к поставщику EntityClientДаНетНет
Хорош для оперативных запросовДаДаНет
Может выдавать DML напрямуюНетНетНет
Строго типизированНетНетДа
Может возвращать сущности как результатНетДаДа

Службы Object Services и Entity SQL

Следующий метод предполагает использовать службы Object Services для выполнения запросов Entity SQL. Этот метод отходит от прямого взаимодействия с поставщиком EntityClient (хотя с этим поставщиком происходит скрытый обмен данными). Чтобы выдавать запросы к EDM, используются ObjectContext и ObjectQuery<T>.

Этот прием так же хорош для выдачи оперативных запросов, как и первый. Однако, используя службы Object Services с Entity SQL, можно получать из EDM сущности, а не просто данные через DbDataReader. Получается добротное сочетание гибкости запросов и возврата настоящих сущностей.

Поскольку настоящее время конструкции ЯМД в Entity SQL отсутствуют, то выдавать запросы с командами вставки, модификации или удаления с помощью Entity SQL и служб Object Services нельзя. Однако рассматриваемый метод позволяет получить сущность из EDM, а затем модифицировать ее методом SaveChanges класса ObjectContext. В следующем коде приводится пример обхода коллекции сущностей «Заказчик»:

string city = "London";
using (Entities entities = new Entities()) 
{
  ObjectQuery<Customers> query = entities.CreateQuery<Customers>(
  "SELECT VALUE c FROM Customers AS c WHERE c.Address.City = @city",
  new ObjectParameter("city", city)
  );

  foreach (Customers c in query)
  Console.WriteLine(c.CompanyName);

Службы Object Services и LINQ

Использование служб Object Services с LINQ не так удобно для составления оперативных запросов, как другие методы. Следующий код возвращает коллекцию сущностей «Заказчик» из EDM:

string city = "London";
using (Entities entities = new Entities()) 
{
  var query = from c in entities.Customers
  where c.Address.City == city
  select c;

  foreach (Customers c in query)
? Console.WriteLine(c.CompanyName);

Как и Entity SQL, LINQ не поддерживает операторы ЯМД напрямую. Пока модифицировать сущности в базе данных можно только с помощью служб Object Services (используя метод SaveChanges), что осуществляется возвратом сущностей из EDM, изменения которых отслеживает Entity Framework. Короче говоря, ни LINQ, ни Entity SQL не выполняют операций модификации; только объект класса ObjectContext может их выполнять.

Сводная таблица различий разных методов приведена на рис. 2. Так почему же может потребоваться использовать Entity SQL, когда уже есть LINQ? Entity SQL — это хороший выбор в случаях, когда нужно составить или оперативный, или более гибкий, чем позволяет LINQ, запрос. В других случаях я предлагаю использовать LINQ со службами Object Services, тем самым сочетая преимущества строгого контроля типов и возможности возвращать сущности и проекции.

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

Какова роль объектов класса ObjectContext?

ObjectContext является шлюзом к EntityConnection для служб Object Services. Он предоставляет доступ к EDM через нижележащий EntityConnection. Например, можно получить доступ к сущностям через ObjectContext, из ObjectContext добыть информацию о состоянии объектов, а потом методом CreateQuery создать ObjectQuery<T>.

Кроме того, ObjectContext предоставляет объектам способ получать информацию об обновлениях записей базы данных. Например, с помощью методов ObjectContext можно добавлять сущности к ObjectContext, удалять их, манипулировать ими и, в конце концов, сохранить все изменения сущностей в базу данных (методом SaveChanges).

Как работает в Entity Framework явная и упреждающая загрузка?

Явная загрузка — это поведение LINQ по умолчанию для Entities и Entity Framework. Когда в Entity Framework исполняется запрос, полностью доступны только сущности, возвращаемые запросом, а связанные с ними сущности не загружаются. Например, запрос, который извлекает из EDM все заказы, выберет все записи с заказами и вернет коллекцию сущностей «Заказ». Однако запись о заказчике, связанная с заказом, этим запросом извлечена не будет, вследствие чего соответствующая этому заказу сущность «Заказчик» не будет загружена. Таким образом, в приведенном образце кода при попытке обратиться к заказчику заказа будет выдано исключение, потому что сущность «Заказчик» не будет загружена:

using (Entities entities = new Entities())
{
  var query = (from o in entities.Orders
  where o.Customers.CustomerID == "ALFKI"
  select o).First<Orders>();

  Orders order = query as Orders;
  Console.WriteLine(order.OrderID);

  Console.WriteLine(order.Customers.CompanyName);

В Entity Framework для каждого экземпляра класса EntityReference есть метод Load. Этот метод позволяет явно загрузить коллекцию, связанную с другой сущностью. Например, в предыдущем коде можно сообщить Entity Framework, что записи заказчиков для заказа необходимо загружать. Измененный код, осуществляющий явную загрузку, показан на рис. 3. Прежде всего в нем проверяется, загружены ли сущности «Заказчик». Если нет, они загружаются для данного заказа. Такой прием называется явной загрузкой.

Рис. 5 Использование ToTraceString

string city = "London";

using (Entities entities = new Entities())
{
  ObjectQuery<Customers> query = entities.CreateQuery<Customers>(
  "SELECT VALUE c FROM Customers AS c WHERE c.City = @city",
  new ObjectParameter("city", city)
  );
  entities.Connection.Open();

  Console.WriteLine(query.ToTraceString());

  foreach (Customers c in query)
  Console.WriteLine(c.CompanyName);
}

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

Рис. 3 Явная загрузка

using (Entities entities = new Entities())
{
  var query = (from o in entities.Orders
  where o.Customers.CustomerID == "ALFKI"
  select o);

  foreach (Orders order in query)
  {
  if (!order.CustomersReference.IsLoaded)
  order.CustomersReference.Load();
  Console.WriteLine(order.OrderID + " --- " + 
  order.Customers.CompanyName);
  }
}

Некоторые дополнительные идеи по этой теме от группы разработчиков Entity Framework можно посмотреть на врезке «Дополнительная информация: загрузка данных в Entity Framework

На рис. 4 показан прием, называемый упреждающей загрузкой. Метод Include, применяемый к сущности «Заказ» в LINQ запросе, принимает аргумент, который указывает запросу извлечь данные не только для заказа, но и для связанного с этим заказом заказчика. Этот метод позволяет создать один оператор SQL, который загрузит все данные о заказах и заказчиках, удовлетворяющие условиям запроса LINQ.

Рис. 4 Упреждающая загрузка.

using (Entities entities = new Entities())
{
  var query = (from o in entities.Orders.Include("Customers")
  where o.ShipCountry == "USA"
  select o);

  foreach (Orders order in query)
  Console.WriteLine(order.OrderID + " --- " + 
  order.Customers.CompanyName);
}

Нужно понимать, насколько важно учитывать тип загрузки. При использовании метода Load (как показано на рис. 3) для явной загрузки сущностей во время обхода коллекции, к базе данных будет направлено несколько запросов — по одному на каждый вызов метода Load.

Этот метод отлично подходит, если требуется только один – два раза получить доступ к данным. Однако упреждающая загрузка с использованием метода Include (как показано на рис. 4) намного больше подходит в ситуациях, когда доступ к некоторой сущности и к сущностям, связанным с ней, требуется постоянно.

Лучший способ выяснить, какой способ подходит больше, — профилировать производительность в разных вариантах. Как и в большинстве случаев, решение о том, использовать упреждающую или явную загрузку, зависит от ситуации.

Как увидеть подготовленный к выполнению запрос SQL?

Мне часто хотелось посмотреть, как выглядит исполняемый SQL, соответствующий запросу, сделанному с помощью ObjectQuery, и я обнаружил два полезных приема. Первый — это использовать профилировщик для SQL Server® (или любое другое доступное вам средство баз данных профилирования). Второй прием — использовать метод класса ObjectQuery, который называется ToTraceString.

На рис.5 показано, как вызвать метод ToTraceString для ObjectQuery<T>. Обратите внимание, что соединение ObjectContext открыто (EntityConnection). Метод ToTraceString требует открытого соединения, чтобы можно было определить исполняемый запрос. Метод ToTraceString доступен как для класса ObjectQuery<T>, так и для EntityCommand.

Что можно сделать со сложными типами данных?

В Entity Framework есть структура данных, называемая сложным типом, которая служит для представления набора свойств, тесно связанных между собой. Рассмотрим тип «адрес». Сложный тип можно использовать для создания типа «адрес» для заказчика, так что связанные с адресом свойства сущности «заказчик» (город, область и номер телефона) окажутся внутри этого типа, а не на уровне типа «заказчик». С помощью сложных типов осуществляется логическая группировка похожих скалярных свойств, что упрощает поиск тесно связанных свойств сущности и сохраняет эту логическую группировку в EDM. Как и сущности, комплексные типы содержат скалярные свойства, однако в отличие от свойства у них нет идентификатора (значений ключа) и их нельзя сохранять в базу данных через ObjectContext. Комплексные типы всего лишь стороны сущностей, но не сами сущности. Это хороший инструмент для группировки логически связанных свойств сущности.

Как создать сложный тип?

Поскольку построитель EDM пока не поддерживает визуальное создание сложных типов, их приходится создавать, редактируя файл EDMX в редакторе XML. Прежде всего, надо создать сложный тип в языке определения концептуальных схем (CSDL) На рис.6 показано представление типа «Заказчик» в CSDL вместе с вновь созданным сложным типом адреса. Обратите внимание, что в записях о заказчиках находятся поля адреса, города и области, а все остальные свойства, связанные с адресом, удалены и заменены свойством «О». Новое свойство адрес имеет сложный тип AddressType.

Рис. 6 Создание ComplexType

<EntityType Name="Customers">
  <Key>
  <PropertyRef Name="CustomerID" />
  </Key>
  <Property Name="CustomerID" Type="String" Nullable="false" 
  MaxLength="5" FixedLength="true" />
  <Property Name="CompanyName" Type="String" Nullable="false" 
  MaxLength="40" />
  <Property Name="ContactName" Type="String" MaxLength="30" />
  <Property Name="ContactTitle" Type="String" MaxLength="30" />
  <Property Name="Address" Type="Self.AddressType"  
  Nullable="false"/>
  <NavigationProperty Name="Orders" 
  Relationship="NorthwindEFModel.FK_Orders_Customers" 
  FromRole="Customers" ToRole="Orders" />
</EntityType>
<ComplexType Name="AddressType">
  <Property Name="Address" Type="String" MaxLength="60" />
  <Property Name="City" Type="String" MaxLength="15" />
  <Property Name="Region" Type="String" MaxLength="15" />
  <Property Name="PostalCode" Type="String" MaxLength="10" />
  <Property Name="Country" Type="String" MaxLength="15" />
  <Property Name="Phone" Type="String" MaxLength="24" />
  <Property Name="Fax" Type="String" MaxLength="24" />
</ComplexType>

Следующим шагом является отображение в язык спецификации отображений (MSL), чтобы новый сложный тип был учтен. На рис.7 показано отображение типа «заказчик» вместе с вновь созданным сложным типом адреса. После сборки проекта к полям сложного типа можно будет обращаться в коде. Например, следующим LINQ-запросом можно получить список заказов только для лондонских заказчиков:

var query = from c in entities.Customers
  where c.Address.City == "London"
  select c;

Выводы

В выпуске этого месяца я сравнил использование EntityClient и служб Object Services с Entity SQL и LINQ. Я также коснулся того, как и зачем создавать сложные типы и продемонстрировал, как работает упреждающая и явная загрузка. Я получил так много вопросов от разработчиков, интересующихся Entity Framework, что намереваюсь обсудить похожие темы и дать практические советы в будущих статьях в Data Points.

Джон Папа (John Papa) — старший консультант по технологии .NET в компании ASPSOFT (aspsoft.com) и страстный поклонник бейсбола, который почти все летние вечера проводит, болея за «Янки» со своим семейством и верным псом Кади. Джон обладает званием MVP по C#, является одним из докладчиков всемирной группы поддержки пользователей .NET INETA, является автором нескольких книг по ADO, XML и SQL Server.