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:
- Criar a instância do DbContext
- Controlar algumas entidades
- Fazer algumas alterações nas entidades
- Chamar SaveChanges para atualizar o banco de dados
- 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 estadoModified
. - 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 estadoModified
. - 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.