Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
Cada DbContext instância controla as alterações feitas nas entidades. Essas entidades controladas, por sua vez, direcionam as alterações no banco de dados quando SaveChanges são chamadas.
O controle de alterações do Entity Framework Core (EF Core) funciona melhor quando a mesma DbContext instância é usada para consultar entidades e atualizá-las chamando SaveChanges. Isso ocorre porque o EF Core rastreia automaticamente o estado das entidades consultadas e, em seguida, deteta quaisquer 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 da entidade e os conceitos básicos 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 tal que o contexto rastreie essas entidades. Isso é útil principalmente quando:
- Criação de novas entidades que serão inseridas na base de dados.
- Reanexar entidades desconectadas que foram previamente consultadas por uma instância diferente do DbContext.
A primeira delas será necessária pela maioria dos aplicativos e é tratada principalmente pelos métodos DbContext.Add.
O segundo só é necessário para aplicativos que alteram entidades ou seus relacionamentos enquanto as entidades não estão sendo rastreadas. Por exemplo, um aplicativo Web pode enviar entidades para o cliente Web onde o usuário faz alterações e envia as entidades de volta. Essas entidades são chamadas de "desconectadas", uma vez que foram originalmente consultadas a partir 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 para que SaveChanges possam fazer atualizações apropriadas no banco de dados. Isso é tratado principalmente pelos DbContext.Attach métodos e DbContext.Update .
Tip
Anexar entidades à mesma instância DbContext da qual elas foram consultadas normalmente não deve ser necessário. Não execute rotineiramente uma consulta sem controle e, em seguida, anexe as entidades retornadas ao mesmo contexto. Isso será mais lento do que usar uma consulta de rastreamento e também pode resultar em problemas como os valores ausentes das propriedades de sombra, tornando mais difícil garantir a precisão.
Valores chave gerados versus explícitos
Por padrão, as propriedades de chave inteiro e GUID são configuradas para utilizar 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 ainda não foi inserido na base de dados.
Dois modelos são usados nas seções a seguir. O primeiro está 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-chave não gerados (ou seja, explicitamente definidos) são mostrados primeiro em cada exemplo, porque tudo é muito explícito e fácil de seguir. Isto é seguido por um exemplo onde os valores-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 configuração adicional aqui, pois usar valores de chave gerados é o padrão para chaves inteiras simples.
Inserção de novas entidades
Valores-chave explícitos
Uma entidade deve ser rastreada no estado Added para ser inserida pela SaveChanges. Normalmente, as entidades são colocadas no estado Adicionado chamando um dos métodos DbContext.Add, DbContext.AddRange, DbContext.AddAsync, DbContext.AddRangeAsync ou métodos equivalentes em DbSet<TEntity>.
Tip
Todos esses métodos funcionam da mesma maneira no contexto do controle de mudanças. 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 controlador de alterações após esta chamada mostra que o contexto está rastreando a nova entidade no Added estado:
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 gráfico inteiro de entidades relacionadas, colocando-as todas para o 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á rastreando 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 Id propriedades principais nos exemplos acima. Isso ocorre porque o modelo aqui foi configurado para usar valores de chave explicitamente definidos, em vez de valores de chave gerados automaticamente. Quando não estiver usando chaves geradas, as propriedades da chave devem ser definidas explicitamente antes de chamar Add. Esses valores-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 estas entidades são rastreadas no estado Unchanged após a conclusão de SaveChanges, uma vez que essas entidades agora existem na base 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-chave gerados
Como mencionado acima, as propriedades de chave inteira e GUID são configuradas 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 todas as publicações 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..."
}
}
});
Tal como acontece com os valores-chave explícitos, o contexto está agora a acompanhar todas estas 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 neste caso que valores de chave temporários 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 reais são lidos de volta 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 de SaveChanges, todas as entidades foram atualizadas com seus valores de chave reais e são rastreadas no Unchanged estado, uma vez que agora 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}
Este é 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á inserir usando esse valor de chave. Algumas configurações de bases de dados, incluindo o SQL Server com colunas Identity, não suportam tais inserções e irão gerar um erro (veja estes documentos para uma solução alternativa).
Anexar entidades existentes
Valores-chave explícitos
As entidades retornadas de consultas são rastreadas no Unchanged estado. 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.AttachRange ou 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 estão a criar entidades explicitamente com new para simplicidade. Normalmente, as instâncias de entidade terão vindo de outra fonte, como serem desserializadas de um cliente ou serem criadas a partir de dados num pedido HTTP POST.
A inspeção da exibição de depuração do controlador de alterações após esta chamada mostra que a entidade é rastreada Unchanged no estado:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: []
Assim como Add, na verdade, Attach define um gráfico 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á rastreando 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 estão marcadas como Unchanged, portanto, não há nada para atualizar no banco de dados.
Valores-chave gerados
Como mencionado acima, as propriedades de chave inteira e GUID são configuradas 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 detete automaticamente novas entidades e as coloque no Added estado. Por exemplo, considere anexar este gráfico de um blogue e publicações.
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. Dois dos postos também têm valores-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 notar aqui é que, com valores de chave gerados, o EF Core é capaz de distinguir automaticamente entidades novas de entidades existentes em um gráfico desconectado. Em resumo, ao usar chaves geradas, o EF Core sempre inserirá uma entidade quando essa entidade não tiver nenhum valor de chave definido.
Atualização de entidades existentes
Valores-chave explícitos
DbContext.Update, DbContext.UpdateRange, e os métodos equivalentes em DbSet<TEntity> comportam-se exatamente como os métodos Attach descritos acima, exceto que as entidades são colocadas no estado Modified em vez do estado Unchanged. Por exemplo, para começar a rastrear um blog existente como Modified:
context.Update(
new Blog { Id = 1, Name = ".NET Blog", });
Inspecionar a exibição de depuração do controlador de alterações após esta chamada mostra que o contexto está rastreando essa entidade no Modified estado:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog' Modified
Posts: []
Assim como com Add e Attach, Update na verdade marca um gráfico 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á rastreando 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 momento fará com que as atualizações sejam enviadas para o 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-chave gerados
Tal como acontece 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 na base de dados. Assim como no Attach, isso permite que o DbContext detete automaticamente novas entidades e as coloque no Added estado. Por exemplo, considere chamar Update com este grafo de um blog e publicações.
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..."
},
}
});
Como no Attach exemplo, a postagem sem valor de chave é detetada e considerada nova e definida para o estado Added. As outras entidades estã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 para o 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();
Esta é uma maneira muito fácil de gerar atualizações e inserções a partir de um gráfico desconectado. No entanto, isso resulta em atualizações ou inserções sendo enviadas para o banco de dados para cada propriedade de cada entidade rastreada, mesmo quando alguns valores de propriedade podem não ter sido alterados. Não se assuste muito com isso; Para muitas aplicações com gráficos pequenos, esta pode ser uma forma fácil e pragmática de gerar atualizações. Dito isso, outros padrões mais complexos às vezes podem resultar em atualizações mais eficientes, conforme descrito em Resolução de identidade no EF Core.
Exclusão de entidades existentes
Para que uma entidade seja excluída por SaveChanges, ela deve estar no estado Deleted sendo rastreada. As entidades são normalmente colocadas no Deleted estado chamando um dos 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 vista de depuração do monitor de alterações após esta chamada mostra que o contexto está a rastrear a entidade no Deleted estado:
Post {Id: 2} Deleted
Id: 2 PK
BlogId: <null> FK
Content: <null>
Title: <null>
Blog: <null>
Esta entidade será eliminada quando o 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 de SaveChanges, a entidade excluída é desanexada do DbContext porque não existe mais no banco de dados. A exibição de depuração está, portanto, vazia porque nenhuma entidade está sendo rastreada.
Exclusão de entidades dependentes/filhas
Excluir entidades dependentes/filhas de um gráfico é mais simples do que excluir entidades principais/pai. Consulte a próxima seção e Alterando chaves estrangeiras e navegações para obter mais informações.
É incomum chamar Remove numa entidade criada com new. Além disso, ao contrário de Add, Attach e Update, é incomum chamar Remove sobre uma entidade que ainda não está a ser rastreada no estado Unchanged ou Modified. Em vez disso, é típico rastrear uma única entidade ou um gráfico de entidades relacionadas e, em seguida, chamar Remove para as entidades que devem ser eliminadas. Este gráfico de entidades controladas é normalmente criado por uma das opções seguintes:
- Executando uma consulta para as entidades
- Usando os métodos
AttachouUpdatenum gráfico de entidades desconectadas, como descrito nas secções anteriores.
Por exemplo, o código na seção anterior é mais provável de obter uma postagem de um cliente e, em seguida, fazer algo assim:
context.Attach(post);
context.Remove(post);
Isso se comporta exatamente da mesma maneira que o exemplo anterior, já que chamar Remove uma entidade não rastreada faz com que ela primeiro seja anexada e, em seguida, marcada como Deleted.
Em exemplos mais realistas, um gráfico de entidades é primeiro anexado 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 na qual 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}
Esta entidade será eliminada quando o 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 de SaveChanges, a entidade excluída é desanexada do DbContext porque 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}
Exclusão de entidades principais/controladoras
Cada relação que conecta dois tipos de entidade tem uma extremidade principal ou pai e uma extremidade dependente ou filha. A entidade dependente/filho é aquela com a propriedade da chave estrangeira. Numa relação um-para-muitos, o principal/progenitor está do lado "um" e o dependente/filho está do lado "muitos". Consulte Relações para obter mais informações.
Nos exemplos anteriores, estávamos excluindo uma postagem, que é uma entidade dependente/filho na relação um-para-muitos. Isto é relativamente simples, uma vez que a remoção de uma entidade dependente/filho não tem qualquer impacto noutras entidades. Por outro lado, a exclusão de uma entidade principal/mãe também deve afetar quaisquer entidades dependentes/filhas. Não fazer isso deixaria um valor de chave estrangeira fazendo referência a um valor de chave primária que não existe mais. Este é 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:
- Definir valores FK para null. Isto indica que os dependentes/filhos já não têm parentesco com nenhum comitente/progenitor. Este é 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 relações necessárias, onde a chave estrangeira normalmente não é anulável.
- Exclusão dos dependentes/filhos. Esse é o padrão para relacionamentos necessários e também é válido para relacionamentos opcionais.
Consulte Alterando chaves estrangeiras e navegações para obter informações detalhadas sobre controle de alterações e relacionamentos.
Relações opcionais
A Post.BlogId propriedade da 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, como 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, todos os posts relacionados agora estão marcados como Modified. Isso ocorre porque a propriedade de chave estrangeira em cada entidade foi definida como null. Chamar SaveChanges atualiza o valor da chave estrangeira para cada postagem para 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 de SaveChanges, a entidade excluída é desanexada do DbContext porque não existe mais no banco de dados. Outras entidades agora são marcadas como Unchanged com valores de chave estrangeira nulos, 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 propriedade da Post.BlogId 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/filhas quando a entidade principal/pai for excluída. 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 visualização de depuração do rastreador de alterações após a chamada para Remove mostra que, como esperado, o blog é novamente marcado 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 neste caso é que todos os posts relacionados também foram marcados 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 de SaveChanges, todas as entidades excluídas são desanexadas do DbContext porque não existem mais no banco de dados. A saída da visualização de depuração está, portanto, vazia.
Note
Este documento apenas arranha a superfície ao trabalhar com relacionamentos no EF Core. Consulte Relações para obter mais informações sobre modelagem de relacionamentos e Alterando chaves estrangeiras e navegações para obter mais informações sobre como atualizar/excluir entidades dependentes/filhas ao chamar SaveChanges.
Acompanhamento personalizado com o TrackGraph
ChangeTracker.TrackGraph funciona como Add, Attach e Update, exceto que gera um retorno de chamada para cada instância de entidade antes de a rastrear. Isso permite que a lógica personalizada seja usada ao determinar como rastrear entidades individuais em um gráfico.
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 é nova e deve ser inserida. Vamos estender essa regra para dizer se o valor da chave for negativo, a entidade deve ser excluída. Isso nos permite alterar os valores de chave primária em entidades de um gráfico 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;
Este gráfico desconectado pode então ser rastreado usando o 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 gráfico, o código acima verifica o valor da chave primária antes de rastrear 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 e não negativo será restaurado e a entidade será rastreada como Deleted.
A saída da execução deste 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, esse código pressupõe que cada entidade tenha uma propriedade de chave primária inteira chamada Id. Isso pode ser codificado em uma classe base abstrata ou interface. Como alternativa, a propriedade ou propriedades da chave primária poderiam ser obtidas dos metadados, IEntityType de modo que esse código funcionasse com qualquer tipo de entidade.
O TrackGraph tem duas sobrecargas. Na sobrecarga simples usada acima, o EF Core determina quando parar de atravessar o gráfico. Deixa de visitar novas entidades relacionadas de uma entidade específica quando essa entidade já está a ser rastreada ou quando a função de retorno não começa a rastrear a entidade.
A sobrecarga avançada, ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>), tem um callback que devolve um booleano. Se o callback retornar false, o percurso do grafo será interrompido, caso contrário, continuará. Deve-se tomar cuidado para evitar ciclos infinitos ao usar esta 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.