Август 2016

Том 31, номер 8

Доступ к данным - Отслеживание изменений в EF Core: Unchanged, Modified и Added

Джули Лерман | Август 2016

Исходный код можно скачать по ссылке

Джули Лерман Вы, вероятно, поняли, что Unchanged, Modified и Added являются перечислимыми для EntityState в Entity Framework (EF). Они также помогают мне описывать поведение механизма отслеживания изменений в EF Core по сравнению с более ранними версиями Entity Framework. Этот механизм стал более согласованным в EF Core, поэтому вы можете быть в большей мере уверенным в том, чего ожидать при работе с отсоединенными данными.

Учтите: хотя в EF Core пытаются сохранить парадигмы и большую часть синтаксиса ранних версий Entity Framework, EF Core является новым набором API — полностью новой кодовой базой, написанной с нуля. Поэтому важно не исходить из того, что все будет вести себя точно так же, как раньше. В этом смысле механизм отслеживания изменений (change tracker) является критически важным случаем.

Поскольку первая итерация EF Core нацелена на приведение в соответствие с ASP.NET Core, огромные усилия были сконцентрированы на поддержке отсоединенного состояния (disconnected state), т. е. на том, чтобы обеспечить способность Entity Framework обрабатывать состояние объектов без подключения к сети, когда EF не отслеживает эти объекты. Типичный случай — Web API, принимающий объекты от клиентского приложения, которые нужно сохранять в базе данных.

В своей рубрике за март 2016 года я написала статью «Handling the State of Disconnected Entities in EF» (msdn.com/magazine/mt694083). Основное внимание в этой статье было уделено присваиванию информации о состоянии отсоединенным сущностям и обмену этой информацией с EF при передаче соответствующих объектов обратно EF-механизму отслеживания изменений. Хотя я использовала EF6 для примера, тот шаблон по-прежнему годится для EF Core, так что после обсуждения поведений EF Core я покажу пример того, как я реализовала данный шаблон в EF Core.

Отслеживание с помощью DbSet: модифицировано

DbSet всегда включал методы Add, Attach и Remove. Результат этих методов в отдельном объекте достаточно прост: они присваивают состоянию объекта релевантное EntityState. Add изменяет состояние на Added, Attach — на Unchanged и Remove — на Deleted. Есть одно исключение: если вы удаляете сущность, уже известную как Added, она будет отсоединена от DbContext, поскольку отслеживать новую сущность больше не требуется. В EF6, когда вы применяете эти методы с графами, их влияние на соответствующие объекты не совсем согласованное. Ранее не отслеживавшиеся объекты нельзя удалить, и такая попытка приводит к ошибке. Уже отслеживавшиеся объекты могут иметь еще не измененное состояние в зависимости от того, какими являются эти состояния. Я создала набор тестов в EF6, чтобы оценивать различные поведения, и вы найдете его на GitHub по ссылке bit.ly/28YvwYd.

Создавая EF Core, группа EF экспериментировала с поведением этих методов в различных бета-версиях. В EF Core RTM данные методы больше не ведут себя так, как они делали это в EF6 и ранее. По большей части изменения в этих методах привели к более согласованному поведению, на которое можно полагаться. Но важно понимать, как они изменились.

При использовании Add, Attach и Remove с объектом, к которому присоединен граф, состоянию каждого объекта в графе, неизвестному механизму отслеживания изменений, будет присваиваться состояние, идентифицированное методом. Позвольте мне прояснить это на примере моей любимой модели EF Core из фильма «Семь самураев» («Seven Samurai») с записями о самураях с присоединенными цитатами из фильма и другой информацией.

Если самурай новый и не отслеживается, Samurais.Add присвоит состоянию самурая Added. Если с самураем связана цитата на момент вызова Add, его состояние также будет установлено в Added. Это желательное поведение и, по сути, идентично таковому в EF6.

Что будет, если вы добавляете новую цитату к существующему объекту самурая и вместо моей рекомендации присвоить newQuote.SamuraiId значение Samurai.Id задаете навигационное свойство newQuote.Samurai=oldSamurai? В автономном сценарии, где ни цитата, ни oldSamurai не отслеживаются EF, Quotes.Add(newQuote) будет делать то же самое, что и в предыдущем случае. Он пометит newQuote и oldSamurai как Added. SaveChanges вставит оба объекта в базу данных, и в ней появится дубликат oldSamurai.

