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

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

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

Совет

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

Совет

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

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

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

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

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

Использование автоматически созданных ключей

Значение автоматически созданного ключа часто может использоваться, чтобы определить, что нужно сделать: вставить или обновить сущность. Если ключ не задан (например, он по-прежнему имеет стандартное значение среды выполнения 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 bool IsItNew(BloggingContext context, Blog blog)
    => context.Blogs.Find(blog.BlogId) == null;

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

Сохранение одной сущности

Если неизвестно, следует выполнять вставку или обновление, то можно должным образом использовать метод Add или Update:

public static void Insert(DbContext context, object entity)
{
    context.Add(entity);
    context.SaveChanges();
}

public static void Update(DbContext context, object entity)
{
    context.Update(entity);
    context.SaveChanges();
}

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

public static void InsertOrUpdate(DbContext context, object entity)
{
    context.Update(entity);
    context.SaveChanges();
}

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

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

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

    context.SaveChanges();
}

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

  • Если метод 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 void InsertGraph(DbContext context, object rootEntity)
{
    context.Add(rootEntity);
    context.SaveChanges();
}

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

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

public static void UpdateGraph(DbContext context, object rootEntity)
{
    context.Update(rootEntity);
    context.SaveChanges();
}

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

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

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

public static void InsertOrUpdateGraph(DbContext context, object rootEntity)
{
    context.Update(rootEntity);
    context.SaveChanges();
}

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

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

public static void InsertOrUpdateGraph(BloggingContext context, Blog blog)
{
    var existingBlog = context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefault(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);
            }
        }
    }

    context.SaveChanges();
}

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

Выполнение удаления может оказаться сложной задачей, так как очень часто отсутствие сущности означает, что ее необходимо удалить. Для решения этой проблемы можно выполнить обратимые удаления. Например, пометить сущность как удаленную, а не удалять ее. Таким образом удаление идентично обновлению. Обратимые удаления можно реализовать с помощью фильтров запросов.

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

public static void InsertUpdateOrDeleteGraph(BloggingContext context, Blog blog)
{
    var existingBlog = context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefault(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);
            }
        }
    }

    context.SaveChanges();
}

TrackGraph

В методах Internally, Add, Attach и Update используется обход графа с определением, выполненным для каждой сущности, независимо от того, следует ли ее пометить как Added (для вставки), Modified (для обновления), Unchanged (не выполнять никаких действий) или Deleted (для удаления). Этот механизм предоставляется через API TrackGraph. Например, предположим, что когда клиент отправляет обратно граф сущностей, он устанавливает некоторые флаги на каждой сущности, указывая, как ее следует обрабатывать. TrackGraph затем может использоваться для обработки этого флага:

public static void 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;
        });

    context.SaveChanges();
}

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