Кое-что из моих предпочтений в Entity Framework 4.2 DbContext

OSzone.net » Microsoft » Разработка приложений » .NET Framework » Кое-что из моих предпочтений в Entity Framework 4.2 DbContext
Автор: Джули Лерман
Иcточник: MSDN Magazine
Опубликована: 19.06.2012
Даже до выпуска Entity Framework (EF) 4.1 в начале 2011 года разработчики в основном использовали не более половины того, что есть в EF: Code First. Code First позволяет выражать модель данных сущностей (Entity Data Model), используя классы предметной области и конфигурации Code First, — это отличная альтернатива для тех, кто не хочет использовать визуальный дизайнер для определения модели. Но каждый бит образцов кода, которые демонстрируют применение EF с этими классами и моделями, определенными на основе Code First, «приводится в движение» другим очень важным компонентом, появившимся в EF 4.1: классом DbContext.

ObjectContext является частью базового EF API в Microsoft .NET Framework 4, а также классом, который позволяет выполнять запросы, изменять параметры отслеживания и обновлять базу данных с применением строго типизированных классов, представляющих модель. Класс DbContext лучше всего описывать как оболочку вокруг ObjectContext, которая обеспечивает наиболее часто используемый функционал ObjectContext. Кроме того, эта оболочка предоставляет некоторые более простые способы выполнения распространенных задач, которые весьма сложно кодировать напрямую с помощью ObjectContext.

Мой совет и рекомендация Microsoft: приступая к новым проектам с применением EF, следует прежде всего подумать об использовании DbContext. Если вы обнаружите, что вам приходится время от времени обращаться к какой-то более тонкой логике, предоставляемой классом ObjectContext, то из экземпляра DbContext можно получить его нижележащий ObjectContext:

var objectContext = (myDbContextInstance
  as IObjectContextAdapter).ObjectContext

Если вы знаете, что ваша работа потребует частого использования средств ObjectContext напрямую, то, вероятно, вы предпочтете использовать этот класс, а не DbContext. Но в целом, группа EF рекомендует избегать прямого использования ObjectContext, кроме тех случаев, когда по каким-то причинам вы действительно не можете задействовать DbContext.

Добавлю, что эта рекомендация распространяется на новые проекты. При работе с DbContext API вы получаете не только более удобный и изящный класс DbContext, но и равным образом улучшенные классы DbSet и DbQuery (эквиваленты ObjectSet и ObjectQuery).

Будучи сторонницей DbContext, я расскажу вам, какие из его средств стали моими любимыми.

DbSet.Find

Один из новых методов в этом API — DbSet.Find. Он помогает в работе с распространенным шаблоном для доступа к данным: извлечении единственного объекта по его основному ключу.

В случае ObjectContext вы должны были бы создать полный запрос, а затем выполнить его, используя такой LINQ-метод, как SingleOrDefault. Это выглядело бы примерно так:

var partyHatQuery = from p in context.PartyHats
  where p.Id == 3 select p;
var partyHatInstance = partyHatQuery.SingleOrDefault();

Вы могли бы написать то же самое более эффективно с помощью LINQ-методов и лямбда-выражения:

var partyHatInstance = context.PartyHats.SingleOrDefault(
  p => p.Id == 3);

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

Это как раз то, что группа EF сделала за вас в DbContext API. Когда вы работаете с DbContext, PartyHats превращается в DbSet<PartyHat>, и вы можете использовать метод DbSet.Find для быстрого выполнения того же запроса:

context.PartyHats.Find(3)

В этом методе предполагается, что предоставленное вами значение является значением ключа для класса, который вы ищете, — в данном случае для PartyHat. После этого EF выполнит за вас запрос SingleOrDefault, чтобы найти данные, где Id равен переданному значению, т. е. 3 в нашем примере. Естественно, вы можете передавать не само значение, а переменную.

Другое преимущество метода DbSet.Find заключается в том, что он дает возможности, недостижимые при запросе. Метод Find сначала просматривает память в поисках подходящего объекта, отслеживаемого контекстом. Если такой объект найден, EF не станет запрашивать базу данных. Это гораздо эффективнее, чем выполнять запрос к базе данных только для того, чтобы отбросить результаты этого запроса при наличии искомого экземпляра объекта в памяти. Тем самым вы избавляетесь от бесполезного обмена данными с базой данных, инициируемого многими разработчиками, даже не понимающими этого факта.

Вы также можете использовать DbSet.Find с составными ключами (composite keys). Сигнатура Find рассчитана на прием не только одного объекта, но и массива параметров. Следовательно, вы можете передать список значений, представляющих те значения, которые образуют ваш ключ.

DbSet.Local

При работе с EF мне часто требуется делать что-то с объектами, которые уже находятся в памяти и отслеживаются контекстом. Типичная места размещения этой логики — переопределенная версия SaveChanges или метод SavingChanges, где я выполняю некоторые проверки. (Благодаря новому Validation API, доступному наряду с DbContext, я сумела существенно сократить эту логику. Но в этой статье я не буду обсуждать Validation API.)

ObjectContext все же предоставляет способ для распознавания отслеживаемых объектов, но API-логику для этого нелегко найти и не так-то просто кодировать. В своей книге «Programming Entity Framework» (O’Reilly Media, 2010) я написала набор из четырех методов-расширений, помогающий упростить эту задачу и решать ее более гибко.

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

var balloons = context.Balloons.Where(
  b => b.Size == "L").ToList();
var balloonCount = context.Balloons.Count();

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

