Controle de Alterações no EF Core

Cada instância DbContext controla as alterações feitas em entidades. Essas entidades controladas, por sua vez, conduzem as alterações ao banco de dados quando SaveChanges é chamado.

Este documento apresenta uma visão geral do controle de alterações do EF Core (Entity Framework Core) e como ele se relaciona com consultas e atualizações.

Dica

Você pode executar e depurar em todo o código neste documento baixando o código de exemplo do GitHub.

Dica

Para simplificar, este documento usa e referencia métodos síncronos, como SaveChanges, em vez de seus equivalentes assíncronos, como SaveChangesAsync. É possível substituir a ação de chamar e aguardar o método assíncrono, a menos que seja indicado de outra forma.

Como controlar entidades

As instâncias de entidade tornam-se controladas quando são:

  • Retornadas de uma consulta executada no banco de dados
  • Explicitamente anexadas ao DbContext por Add, Attach, Update ou métodos semelhantes
  • Detectadas como novas entidades conectadas a entidades controladas existentes

As instâncias de entidade não são mais controladas quando:

  • O DbContext é descartado
  • O rastreador de alterações está limpo
  • As entidades são explicitamente desanexadas

O DbContext foi projetado para representar uma unidade de trabalho de curta duração, conforme descrito em Inicialização e configuração de DbContext. Isso significa que descartar o DbContext é a maneira normal de parar de controlar entidades. Em outras palavras, o tempo de vida de um DbContext deve ser:

  1. Criar a instância do DbContext
  2. Controlar algumas entidades
  3. Fazer algumas alterações nas entidades
  4. Chamar SaveChanges para atualizar o banco de dados
  5. Descartar a instância do DbContext

Dica

Não é necessário limpar o controlador de alterações ou desanexar explicitamente as instâncias de entidade ao adotar essa abordagem. No entanto, se você precisar desanexar entidades, a chamada a ChangeTracker.Clear será mais eficiente do que desanexar entidades uma a uma.

Estados da entidade

Cada entidade está associada a um determinado EntityState:

  • As entidades Detached não estão sendo controladas pelo DbContext.
  • As entidades Added são novas e ainda não foram inseridas no banco de dados. Isso significa que serão inseridas quando SaveChanges for chamado.
  • As entidades Unchanged não foram alteradas desde que foram consultadas a partir do banco de dados. Todas as entidades retornadas de consultas estão inicialmente nesse estado.
  • As entidades Modified foram alteradas desde que foram consultadas a partir do banco de dados. Isso significa que serão atualizadas quando SaveChanges for chamado.
  • As entidades Deleted existem no banco de dados, mas são marcadas para serem excluídas quando SaveChanges é chamado.

O EF Core controla as alterações no nível da propriedade. Por exemplo, se apenas um único valor de propriedade for modificado, uma atualização de banco de dados alterará apenas esse valor. No entanto, as propriedades só podem ser marcadas como modificadas quando a própria entidade estiver no estado Modificado. (Ou, de uma perspectiva alternativa, o estado Modificado significa que pelo menos um valor de propriedade foi marcado como modificado.)

Esta tabela resume os diferentes estados:

Status da Entidade Controlada por DbContext Existe no banco de dados Propriedades modificadas Ação em SaveChanges
Detached Não - - -
Added Sim Não - Inserir
Unchanged Sim Sim Não -
Modified Sim Sim Yes Atualizar
Deleted Sim Yes - Delete

Observação

Esse texto usa termos do banco de dados relacional para maior clareza. Os bancos de dados NoSQL normalmente dão suporte a operações semelhantes, mas possivelmente com nomes diferentes. Confira a documentação do provedor de banco de dados para obter mais informações.

Acompanhamento de consultas

O controle de alterações do EF Core funciona melhor quando a mesma instância DbContext é usada para consultar entidades e atualizá-las chamando SaveChanges. Isso ocorre porque o EF Core controla automaticamente o estado das entidades consultadas e detecta as alterações feitas nessas entidades quando SaveChanges é chamado.