Если вы делаете это в клиентском приложении, например Windows Presentation Foundation (WPF), и используете свой контекст для запроса самураев, а затем применяете тот же экземпляр контекста для вызова context.Quotes.Add(newQuote), то контекст уже знает об oldSamurai и не станет изменять состояние Unchanged на Added. Вот что я имела в виду, когда говорила о том, что не следует изменять состояние уже отслеживаемых объектов.

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

Роуэн Миллер (Rowan Miller) суммировал это новое поведение на GitHub по ссылке bit.ly/295goxw.

Add  Добавляет каждую досягаемую сущность, которая еще не отслеживается.

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

Update  То же, что Attach, но сущности помечаются как Modified.

Remove То же, что Attach, но потом помечает корень как удаленный. Поскольку каскадное удаление теперь происходит при вызове SaveChanges, это позволяет позднее применять к сущностям правила каскадных операций.

Есть еще одно изменение в методах DbSet, которое вы, вероятно, заметили в этом списке: в DbSet наконец-то появился метод Update, который будет устанавливать состояние не отслеживаемых объектов в Modified. Ура! Какая замечательная альтернатива тому, что раньше приходилось всегда добавлять или подключать сущности, а затем явным образом выставлять их состояние в Modified.

Range-методы DbSet: тоже модифицированы

В EF6 были введены два Range-метода в DbSet (AddRange и RemoveRange), позволяющие передавать массив сходных типов. Это обеспечивало огромный прирост производительности, так как механизм отслеживания изменений вступал в действие лишь раз, а не для каждого элемента массива. Эти методы вызывают Add и Remove, как было подробно описано ранее, и поэтому вам нужно продумывать, как это отразится на объектах графа.

В EF6 Range-методы существовали только для Add и Remove, но в EF Core добавлены UpdateRange и AttachRange. Методы Update и Attach, вызываемые индивидуально для каждого объекта или графа, передаваемого в Range-методы, будут вести себя, как описывалось ранее.

Методы отслеживания изменений в DbContext: добавлены

Если вы работали с EF ObjectContext до введения DbContext, то, по-видимому, помните, что в ObjectContext были методы Add, Attach и Delete. Поскольку контекст никак не мог узнать о том, к какому ObjectSet принадлежала целевая сущность, вам приходилось добавлять строковое представление имени ObjectSet в качестве параметра. Это было так громоздко, что большинство предпочитало просто использовать методы Add, Attach и Delete в ObjectSet. Когда появился DbContext, эти громоздкие методы ушли, и вы могли вызывать Add, Attach и Remove только через DbSet.

В EF Core методы Add, Attach и Remove возвращены как методы в DbContext — наряду с Update и четырьмя соответствующими Range-методами (AddRange и т. д.). Но эти методы теперь гораздо интеллектуальнее. Они способны определять тип и автоматически соотносить сущность с правильным DbSet. Это по-настоящему удобно, так как позволяет писать обобщенный код без необходимость в создании экземпляра DbSet. Код стал проще и, что важнее, в большей мере поддающимся проверке. Вот сравнение кода для EF6 и EF Core:

private void AddToSetEF6<T>(T entity) where T : class {Pull
  using (var context = new SamuraiContext()) {
    context.Set<T>().Add(entity);
  }
}

private void AddToSetEFCore(object entity) {
  using (var context = new SamuraiContext()) {
    context.Add(entity);
  }
}

Range-методы даже еще полезнее, поскольку вы можете передавать им самые разнообразные типы:

private void AddViaContextEFCore(
  object[] entitiesOfVaryingTypes)
{
  using (var context = new SamuraiContext()) {
    context.AddRange(entitiesOfVaryingTypes);
  }
}

DbContext.Entry: модифицирован — будьте осторожны с этим изменением в поведении

Хотя нас предупредили, что EF Core — это не EF6 и нам не следует ожидать, что знакомый код будет вести себя так же, как в EF6, все равно трудно отказаться от таких ожиданий, когда было перенесено столь много поведений. DbContext.Entry — как раз такой случай, и важно понимать, как он изменился.

Для меня эта модификация желанная, потому что она вносит согласованность в отслеживание изменений. В EF6 метод Add из DbSet (и прочие) и метод DbContext.Entry в сочетании со свойством State оказывали одинаковое действие на сущности и графы. Поэтому задание DbContext.Entry(object).State=EntityState.Added перевело бы состояние всех объектов в графе (которые еще не отслеживаются) в Added.

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

В EF Core DbContext.Entry теперь действует только на передаваемые ему объекты. Если к данному объекту присоединены другие релевантные объекты, DbContext.Entry будет игнорировать их.

