Уведомления о изменениях данных

175 Исходный проект

Ранее вы видели, что при внесении изменений в сущностные объекты Entity Framework отслеживает изменения в этих объектах. Благодаря этому, при вызове метода DbContex.SaveChanges(), Entity Framework знает, какие объекты нужно добавить в базу данных, какие обновить, а какие удалить. Для этого Entity Framework использует специальный механизм отслеживания изменений – Change Tracker API.

Вы можете получить доступ к информации об изменениях и некоторым операциям отслеживания связанных данных, с помощью свойства DbContext.ChangeTracker. Есть два способа взаимодействия с механизмом Change Tracker в Entity Framework:

Отслеживание изменений снимка данных (snapshot)

Код, написанный в предыдущих примерах, опирался на этот подход по отслеживанию изменений. Классы модели ничем не примечательны и при создании их объектов и инициализации, Entity Framework не может отслеживать изменения в данных. Вообще в .NET есть специальные типы, которые позволяют отслеживать изменения в своих экземплярах, например, коллекция ObservableCollection, с которой мы работали при обсуждении локальных данных, но наши простые классы модели такое поведение не поддерживают. Поэтому, при загрузке данных из базы в объекты классов модели, Entity Framework делает снимок этих данных. Когда мы вызываем метод SaveChanges(), EF делает новый снимок и сравнивает эти данные с данными из первого снимка. Если они изменились, Entity Framework генерирует SQL-инструкции для модификации данных в базе. Этот процесс сканирования каждого объекта работает через метод ChangeTracker.DetectChanges().

Отслеживание прокси-объектов (proxies objects)

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

Использование снимка данных (snapshot)

Отслеживание изменений с помощью снимка данных используется по умолчанию и позволяет Entity Framework определить, когда происходят изменения в объектах модели. DbContext API обнаруживает многие изменения в данных с помощью снимка и вызывает метод DetectChanges() класса ChangeTracker, который не только обновляет информацию по данным, которые должны быть сохранены в базе данных, но также обрабатывает изменения при инициализации навигационных свойств и внешних ключей. Важно иметь четкое представление о том, как и когда изменения будут обнаружены, что от них ожидать и как их контролировать. В этом разделе рассматриваются эти проблемы.

Метод DetectChanges() из класса ObjectContext появился в версии Entity Framework 4, и обрабатывал изменения в снимке данных для объектов POCO. Метод DbContext.ChangeTracker.DetectChanges() (который, в свою очередь, вызывает ObjectContext.DetectChanges()), отличается тем, что обрабатывает еще и другие события изменений данных, которые приводят к его вызову. Ниже показан список свойств и методов для работы с данными, которые неявно вызывают метод DetectChanges(), т.к. вносят изменения в структуру данных (вы должны быть уже знакомы с этими методами по предыдущим статьям):

Есть еще несколько методов, которые будут вызывать DetectChanges() и используются реже:

Управление вызовами DetectChanges()

Наиболее очевидным вызовом этого для отслеживания изменений, является вызов метода DbContext.SaveChanges(), который уведомляет Entity Framework о создании запроса к базе данных. Помимо этого мы можем вручную вызвать этот метод в любом месте, чтобы узнать какие данные изменились на текущий момент.

Сканирование данных не ограничивается только отслеживанием простых объектов. Например, в коде мы можем загрузить заказ из таблицы Orders базы данных, а затем добавить его в коллекцию заказов для нового покупателя. Объект, хранящий заказ, изменяется, потому что мы изменяем значение его навигационного свойства Customer и соответственно значение внешнего ключа UserId (если вы используете пример нашей модели, где мы определили внешний ключ явно). Но чтобы узнать, что это изменение произошло (или не произошло) Entity Framework необходимо сканировать все объекты типа Customer.

В большинстве случаев DetectChanges() вызывается достаточно быстро, что не вызывает проблем с производительностью. Тем не менее, если у вас есть очень большое количество объектов в памяти или вы выполняете много операций по DbContext с кучей запросов, то автоматический вызов DetectChanges() может серьезно ухудшить производительность приложения. К счастью у вас есть возможность выключить автоматические вызовы DetectChanges() и вызвать его вручную, когда это необходимо.

Entity Framework предполагает, что вы будете вызывать DetectChanges() перед каждым вызовом API, если вы изменили любой из объектов модели с момента последнего вызова API и перед запуском каких-либо запросов. Невыполнение этого требования может привести к неожиданным побочным эффектам. DbContext заботится об выполнении этого требования, при условии, что вы оставите включенный автоматический вызов DetectChanges(). Если вы выключите автоматические вызовы, то должны будете позаботиться о ручном вызове методов DetectChanges() там, где это необходимо.

Автоматические вызовы DetectChanges() можно включать и выключать с помощью логического свойства AutoDetectChangesEnabled, доступного в классе конфигурации контекста (DbContext.Configuration). Мы использовали эту настройку, когда определяли универсальный метод Inserts(), позволяющий вставлять несколько записей в таблицу. При условии, что мы бы вставляли сотни или тысячи записей с помощью этого метода, мы бы получили значительный выигрыш в производительности.

Давайте добавим метод, который отключает автоматические вызовы DetectChanges() и понаблюдаем за изменениями объектов:

public static void UsingDetectChanges()
{
    SampleContext context = new SampleContext();
    context.Configuration.AutoDetectChangesEnabled = false;

    Customer customer = context.Customers
        .Where(c => c.LastName == "Иванов")
        .First();

    customer.LastName = "Петров";

    Console.WriteLine("Состояние объекта до обнаружения изменений: \t{0}",
        context.Entry(customer).State);

    context.ChangeTracker.DetectChanges();

    Console.WriteLine("Состояние объекта после обнаружения изменений: \t{0}",
        context.Entry(customer).State);
}

