Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Cada DbContext instância controla as alterações feitas em entidades. Essas entidades controladas, por sua vez, conduzem as alterações ao banco de dados quando SaveChanges é chamado.
O controle de alterações do EF Core (Entity Framework Core) funciona melhor quando a mesma DbContext instância é 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 é abordada no Controle de Alterações no EF Core.
Tip
Este documento pressupõe que os estados de entidade e as noções básicas do controle de alterações do EF Core sejam compreendidos. Consulte Controle de Alterações no EF Core para obter mais informações sobre esses tópicos.
Tip
Você pode executar e depurar todo o código neste documento baixando o código de exemplo do GitHub.
Introduction
As entidades podem ser explicitamente "anexadas" a um DbContext modo que o contexto acompanhe essas entidades. Isso é útil principalmente quando:
- Criando novas entidades que serão inseridas no banco de dados.
- Anexar novamente entidades desconectadas que foram consultadas anteriormente por uma instância de DbContext diferente.
O primeiro deles será necessário para a maioria dos aplicativos e é tratado principalmente pelos DbContext.Add métodos.
O segundo é necessário apenas para aplicativos que alteram entidades ou suas relações enquanto as entidades não estão sendo controladas. Por exemplo, um aplicativo Web pode enviar entidades para o cliente Web em que o usuário faz alterações e envia as entidades de volta. Essas entidades são conhecidas como "desconectadas", pois foram consultadas originalmente de um DbContext, mas foram desconectadas desse contexto quando enviadas ao cliente.
O aplicativo Web agora deve anexar novamente essas entidades para que elas sejam novamente controladas e indicar as alterações que foram feitas de modo que SaveChanges possa fazer atualizações apropriadas ao banco de dados. Isso é tratado principalmente pelos métodos DbContext.Attach e DbContext.Update.
Tip
A anexação de entidades à mesma instância DbContext da qual elas foram consultadas normalmente não deve ser necessária. Não execute rotineiramente uma consulta sem acompanhamento e anexe as entidades retornadas ao mesmo contexto. Isso será mais lento do que usar uma consulta de acompanhamento e também poderá resultar em problemas como valores de propriedade de sombra ausentes, tornando mais difícil acertar.
Valores de chave gerados versus explícitos
Por padrão, as propriedades de chave inteiras e de GUID são configuradas para usar valores de chave gerados automaticamente. Isso tem uma grande vantagem para o controle de alterações: um valor de chave não definido indica que a entidade é "nova". Por "novo", queremos dizer que ele ainda não foi inserido no banco de dados.
Dois modelos são usados nas seções a seguir. O primeiro é configurado para não usar valores de chave gerados:
public class Blog
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public string Name { get; set; }
public IList<Post> Posts { get; } = new List<Post>();
}
public class Post
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
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; }
}
Os valores de chave não gerados (ou seja, definidos explicitamente) são mostrados primeiro em cada exemplo porque tudo é muito explícito e fácil de seguir. Em seguida, isso é seguido por um exemplo em que os valores de chave gerados são usados:
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; }
}
Observe que as propriedades de chave neste modelo não precisam de nenhuma configuração adicional aqui, pois o uso de valores de chave gerados é o padrão para chaves inteiros simples.
Inserindo novas entidades
Valores de chave explícitos
Uma entidade deve ser controlada no estado Added a ser inserida por SaveChanges. As entidades normalmente são colocadas no estado Adicionado chamando um dos DbContext.Add, DbContext.AddRange, DbContext.AddAsync, DbContext.AddRangeAsync ou métodos equivalentes em DbSet<TEntity>.
Tip
Todos esses métodos funcionam da mesma forma no contexto do controle de alterações. Consulte Recursos Adicionais de Controle de Alterações para obter mais informações.
Por exemplo, para começar a acompanhar um novo blog:
context.Add(
new Blog { Id = 1, Name = ".NET Blog", });
Inspecionar a exibição de depuração do controle de alterações após essa chamada mostra que o contexto está controlando a nova entidade no estado Added:
Blog {Id: 1} Added
Id: 1 PK
Name: '.NET Blog'
Posts: []
No entanto, os métodos Add não funcionam apenas em uma entidade individual. Na verdade, eles começam a rastrear um grafo inteiro de entidades relacionadas, colocando-as todas no Added estado. Por exemplo, para inserir um novo blog e novas postagens associadas:
context.Add(
new Blog
{
Id = 1,
Name = ".NET Blog",
Posts =
{
new Post
{
Id = 1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = 2,
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
}
}
});
O contexto agora está acompanhando todas essas entidades como Added:
Blog {Id: 1} Added
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Added
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} Added
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 valores explícitos foram definidos para as propriedades de Id chave nos exemplos acima. Isso ocorre porque o modelo aqui foi configurado para usar valores de chave definidos explicitamente, em vez de valores de chave gerados automaticamente. Quando não estiver usando chaves geradas, as propriedades de chave devem ser definidas explicitamente antes de chamar Add. Esses valores de chave são inseridos quando SaveChanges é chamado. Por exemplo, ao usar SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Id", "Name")
VALUES (@p0, @p1);
-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String), @p3='1' (DbType = String), @p4='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p5='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p2, @p3, @p4, @p5);
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String), @p1='1' (DbType = String), @p2='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p3='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2, @p3);
Todas essas entidades são controladas no estado Unchanged após a conclusão de SaveChanges, já que essas entidades agora existem no banco de dados:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
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} Unchanged
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}
Valores de chave gerados
Como mencionado anteriormente, as propriedades de chave inteiras e de GUID são configurados para usar valores de chave gerados automaticamente por padrão. Isso significa que o aplicativo não deve definir nenhum valor de chave explicitamente. Por exemplo, para inserir um novo blog e todos os posts com valores de chave gerados:
context.Add(
new Blog
{
Name = ".NET Blog",
Posts =
{
new Post
{
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
}
}
});
Assim como acontece com valores de chave explícitos, o contexto agora está acompanhando todas essas entidades como Added:
Blog {Id: -2147482644} Added
Id: -2147482644 PK Temporary
Name: '.NET Blog'
Posts: [{Id: -2147482637}, {Id: -2147482636}]
Post {Id: -2147482637} Added
Id: -2147482637 PK Temporary
BlogId: -2147482644 FK Temporary
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: -2147482644}
Post {Id: -2147482636} Added
Id: -2147482636 PK Temporary
BlogId: -2147482644 FK Temporary
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: -2147482644}
Observe, nesse caso, que os valores de chave temporária foram gerados para cada entidade. Esses valores são usados pelo EF Core até que SaveChanges seja chamado, momento em que os valores de chave real são lidos novamente do banco de dados. Por exemplo, ao usar SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Name")
VALUES (@p0);
SELECT "Id"
FROM "Blogs"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p2='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p3='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p1, @p2, @p3);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], 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();
Após a conclusão do SaveChanges, todas as entidades foram atualizadas com seus valores reais de chave e são agora monitoradas no estado Unchanged, já que correspondem ao estado no banco de dados.
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
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} Unchanged
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}
Esse é exatamente o mesmo estado final do exemplo anterior que usou valores de chave explícitos.
Tip
Um valor de chave explícito ainda pode ser definido mesmo ao usar valores de chave gerados. O EF Core tentará fazer a inserção usando esse valor de chave. Algumas configurações de banco de dados, incluindo o SQL Server com colunas de Identidade, não dão suporte a essas inserções e vão gerar (confira esses documentos para uma solução alternativa).
Anexando entidades existentes
Valores de chave explícitos
As entidades retornadas de consultas são controladas no estado Unchanged. O Unchanged estado significa que a entidade não foi modificada desde que foi consultada. Uma entidade desconectada, talvez retornada de um cliente Web em uma solicitação HTTP, pode ser colocada nesse estado usando DbContext.Attach, DbContext.AttachRangeou os métodos equivalentes em DbSet<TEntity>. Por exemplo, para começar a acompanhar um blog existente:
context.Attach(
new Blog { Id = 1, Name = ".NET Blog", });
Note
Os exemplos aqui envolvem criar entidades explicitamente com new para facilitar. Normalmente, as instâncias de entidade terão vindo de outra fonte, como ser desserializada de um cliente ou ser criada a partir de dados em um HTTP Post.
Inspecionar a exibição de depuração do controle de alterações após essa chamada mostra que a entidade é controlada no estado Unchanged:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: []
Assim como Add, na verdade, Attach define um grafo inteiro de entidades conectadas ao Unchanged estado. Por exemplo, para anexar um blog existente e postagens existentes associadas:
context.Attach(
new Blog
{
Id = 1,
Name = ".NET Blog",
Posts =
{
new Post
{
Id = 1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = 2,
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
}
}
});
O contexto agora está acompanhando todas essas entidades como Unchanged:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
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} Unchanged
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}
Chamar SaveChanges neste momento não terá efeito. Todas as entidades são marcadas como Unchanged, portanto, não há nada para atualizar no banco de dados.
Valores de chave gerados
Como mencionado anteriormente, as propriedades de chave inteiras e de GUID são configurados para usar valores de chave gerados automaticamente por padrão. Isso tem uma grande vantagem ao trabalhar com entidades desconectadas: um valor de chave não definido indica que a entidade ainda não foi inserida no banco de dados. Isso permite que o rastreador de alterações detecte automaticamente novas entidades e as coloque no Added estado. Por exemplo, considere anexar este gráfico de um blog e de postagens.
context.Attach(
new Blog
{
Id = 1,
Name = ".NET Blog",
Posts =
{
new Post
{
Id = 1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = 2,
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
},
new Post
{
Title = "Announcing .NET 5.0",
Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
},
}
});
O blog tem um valor de chave de 1, indicando que ele já existe no banco de dados. Duas das postagens também têm valores de chave definidos, mas o terceiro não. O EF Core verá esse valor de chave como 0, o padrão CLR para um inteiro. Isso resulta em EF Core marcando a nova entidade como Added em vez de Unchanged:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}, {Id: -2147482636}]
Post {Id: -2147482636} Added
Id: -2147482636 PK Temporary
BlogId: 1 FK
Content: '.NET 5.0 includes many enhancements, including single file a...'
Title: 'Announcing .NET 5.0'
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} Unchanged
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Chamar SaveChanges neste ponto não faz nada com as Unchanged entidades, mas insere a nova entidade no banco de dados. Por exemplo, ao usar SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], 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();
O ponto importante a observar aqui é que, com os valores de chave gerados, o EF Core é capaz de distinguir automaticamente novas entidades existentes em um grafo desconectado. Em poucas palavras, ao usar chaves geradas, o EF Core sempre inserirá uma entidade quando essa entidade não tiver nenhum conjunto de valores de chave.
Atualizando entidades existentes
Valores de chave explícitos
DbContext.Update, DbContext.UpdateRange e os métodos equivalentes de DbSet<TEntity> comportam-se exatamente como os métodos de Attach descritos acima, exceto que as entidades são colocadas no estado Modified em vez do estado Unchanged. Por exemplo, para começar a acompanhar um blog existente como Modified:
context.Update(
new Blog { Id = 1, Name = ".NET Blog", });
Inspecionar a exibição de depuração do controle de alterações após essa chamada mostra que o contexto está controlando essa entidade no estado Modified:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog' Modified
Posts: []
Assim como com Add e Attach, na verdade, Update marca um grafo inteiro de entidades relacionadas como Modified. Por exemplo, para anexar um blog existente e postagens existentes associadas como Modified:
context.Update(
new Blog
{
Id = 1,
Name = ".NET Blog",
Posts =
{
new Post
{
Id = 1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = 2,
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
}
}
});
O contexto agora está acompanhando todas essas entidades como Modified:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog' Modified
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Modified
Id: 1 PK
BlogId: 1 FK Modified Originally <null>
Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
Title: 'Announcing the Release of EF Core 5.0' Modified
Blog: {Id: 1}
Post {Id: 2} Modified
Id: 2 PK
BlogId: 1 FK Modified Originally <null>
Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
Title: 'Announcing F# 5' Modified
Blog: {Id: 1}
Chamar SaveChanges neste ponto fará com que as atualizações sejam enviadas ao banco de dados para todas essas entidades. Por exemplo, ao usar SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();
Valores de chave gerados
Assim como ocorre com Attach, os valores de chave gerados têm o mesmo grande benefício para Update: um valor de chave não definido indica que a entidade é nova e ainda não foi inserida no banco de dados. Assim como acontece com Attach, isso permite que o DbContext detecte automaticamente novas entidades e as coloque no estado Added. Por exemplo, considere a chamada Update com este grafo representando um blog e suas postagens.
context.Update(
new Blog
{
Id = 1,
Name = ".NET Blog",
Posts =
{
new Post
{
Id = 1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = 2,
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
},
new Post
{
Title = "Announcing .NET 5.0",
Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
},
}
});
Assim como acontece com o Attach exemplo, a postagem sem nenhum valor de chave é detectada como nova e definida como o Added estado. As outras entidades são marcadas como Modified:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog' Modified
Posts: [{Id: 1}, {Id: 2}, {Id: -2147482633}]
Post {Id: -2147482633} Added
Id: -2147482633 PK Temporary
BlogId: 1 FK
Content: '.NET 5.0 includes many enhancements, including single file a...'
Title: 'Announcing .NET 5.0'
Blog: {Id: 1}
Post {Id: 1} Modified
Id: 1 PK
BlogId: 1 FK Modified Originally <null>
Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
Title: 'Announcing the Release of EF Core 5.0' Modified
Blog: {Id: 1}
Post {Id: 2} Modified
Id: 2 PK
BlogId: 1 FK Modified Originally <null>
Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
Title: 'Announcing F# 5' Modified
Blog: {Id: 1}
A chamada SaveChanges neste ponto fará com que as atualizações sejam enviadas ao banco de dados para todas as entidades existentes, enquanto a nova entidade é inserida. Por exemplo, ao usar SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], 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();
Essa é uma maneira muito fácil de gerar atualizações e inserções de um grafo desconectado. No entanto, isso resulta em atualizações ou inserções sendo enviadas ao banco de dados para cada propriedade de cada entidade controlada, mesmo quando alguns valores de propriedade podem não ter sido alterados. Não se assuste muito com isso; para muitos aplicativos com grafos pequenos, essa pode ser uma maneira fácil e pragmática de gerar atualizações. Dito isto, outros padrões mais complexos às vezes podem resultar em atualizações mais eficientes, conforme descrito na Resolução de Identidade no EF Core.
Excluindo entidades existentes
Para que uma entidade seja excluída pelo SaveChanges, ela deve ser rastreada no estado Deleted. Normalmente, as entidades são colocadas no estado Deleted ao chamar um dos métodos DbContext.Remove, DbContext.RemoveRange ou os métodos equivalentes em DbSet<TEntity>. Por exemplo, para marcar uma postagem existente como Deleted:
context.Remove(
new Post { Id = 2 });
Inspecionar a exibição de depuração do controle de alterações após essa chamada mostra que o contexto está controlando a entidade no estado Deleted:
Post {Id: 2} Deleted
Id: 2 PK
BlogId: <null> FK
Content: <null>
Title: <null>
Blog: <null>
Essa entidade será excluída quando SaveChanges for chamado. Por exemplo, ao usar SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
Após a conclusão do SaveChanges, a entidade excluída será desanexada do DbContext, pois ela não existe mais no banco de dados. A exibição de depuração está, portanto, vazia porque nenhuma entidade está sendo controlada.
Excluir entidades dependentes/filho
Excluir entidades dependentes/filho de um grafo é mais simples do que excluir entidades principais/pai. Consulte a próxima seção e Alterando chaves estrangeiras e navegações para mais informações.
É incomum chamar Remove uma entidade criada com new. Além disso, ao contrário de Add, Attach e Update, é incomum chamar Remove em uma entidade que ainda não está rastreada no estado Unchanged ou Modified. Em vez disso, é comum rastrear uma única entidade ou grafo de entidades relacionadas e, em seguida, chamar Remove as entidades que devem ser excluídas. Esse gráfico de entidades monitoradas normalmente é criado por:
- Executar uma consulta para as entidades
- Usando os métodos
AttachouUpdateem um grafo de entidades desconectadas, conforme descrito nas seções anteriores.
Por exemplo, é mais provável que o código na seção anterior obtenha um post de um cliente e então faça algo assim:
context.Attach(post);
context.Remove(post);
Isso se comporta exatamente da mesma maneira que o exemplo anterior, pois ao utilizar Remove em uma entidade não rastreada, isso faz com que ela seja anexada e então marcada como Deleted.
Em exemplos mais realistas, um grafo de entidades é anexado primeiro e, em seguida, algumas dessas entidades são marcadas como excluídas. Por exemplo:
// Attach a blog and associated posts
context.Attach(blog);
// Mark one post as Deleted
context.Remove(blog.Posts[1]);
Todas as entidades são marcadas como Unchanged, exceto aquela em que Remove foi chamada:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
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}
Essa entidade será excluída quando SaveChanges for chamado. Por exemplo, ao usar SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
Após a conclusão do SaveChanges, a entidade excluída será desanexada do DbContext, pois ela não existe mais no banco de dados. Outras entidades permanecem no estado Unchanged:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{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}
Excluir entidades principais/pai
Cada relação que conecta dois tipos de entidade tem um lado principal ou pai e um lado dependente ou filho. A entidade dependente/filho é aquela com a propriedade de chave estrangeira. Em uma relação um-para-muitos, o principal/pai está no lado "um" e o dependente/filho está no lado "muitos". Consulte Relações para obter mais informações.
Nos exemplos anteriores, excluímos uma postagem, que é uma entidade dependente/filho no relacionamento um-para-muitos de blog-postagens. Isso é relativamente simples, pois a remoção de uma entidade dependente/filho não afeta outras entidades. Por outro lado, a exclusão de uma entidade principal/pai também afeta todas as entidades dependentes/filho. Não fazer isso deixaria um valor de chave estrangeira referenciando um valor de chave primária que não existe mais. Esse é um estado de modelo inválido e resulta em um erro de restrição referencial na maioria dos bancos de dados.
Esse estado de modelo inválido pode ser tratado de duas maneiras:
- Definindo valores FK como nulos. Isso indica que os dependentes/filhos não estão mais relacionados a nenhum responsável. Esse é o padrão para relações opcionais em que a chave estrangeira deve ser anulável. Definir o FK como nulo não é válido para as relações necessárias, em que a chave estrangeira normalmente não é anulável.
- Excluindo os dependentes/filhos. Esse é o padrão para relações necessárias e também é válido para relações opcionais.
Consulte Alteração de chaves estrangeiras e navegações para obter informações detalhadas sobre controle de alterações e relacionamentos.
Relações opcionais
A Post.BlogId propriedade de chave estrangeira é anulável no modelo que temos usado. Isso significa que a relação é opcional e, portanto, o comportamento padrão do EF Core é definir BlogId propriedades de chave estrangeira como nulas quando o blog é excluído. Por exemplo:
// Attach a blog and associated posts
context.Attach(blog);
// Mark the blog as deleted
context.Remove(blog);
Inspecionar a exibição de depuração do controlador de alterações após a chamada para Remove mostra que, conforme o esperado, o blog agora está marcado como Deleted:
Blog {Id: 1} Deleted
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Modified
Id: 1 PK
BlogId: <null> FK Modified Originally 1
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: <null>
Post {Id: 2} Modified
Id: 2 PK
BlogId: <null> FK Modified Originally 1
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: <null>
Mais interessante, todas as postagens relacionadas agora estão marcadas como Modified. Isso ocorre porque a propriedade de chave estrangeira em cada entidade foi definida como nula. Chamar SaveChanges atualiza o valor da chave estrangeira para cada postagem como nulo no banco de dados, antes de excluir o blog:
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p2;
SELECT changes();
Após a conclusão do SaveChanges, a entidade excluída será desanexada do DbContext, pois ela não existe mais no banco de dados. Outras entidades agora estão marcadas como Unchanged com valores nulos de chave estrangeira, que correspondem ao estado do banco de dados:
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: <null> FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: <null>
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: <null> FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: <null>
Relações necessárias
Se a Post.BlogId propriedade de chave estrangeira não for anulável, a relação entre blogs e postagens se tornará "necessária". Nessa situação, o EF Core excluirá, por padrão, entidades dependentes/filho quando o principal/pai for excluído. Por exemplo, excluir um blog com postagens relacionadas como no exemplo anterior:
// Attach a blog and associated posts
context.Attach(blog);
// Mark the blog as deleted
context.Remove(blog);
Inspecionar a exibição de depuração do controlador de alterações após a chamada para Remove mostra que, conforme o esperado, o blog está marcado novamente como Deleted:
Blog {Id: 1} Deleted
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Deleted
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}
Mais interessante nesse caso é que todas as postagens relacionadas também foram marcadas como Deleted. Chamar SaveChanges faz com que o blog e todas as postagens relacionadas sejam excluídas do banco de dados:
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
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=[@p1='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p1;
Após a conclusão do SaveChanges, todas as entidades excluídas serão desanexadas do DbContext, pois elas não existem mais no banco de dados. A saída da exibição de depuração está, portanto, vazia.
Note
Esse documento apenas arranha a superfície ao trabalhar com relações no EF Core. Consulte Relações para obter mais informações sobre a modelagem de relações e alteração de chaves estrangeiras e navegações para obter mais informações sobre como atualizar/excluir entidades dependentes/filhas ao chamar SaveChanges.
Acompanhamento personalizado com TrackGraph
ChangeTracker.TrackGraph funciona como Add, Attach e Update, exceto que ele gera um retorno de chamada para cada instância de entidade antes de rastreá-la. Isso permite que a lógica personalizada seja usada ao determinar como rastrear entidades individuais em um grafo.
Por exemplo, considere a regra que o EF Core usa ao rastrear entidades com valores de chave gerados: se o valor da chave for zero, a entidade será nova e deverá ser inserida. Vamos estender essa regra para dizer se o valor da chave é negativo, então a entidade deve ser excluída. Isso nos permite alterar os valores de chave primária em entidades de um grafo desconectado para marcar entidades excluídas:
blog.Posts.Add(
new Post
{
Title = "Announcing .NET 5.0",
Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
}
);
var toDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
toDelete.Id = -toDelete.Id;
Esse grafo desconectado pode ser acompanhado usando TrackGraph:
public static async Task UpdateBlog(Blog blog)
{
using var context = new BlogsContext();
context.ChangeTracker.TrackGraph(
blog, node =>
{
var propertyEntry = node.Entry.Property("Id");
var keyValue = (int)propertyEntry.CurrentValue;
if (keyValue == 0)
{
node.Entry.State = EntityState.Added;
}
else if (keyValue < 0)
{
propertyEntry.CurrentValue = -keyValue;
node.Entry.State = EntityState.Deleted;
}
else
{
node.Entry.State = EntityState.Modified;
}
Console.WriteLine($"Tracking {node.Entry.Metadata.DisplayName()} with key value {keyValue} as {node.Entry.State}");
});
await context.SaveChangesAsync();
}
Para cada entidade no grafo, o código acima verifica o valor da chave primária antes de acompanhar a entidade. Para valores de chave não definidos (zero), o código faz o que o EF Core normalmente faria. Ou seja, se a chave não estiver definida, a entidade será marcada como Added. Se a chave estiver definida e o valor não for negativo, a entidade será marcada como Modified. No entanto, se um valor de chave negativo for encontrado, seu valor real não negativo será restaurado e a entidade será controlada como Deleted.
A saída da execução desse código é:
Tracking Blog with key value 1 as Modified
Tracking Post with key value 1 as Modified
Tracking Post with key value -2 as Deleted
Tracking Post with key value 0 as Added
Note
Para simplificar, este código assume que cada entidade tem uma propriedade de chave primária inteira chamada Id. Isso pode ser codificado em uma interface ou classe base abstrata. Como alternativa, a propriedade ou as propriedades da chave primária podem ser obtidas dos IEntityType metadados de modo que esse código funcione com qualquer tipo de entidade.
TrackGraph tem duas sobrecargas. Na sobrecarga simples usada acima, o EF Core determina quando parar de atravessar o grafo. Especificamente, ele interrompe a visita de novas entidades relacionadas de uma determinada entidade quando essa entidade já está sendo controlada ou quando o retorno de chamada não começa a controlar a entidade.
A sobrecarga avançada, ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>), tem um retorno de chamada que retorna um bool. Se o retorno de chamada retornar false, a travessia do grafo será interrompida, caso contrário, continuará. Deve-se tomar cuidado para evitar loops infinitos ao usar essa sobrecarga.
A sobrecarga avançada também permite que o estado seja fornecido ao TrackGraph e esse estado é então passado para cada retorno de chamada.