Если вы привыкли использовать метод Entry для подключения графов к экземпляру DbContext, то сразу поймете, почему это изменение столь кардинальное. Оно означает, что вы можете работать с индивидуальным объектом, даже если он является частью графа.

Еще важнее, что теперь можно явно использовать методы отслеживания DbSet и DbContext (Add и ему подобные), чтобы явным образом работать с графами, и применять метод DbContext.Entry для операций с индивидуальными объектами. В сочетании со следующим изменением, которое я поясню, это означает, что теперь у вас есть ясные варианты при передаче объектных графов в механизм отслеживания изменений EF Core.

DbContext.ChangeTracker.TrackGraph: добавлен

TrackGraph — совершенно новая концепция в EF Core. Он обеспечивает полный контроль над каждым объектом в графе, который ваш DbContext должен начать отслеживать.

TrackGraph обходит граф (т. е. перебирает в цикле каждый объект в графе) и применяет указанную функцию (designated function) к каждому из этих объектов. Функция является вторым параметром метода TrackGraph.

Наиболее распространенный пример — тот, где для каждого объекта устанавливается некое общее состояние. В следующем коде TrackGraph будет перебирать все объекты в графе newSword и задавать их состояние как Added:

context.ChangeTracker.TrackGraph(newSword, e =>
  e.Entry.State = EntityState.Added);

Тот же подвох, что и в методах из DbSet и DbContext, есть и в TrackGraph: если сущность уже отслеживается, TrackGraph будет игнорировать ее. Хотя конкретно это применение TrackGraph дает поведение, идентичное методам отслеживания из DbSet и DbContext, оно действительно обеспечивает больше возможностей в написании повторно используемого кода.

Лямбда (e в коде, приведенном выше) представляет тип EntityEntryGraphNode. Этот тип также делает доступным свойство NodeType, и вы можете встретить его в IntelliSense по мере набора кода своей лямбды. Похоже, что это свойство предназначено для внутренних целей и не оказывает эффекта, обеспечиваемого e.Entry.State, так что будьте внимательны, чтобы случайно не задействовать его.

В сценарии с отсоединенными данными игнорирование подвоха с уже отслеживаемыми объектами может оказаться не подходящим вариантом. Дело в том, что экземпляр DbContext будет новым и пустым, поэтому все объекты графа должны быть новыми в DbContext. Но рассмотрим возможность передачи набора графов в Web API. Теперь есть шанс существования нескольких ссылок на общий объект, и EF-механизм отслеживания изменений будет проверять идентификацию, чтобы определить, отслеживается ли сущность. Это хороший вариант не добавлять объект в механизм отслеживания изменений повторно.

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

И вот здесь эта статья будет перекликаться с моей статьей за март 2016 года и шаблоном, которым я поделилась для задания состояния объектов в ваших классах и последующего считывания этого состояния, чтобы сообщить механизму отслеживания изменений, каким должен быть EntityState объекта. Теперь я могу объединить этот шаблон с методом TrackGraph, заставив вызовы функции TrackGraph выполнять задачу установки EntityState на основе метода State объекта.

Работа с классами предметной области не отличается от того, что я делала в мартовской статье. Я начинаю с определения перечисления для локально отслеживаемого ObjectState:

public enum ObjectState {
    Unchanged,
    Added,
    Modified,
    Deleted
  }

Затем создаю интерфейс IObjectWithState, который предоставляет свойство State, основанное на этом перечислении:

public interface IObjectWithState
{
  ObjectState State { get; set; }
}

Теперь я модифицирую свои классы для реализации этого интерфейса. В качестве примера посмотрите на небольшой класс Location с таким интерфейсом:

using SamuraiTracker.Domain.Enums;
using SamuraiTracker.Domain.Interfaces;

namespace SamuraiTracker.Domain
{
  public class Location : IObjectWithState
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public ObjectState State { get; set; }
  }
}

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

В случае DbContext я располагаю некоторыми статическими методами во вспомогательном классе ChangeTrackerHelpers (рис. 1).

Рис. 1. Класс ChangeTrackerHelpers

public static class ChangeTrackerHelpers
{
  public static void ConvertStateOfNode(
    EntityEntryGraphNode node)
  {
    IObjectWithState entity =
      (IObjectWithState)node.Entry.Entity;
    node.Entry.State = ConvertToEFState(entity.State);
  }

