Отключенные сущности

Объект DbContext автоматически отслеживает сущности, возвращаемые из базы данных. Изменения, внесенные в эти сущности, будут обнаружены при вызове SaveChanges, и база данных будет обновлена по мере необходимости. Дополнительные сведения см. в разделе "Базовые сведения о сохранении и связанных данных ".

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

Подсказка

Вы можете скачать используемый в этой статье пример из репозитория GitHub.

Подсказка

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

Определение новых сущностей

Клиент определяет новые сущности

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

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

С автоматически созданными ключами

Значение автоматически созданного ключа часто можно использовать для определения необходимости вставки или обновления сущности. Если ключ не задан (то есть он по-прежнему имеет значение CLR по умолчанию null, ноль и т. д.), сущность должна быть новой и нуждается в вставке. С другой стороны, если задано значение ключа, оно должно быть уже сохранено и теперь требует обновления. Другими словами, если ключ имеет какое-либо значение, то сущность была запрошена, отправлена клиенту и теперь вернулась для обновления.

Легко проверить отсутствие ключа, когда тип сущности известен.

public static bool IsItNew(Blog blog)
    => blog.BlogId == 0;

Однако EF также имеет встроенный способ сделать это для любого типа сущности и типа ключа:

public static bool IsItNew(DbContext context, object entity)
    => !context.Entry(entity).IsKeySet;

Подсказка

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

С другими ключами

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

  • Запрос для сущности
  • Передача флага от клиента

Чтобы запросить сущность, просто используйте метод Find:

public static async Task<bool> IsItNew(BloggingContext context, Blog blog)
    => (await context.Blogs.FindAsync(blog.BlogId)) == null;

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

Сохранение отдельных объектов

Если известно, требуется ли вставка или обновление, тогда можно использовать либо Добавить, либо Обновить.

public static async Task Insert(DbContext context, object entity)
{
    context.Add(entity);
    await context.SaveChangesAsync();
}

public static async Task Update(DbContext context, object entity)
{
    context.Update(entity);
    await context.SaveChangesAsync();
}

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

public static async Task InsertOrUpdate(DbContext context, object entity)
{
    context.Update(entity);
    await context.SaveChangesAsync();
}

Обычно метод Update помечает сущность для обновления, а не для вставки. Однако если сущность имеет автоматически созданный ключ, и значение ключа не задано, то сущность вместо этого автоматически помечается для вставки.

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

public static async Task InsertOrUpdate(BloggingContext context, Blog blog)
{
    var existingBlog = await context.Blogs.FindAsync(blog.BlogId);
    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
    }

    await context.SaveChangesAsync();
}

Ниже приведены действия.

  • Если функция Find возвращает значение null, база данных еще не содержит блог с этим идентификатором, поэтому мы вызываем метод Add, чтобы отметить его для вставки.
  • Если Поиск возвращает сущность, она существует в базе данных, и контекст теперь отслеживает существующую сущность.
    • Затем мы используем SetValues, чтобы задать значения для всех свойств этой сущности тем, которые были получены от клиента.
    • Вызов SetValues пометит сущность для ее обновления при необходимости.

Подсказка

SetValues помечает только измененные свойства, имеющие разные значения в отслеживаемой сущности. Это означает, что при отправке обновления будут обновлены только те столбцы, которые фактически изменились. (И если ничего не изменилось, обновление не будет отправлено вообще.)

Работа с графами

Определение личности

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

Все новые или все существующие сущности

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

var blog = new Blog
{
    Url = "http://sample.com", Posts = new List<Post> { new Post { Title = "Post 1" }, new Post { Title = "Post 2" }, }
};

можно вставить следующим образом:

public static async Task InsertGraph(DbContext context, object rootEntity)
{
    context.Add(rootEntity);
    await context.SaveChangesAsync();
}

Вызов "Добавить" отметит блог и все записи, которые будут вставлены.

Аналогичным образом, если необходимо обновить все сущности в графе, можно использовать обновление:

public static async Task UpdateGraph(DbContext context, object rootEntity)
{
    context.Update(rootEntity);
    await context.SaveChangesAsync();
}

Блог и все его записи будут помечены для обновления.

Сочетание новых и существующих сущностей

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

public static async Task InsertOrUpdateGraph(DbContext context, object rootEntity)
{
    context.Update(rootEntity);
    await context.SaveChangesAsync();
}

Обновление помечает любую сущность в графе, блоге или сообщении для вставки, если у нее не установлен ключ, а все остальные сущности помечены для обновления.

Как и раньше, если не использовать автоматически созданные ключи, можно использовать запрос и некоторую обработку:

public static async Task InsertOrUpdateGraph(BloggingContext context, Blog blog)
{
    var existingBlog = await context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefaultAsync(b => b.BlogId == blog.BlogId);

    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
        foreach (var post in blog.Posts)
        {
            var existingPost = existingBlog.Posts
                .FirstOrDefault(p => p.PostId == post.PostId);

            if (existingPost == null)
            {
                existingBlog.Posts.Add(post);
            }
            else
            {
                context.Entry(existingPost).CurrentValues.SetValues(post);
            }
        }
    }

    await context.SaveChangesAsync();
}

Обработка удалений

Удаление может быть сложным, так как часто отсутствие сущности означает, что её следует удалить. Одним из способов решения этой проблемы является использование "мягких удалений", когда сущность помечена как удалённая, а не фактически удалена. Удаляется, а затем становится таким же, как обновления. Мягкое удаление можно реализовать посредством фильтров запросов.

Для истинных удалений распространенный шаблон — использовать расширение шаблона запроса для выполнения того, что по сути является диффом графа. Рассмотрим пример.

public static async Task InsertUpdateOrDeleteGraph(BloggingContext context, Blog blog)
{
    var existingBlog = await context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefaultAsync(b => b.BlogId == blog.BlogId);

    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
        foreach (var post in blog.Posts)
        {
            var existingPost = existingBlog.Posts
                .FirstOrDefault(p => p.PostId == post.PostId);

            if (existingPost == null)
            {
                existingBlog.Posts.Add(post);
            }
            else
            {
                context.Entry(existingPost).CurrentValues.SetValues(post);
            }
        }

        foreach (var post in existingBlog.Posts)
        {
            if (!blog.Posts.Any(p => p.PostId == post.PostId))
            {
                context.Remove(post);
            }
        }
    }

    await context.SaveChangesAsync();
}

TrackGraph

Внутренне добавление, присоединение и обновление используют обход графа с определением для каждой сущности относительно того, следует ли ее пометить как добавленную (для вставки), измененную (для обновления), неизмененной (ничего не делать) или удаленной (для удаления). Этот механизм предоставляется через API TrackGraph. Например, предположим, что когда клиент отправляет обратно граф сущностей, он устанавливает некоторый флаг для каждой сущности, указывающий, как она должна обрабатываться. Затем для обработки этого флага можно использовать TrackGraph:

public static async Task SaveAnnotatedGraph(DbContext context, object rootEntity)
{
    context.ChangeTracker.TrackGraph(
        rootEntity,
        n =>
        {
            var entity = (EntityBase)n.Entry.Entity;
            n.Entry.State = entity.IsNew
                ? EntityState.Added
                : entity.IsChanged
                    ? EntityState.Modified
                    : entity.IsDeleted
                        ? EntityState.Deleted
                        : EntityState.Unchanged;
        });

    await context.SaveChangesAsync();
}

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