Essa abordagem tem várias vantagens em relação ao controle explícito de instâncias de entidade:

  • É simples. Os estados de entidade raramente precisam ser manipulados explicitamente – o EF Core cuida das alterações de estado.
  • Atualizações são limitadas apenas aos valores que realmente foram alterados.
  • Os valores das propriedades de sombra são preservados e usados conforme o necessário. Isso é especialmente relevante quando chaves estrangeiras são armazenadas em estado de sombra.
  • Os valores originais das propriedades são preservados automaticamente e usados para atualizações eficientes.

Consulta e atualização simples

Por exemplo, considere um modelo de blog/postagens simples:

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; }
    public Blog Blog { get; set; }
}

Podemos usar esse modelo para consultar blogs e postagens e, em seguida, fazer algumas atualizações no banco de dados:

using var context = new BlogsContext();

var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

blog.Name = ".NET Blog (Updated!)";

foreach (var post in blog.Posts.Where(e => !e.Title.Contains("5.0")))
{
    post.Title = post.Title.Replace("5", "5.0");
}

context.SaveChanges();

Chamar SaveChanges resulta nas seguintes atualizações de banco de dados, usando o SQLite como um banco de dados de exemplo:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0='Announcing F# 5.0' (Size = 17)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "Title" = @p0
WHERE "Id" = @p1;
SELECT changes();

A exibição de depuração do controlador de alterações é uma ótima maneira de visualizar quais entidades estão sendo rastreadas e quais são seus estados. Por exemplo, inserir o seguinte código no exemplo acima antes de chamar SaveChanges:

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Isso gera a saída a seguir:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5'
  Blog: {Id: 1}

Observe especificamente:

  • A propriedade Blog.Name é marcada como modificada (Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog') e isso resulta no blog assumindo o estado Modified.
  • A propriedade Post.Title da postagem 2 é marcada como modificada (Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5') e isso resulta na postagem assumindo o estado Modified.
  • Os outros valores de propriedade da postagem 2 não foram alterados e, portanto, não são marcados como modificados. É por isso que esses valores não são incluídos na atualização do banco de dados.
  • A outra postagem não foi modificada de forma alguma. É por isso que ela ainda está no estado Unchanged e não está incluída na atualização do banco de dados.

Consultar, em seguida, inserir, atualizar e excluir

Atualizações como as do exemplo anterior podem ser combinadas com inserções e exclusões na mesma unidade de trabalho. Por exemplo:

using var context = new BlogsContext();

var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Modify property values
blog.Name = ".NET Blog (Updated!)";

// Insert a new Post
blog.Posts.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
    });

// Mark an existing Post as Deleted
var postToDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
context.Remove(postToDelete);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

Neste exemplo:

  • Um blog e postagens relacionadas são consultados do banco de dados e rastreados
  • A propriedade Blog.Name é alterada
  • Uma nova postagem é adicionada à coleção de postagens existentes para o blog
  • Uma postagem existente é marcada para exclusão chamando DbContext.Remove

Um novo exame da exibição de depuração do controlador de alterações antes de chamar SaveChanges mostra como o EF Core está controlando essas alterações:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}, {Id: -2147482638}]
Post {Id: -2147482638} Added
  Id: -2147482638 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 was released recently and has come with many...'
  Title: 'What's next for System.Text.Json?'
  Blog: {Id: 1}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Observe que:

  • O blog está marcado como Modified. Isso gerará uma atualização de banco de dados.
  • A postagem 2 está marcada como Deleted. Isso gerará uma exclusão de banco de dados.
  • Uma nova postagem com uma ID temporária está associada ao blog 1 e está marcada como Added. Isso gerará uma inserção de banco de dados.

Isso resulta nos seguintes comandos de banco de dados (usando SQLite) quando SaveChanges é chamado:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 was released recently and has come with many...' (Size = 56), @p2='What's next for System.Text.Json?' (Size = 33)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Confira Entidades de controle explícito para obter mais informações sobre como inserir e excluir entidades. Confira Detecção e notificações de alterações para saber mais sobre como o EF Core detecta automaticamente alterações como esta.

Dica

Chame ChangeTracker.HasChanges() para determinar se alguma alteração foi feita que fará com que o SaveChanges faça atualizações no banco de dados. Se HasChanges retornar false, SaveChanges não terá operações.