  private static EntityState ConvertToEFState(
    ObjectState objectState)
  {
    EntityState efState = EntityState.Unchanged;
    switch (objectState) {
      case ObjectState.Added:
        efState = EntityState.Added;
        break;
      case ObjectState.Modified:
        efState = EntityState.Modified;
        break;
      case ObjectState.Deleted:
        efState = EntityState.Deleted;
        break;
      case ObjectState.Unchanged:
        efState = EntityState.Unchanged;
        break;
    }
    return efState;
  }
}

ConvertStateOfNode — это метод, который будет вызываться TrackGraph. Он будет присваивать EntityState объекта значение, определяемое методом ConvertToEFState, который преобразует значение IObjectWithState.State в значение EntityState.

Покончив с этим, теперь можно использовать TrackGraph для отслеживания объектов наряду с их корректно назначаемыми EntityState. Вот пример, где я передаю объектный граф, состоящий из Samurai со связанными Quote и Sword:

context.ChangeTracker.TrackGraph(samurai,
  ChangeTrackerHelpers.ConvertStateOfNode);

В EF6-решении я должна была добавлять элементы в механизм отслеживания изменений, а затем явно вызывать какой-то метод, который перебирал бы все записи в механизме отслеживания изменений, чтобы задавать релевантное состояние каждого объекта. Решение на основе EF Core гораздо эффективнее. Заметьте, что я пока не исследовала возможное влияние на производительность при операциях с большими объемами данных в одной транзакции.

Если вы скачаете код для этой статьи, то увидите, что я применяю новый шаблон внутри интеграционного теста CanApplyStateViaChangeTracker, в котором я создаю граф, назначаю различные состояния разным объектам, а затем проверяю, что полученные в итоге значения EntityState правильны.

IsKeySet: добавлено

Объект EntityEntry, который хранит информацию об изменениях для каждой сущности, получил новое свойство — IsKeySet. Это отличное пополнение в API. Оно проверяет, присвоено ли значение свойству ключа в сущности. Это исключает всякие гадания (и связанный с ними код) в попытках понять, есть ли у какого-либо объекта значение в его свойстве ключа (или свойствах, если ключ является составным). IsKeySet также проверяет, установлено ли значение по умолчанию для конкретного типа, указанного вами для свойства ключа. Если это int, равно ли значение 0? Если это Guid, равно ли оно Guid.Empty (00000000-0000-0000-0000-000000000000)? Если значение отличается от значения по умолчанию для данного типа, IsKeySet возвращает true.

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

EF Core с широко раскрытыми глазами

Хотя группа EF определенно сделала, что могла, чтобы облегчить вам переход с ранних версий Entity Framework на EF Core, и воспроизвела многое из прежнего синтаксиса и поведения, важно учитывать, что это разные API. Портирование кода будет сложным, а потому не рекомендуется — особенно на этом этапе, когда RTM-версия имеет лишь подмножество знакомой функциональности. Но даже если вы приступаете к новым проектам с уверенностью, что набор функциональности в EF Core предлагает все, что вам нужно, не предполагайте, что она будет работать одинаково. Мне до сих пор приходится напоминать себе об этом. Тем не менее, я рада изменениям в ChangeTracker. Группа сумела придать ему больше ясности, больше согласованности и расширила возможности контроля при работе с отсоединенными данными.

Группа EF опубликовала дорожную карту на странице GitHub, для которой я создала удобную сокращенную ссылку: bit.ly/efcoreroadmap. Это позволит вам отслеживать функционал, хотя в дорожной карте нет мелких деталей вроде изменений в поведении. Для этого я рекомендую тесты, много тестов, чтобы убедиться в том, что ваш код работает, как вы ожидали. И если вы планируете портировать код с прежних версий EF, то, возможно, захотите разобрать Llewellyn Falco’s Approval Tests (approvaltests.com), которые позволяют сравнивать вывод от тестов и проверять его на соответствие вашим ожиданиям.


Джули Лерман (Julie Lerman) — Microsoft MVP, преподаватель и консультант по .NET, живет в Вермонте. Часто выступает на конференциях по всему миру и в группах пользователей по тематике, связанной с доступом к данным и другими технологиями Microsoft .NET. Ведет блог thedatafarm.com/blog и является автором серии книг «Programming Entity Framework» (O’Reilly Media, 2010), в том числе «Code First Edition» (2011) и «DbContext Edition» (2012), также выпущенных издательством O’Reilly Media. Вы можете читать ее заметки в twitter.com/julielerman и смотреть ее видеокурсы для Pluralsight на juliel.me/PS-Videos.

Выражаю благодарность за рецензирование статьи эксперту Microsoft Эрику Эйлскову Йенсену (Erik Ejlskov Jensen).