В этом примере сначала отключается автоматический поиск изменений. Затем мы загружаем первого попавшегося покупателя из базы данных с фамилией Иванов и изменяем его фамилию на Петров. Т.к. мы отключили поиск изменений, при изменении сущностного объекта customer в памяти приложения, Entity Framework ничего не будет знать о его изменениях и о том, что нужно будет обновить запись в базе данных. Затем мы вручную запустили поиск изменений с помощью метода DetectChanges(). На рисунке ниже показан результат запуска приложения, с вызовом метода UsingDetectChanges из примера:

Запуск метода DetectChanges для обнаружения изменений в данных

Как и ожидалось, контекст не обнаружит изменений в сущностном объекте до тех пор, пока мы не вызовем метод DetectChanges() вручную.

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

public static void UsingDetectChanges()
{
    SampleContext context = new SampleContext();
    context.Configuration.AutoDetectChangesEnabled = false;

    context.Customers.Add(new Customer
    {
        FirstName = "Вася",
        LastName = "Ивин",
        Age = 34
    });

    context.Customers.Add(new Customer
    {
        FirstName = "Ольга",
        LastName = "Петрова",
        Age = 30
    });

    context.Customers.Add(new Customer
    {
        FirstName = "Олег",
        LastName = "Смирнов",
        Age = 28
    });

    context.SaveChanges();
}

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

Обновление навигационных свойств и внешних ключей

Метод DetectChanges() несет ответственность также за изменения значений в навигационных свойствах и внешних ключах. Если вы изменили некоторые отношения и хотели бы, чтобы все навигационные свойства и внешние ключи были синхронизированы, можно использовать метод DetectChanges(). Это может быть особенно полезно в сценариях привязки данных, где ваш графический интерфейс приложения будет изменять одно из навигационных свойств (или, возможно, внешний ключ), но при этом вы хотите, чтобы изменения сразу коснулись и связанных с ним объектов. Ниже показан пример:

private static void DetectRelationshipChanges()
{
    SampleContext context = new SampleContext();

    Customer customer = context.Customers
        .Include(c => c.Orders)
        .Where(c => c.LastName == "Иванов")
        .First();

    Order order = context.Orders
        .Include(o => o.Customer)
        .Where(o => o.ProductName == "Товар 4")
        .First();

    customer.Orders.Add(order);

    Console.WriteLine("Значения навигационного свойства order.Customer");
    Console.WriteLine("\n\tДо обнаружения изменений: \t{0}",
        order.Customer.LastName);

    context.ChangeTracker.DetectChanges();

    Console.WriteLine("\n\tПосле обнаружения изменений: \t{0}",
        order.Customer.LastName);
}

В этом примере мы загружаем данные покупателя, а также заказ с названием “Товар 4”, который принадлежит другому покупателю. Затем мы добавляем этот заказ этому покупателю, используя навигационное свойство Customer.Orders. Это свойство благополучно обновляется, но не вызывает обновления связанного навигационного свойства Order.Customer, а также значения внешнего ключа Order.UserId. Чтобы это исправить, мы вручную вызываем метод DetectChanges(). На рисунке ниже показан результат:

Ручное обновление навигационных свойств с помощью метода DetectChanges

Как видите, до проверки изменений, ссылка на покупателя не обновилась в объекте order. Мы решили эту проблему вызовом метода DetectChanges(). Такой сценарий бывает полезен в приложениях, использующих графические платформы, например, WPF, где нужно сразу изменять пользовательский интерфейс при изменении данных.

Использование отслеживания прокси-объектов (proxies objects)

Если счетчик производительности выявил чрезмерное количество вызовов DetectChanges() или вы предпочитаете обрабатывать изменения в отношениях в реальном времени, есть еще один способ отслеживания изменений, используя прокси-объекты. Вам достаточно внести некоторые незначительные изменения в классы модели, чтобы Entity Framework мог создать прокси-объекты, унаследованные от этих классов и отслеживающие изменения. Следующие несколько правил описывают какую вы должны использовать структуру для классов модели:

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

public class Customer
{
    public virtual int CustomerId { get; set; }
    public virtual string FirstName { get; set; }
    public virtual string LastName { get; set; }
    public virtual string Email { get; set; }
    public virtual int Age { get; set; }

    [Column(TypeName = "Image")]
    public virtual byte[] Photo { get; set; }

    public virtual ICollection<Order> Orders { get; set; }
    public virtual Profile Profile { get; set; }
}

Указание обобщенного типа коллекции ICollection<T> для навигационного свойства нужно затем, что прокси объект переопределяет это свойство и использует свой тип (EntityCollection<TEntity>). Этот тип коллекции будет отслеживать любые изменения в коллекции и сообщать их механизму Change Tracker.

Теперь, если вы запустите метод DetectRelationshipChanges(), то сможете убедиться, что изменения определяются автоматически с помощью прокси-объектов, еще до вызова метода DetectChanges(), как показано на рисунке ниже:

Прокси-объект обнаруживает изменение

Entity Framework автоматически создает прокси-объекты для результатов любых запросов, которые вы запускаете. Тем не менее, если вы просто используете конструктор вашего класса POCO для создания нового объекта, то он не будет прокси-объектом по умолчанию. Для того, чтобы получить прокси-объект без использования запроса, необходимо использовать метод DbSet.Create().

Для отключения отслеживания изменений только для определенных запросов, может использовать метод AsNoTracking(), который вызывается на экземпляре DbSet(). После этого, все запросы, связанные с этим объектом, не будут отслеживать в нем изменения данных.

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