Если у вас нет доступа к некоей переменной, но вы все равно хотите выяснить, сколько объектов Balloon отслеживается ObjectContext, способ узнать это есть, но он не прост: ObjectContext предоставляет ObjectStateManager, в котором имеется метод GetObjectStateEntries. Этот метод требует, чтобы вы передали одно или более перечислений EntityState (например, Added, Modifed и т. д.);  тогда он будет знать, какие записи следует возвращать. Хотя результаты можно запрашивать, фильтрация окажется весьма громоздкой, и даже в этом случае он вернет вовсе не ваши сущности, а экземпляры ObjectStateEntry, представляющие информацию о состоянии ваших объектов.

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

objectContext.ObjectStateManager
  .GetObjectStateEntries(EntityState.Added |
  EntityState.Modified | EntityState.Unchanged)
  .Where(e => e.Entity is Balloon).Count();

Если вы хотите получить эти объекты Balloon, а не просто экземпляры ObjectStateEntry, то должны добавить логику приведения типов, чтобы возвращать типы ObjectStateEntry.Entity как Balloons:

objectContext.ObjectStateManager
  .GetObjectStateEntries(EntityState.Added |
  EntityState.Modified | EntityState.Unchanged)
  .Where(e => e.Entity is Balloon)
  .Select(e => e.Entity as Balloon);

Увидев этот код, вы наверняка оцените новое свойство DbSet.Local ничуть не меньше, чем я.

Используя DbSet.Local, чтобы получить все отслеживаемые экземпляры Balloon из контекста, вы можете просто вызывать:

context.Balloons.Local;

Local возвращает ObservableCollection, который дает два преимущества. Первое заключается в том, что этот набор можно запрашивать (queryable), поэтому вы можете возвращать любое (нужное вам) подмножество локально кешируемых Balloon. И второе — ваш код (или такие компоненты, как элементы управления, связывающие с данными) может слушать и реагировать на добавление или удаление объектов в кеше.

Помимо всего прочего, есть еще два заметных различия между DbSet.Local и GetObjectStateEntries. Одно из них в том, что Local возвращает объекты только из конкретного DbSet, тогда как GetObjectStateEntries возвращает записи независимо от представляемых ими типов объектов. Другое отличие — Local не будет возвращать объекты, которые известны контексту как помеченные Deleted. В случае GetObjectStateEntries вы имеете доступ к объектам, помеченным Added, Modified, Unchanged и Deleted, как указано в списке параметров, переданном этому методу.

LINQ-запросы NoTracking

Обсуждая вопросы производительности с клиентами, я часто рекомендую, чтобы они использовали способность EF возвращать данные, которые не требуется отслеживать контекстом. Например, возможно, у вас есть данные, которые вам нужно предоставлять для раскрывающегося списка выбора. Вы никогда не будете вносить изменения в эти данные — и тем более сохранять их в базе данных. Значит, будет разумно избегать падения производительности, наблюдаемого, когда EF создает экземпляры ObjectStateEntry для каждого отслеживаемого объекта и вынуждает контекст отслеживать любые изменения в этих объектах.

Но в случае ObjectContext поддержка NoTracking доступна только через класс ObjectQuery, а не запросы LINQ to Entities.

Вот типичный пример получения запроса NoTracking с использованием ObjectContext (с именем context):

string entitySQL = " SELECT p, p.Filling " +
                   "FROM PartyContext.Pinatas AS p " +
                   "WHERE p.Filling.Description='Candy'";
var query=context.CreateQuery<DbDataRecord>(entitySQL);
query.MergeOption = System.Data.Objects.MergeOption.NoTracking;
var pinatasWithFilling=query.ToList();

Извлекаемые Pinatas и Filling будут объектами в памяти, но контекст ничего не будет знать о них.

Однако, если бы вам понадобился следующий запрос LINQ to Entities, который возвращает IQueryable, а не ObjectQuery, то свойства MergeOption не было бы:

context.Pinatas.Include("Filling")
  .Where(p=>p.Filling.Description=="Candy")

Одно из решений — привести LINQ-запрос к ObjectQuery, а затем задать MergeOption. Это не только неочевидно, но и неудобно.

Осознавая это, группа EF нашла способ, позволяющий совместить несовместимое с помощью нового метода расширения AsNoTracking для IQueryables, который является частью DbContext API. Теперь я могу вставить его в свой LINQ-запрос:

context.Pinatas.Include("Filling")
  .Where(p=>p.Filling.Description=="Candy")
  .AsNoTracking();

Это вернет набор Pinatas и Fillings, которые будут игнорироваться контекстом. EF не станет впустую тратить ресурсы, создавая экземпляры DbEntityEntry (DbContext API-версию ObjectStateEntry) для каждого объекта, равно как не будет заставлять контекст анализировать эти объекты при вызове DetectChanges.

В итоге вам будет легко кодировать и быстро находить нужное через IntelliSense.

Другие улучшения

Три новых средства — Find, Local и AsNoTracking — не дали мне возможности выполнять задачи, которые нельзя выполнить с помощью ObjectContext. Но они радуют меня всякий раз, когда я пользуюсь ими. DbContext API упрощает множество задач (в сравнении с ObjectContext), и это позволило мне весьма заметно улучшить процесс разработки своих приложений. Я также вернулась к старому коду с ObjectContext и провела его рефакторинг, чтобы задействовать DbContext вместе с Code First и тем самым существенно сократить объем кода в старых приложениях. Но разработчики, которые не столь близко знакомы с EF, как я, почувствуют куда большую разницу от этих новых возможностей.


Ссылка: http://www.oszone.net/18265/Entity-Framework-DbContext