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.
Uma instância DbContext controlará automaticamente as entidades retornadas do banco de dados. As alterações feitas nessas entidades serão detectadas quando SaveChanges for chamado e o banco de dados será atualizado conforme necessário. Consulte Salvar Básico e Dados Relacionados para obter detalhes.
No entanto, às vezes, as entidades são consultadas usando uma instância de contexto e salvas usando uma instância diferente. Isso geralmente ocorre em cenários "desconectados", como um aplicativo Web em que as entidades são consultadas, enviadas ao cliente, modificadas, enviadas de volta ao servidor em uma solicitação e salvas. Nesse caso, a segunda instância de contexto precisa saber se as entidades são novas (devem ser inseridas) ou existentes (devem ser atualizadas).
Dica
Você pode exibir o exemplo deste artigo no GitHub.
Dica
O EF Core só pode acompanhar uma instância de qualquer entidade com um determinado valor de chave primária. A melhor maneira de evitar que isso seja um problema é usar um contexto temporário para cada unidade de trabalho, de modo que o contexto comece vazio, tenha entidades anexadas a ele, salve-as e, em seguida, o contexto seja finalizado e descartado.
Identificando novas entidades
O cliente identifica novas entidades
O caso mais simples com o qual lidar é quando o cliente informa ao servidor se a entidade é nova ou existente. Por exemplo, geralmente, a solicitação para inserir uma nova entidade é diferente da solicitação para atualizar uma entidade existente.
O restante desta seção aborda os casos em que é necessário determinar de alguma outra forma se deseja inserir ou atualizar.
Com chaves geradas automaticamente
O valor de uma chave gerada automaticamente geralmente pode ser usado para determinar se uma entidade precisa ser inserida ou atualizada. Se a chave não tiver sido definida (ou seja, ela ainda terá o valor padrão CLR de nulo, zero etc.), a entidade deverá ser nova e precisa ser inserida. Por outro lado, se o valor da chave tiver sido definido, ele já deve ter sido salvo anteriormente e agora precisa ser atualizado. Em outras palavras, se a chave tem um valor, isso significa que a entidade foi consultada, enviada ao cliente, e agora retornou para ser atualizada.
É fácil verificar se há uma chave não configurada quando o tipo de entidade é conhecido:
public static bool IsItNew(Blog blog)
=> blog.BlogId == 0;
No entanto, o EF também tem uma maneira interna de fazer isso para qualquer tipo de entidade e tipo de chave:
public static bool IsItNew(DbContext context, object entity)
=> !context.Entry(entity).IsKeySet;
Dica
As chaves são definidas assim que o contexto rastreia as entidades, mesmo que a entidade esteja no estado Adicionado. Isso ajuda ao percorrer um grafo de entidades e decidir o que fazer com cada uma delas, como ao usar a API do TrackGraph. O valor da chave só deve ser usado da maneira mostrada aqui antes que qualquer chamada seja feita para acompanhar a entidade.
Com outras chaves
Algum outro mecanismo é necessário para identificar novas entidades quando os valores de chave não são gerados automaticamente. Há duas abordagens gerais para isso:
- Consulta de entidade
- Passar um sinalizador do cliente
Para consultar a entidade, basta usar o método Find:
public static async Task<bool> IsItNew(BloggingContext context, Blog blog)
=> (await context.Blogs.FindAsync(blog.BlogId)) == null;
Está além do escopo deste documento mostrar o código completo para enviar uma flag a partir de um cliente. Em um aplicativo Web, geralmente significa fazer solicitações diferentes para ações diferentes ou passar algum estado na solicitação e extraí-lo no controlador.
Salvando entidades individuais
Se souber se uma inserção ou atualização é necessária ou não, adicionar ou atualizar pode ser usado adequadamente:
public static async Task Insert(DbContext context, object entity)
{
context.Add(entity);
await context.SaveChangesAsync();
}
public static async Task Update(DbContext context, object entity)
{
context.Update(entity);
await context.SaveChangesAsync();
}
No entanto, se a entidade usar valores de chave gerados automaticamente, o método Update poderá ser usado para ambos os casos:
public static async Task InsertOrUpdate(DbContext context, object entity)
{
context.Update(entity);
await context.SaveChangesAsync();
}
O método Update normalmente marca a entidade para atualização, não inserção. No entanto, se a entidade tiver uma chave gerada automaticamente e nenhum valor de chave tiver sido definido, a entidade será marcada automaticamente para inserção.
Se a entidade não estiver usando chaves geradas automaticamente, o aplicativo deverá decidir se a entidade deve ser inserida ou atualizada: Por exemplo:
public static async Task InsertOrUpdate(BloggingContext context, Blog blog)
{
var existingBlog = await context.Blogs.FindAsync(blog.BlogId);
if (existingBlog == null)
{
context.Add(blog);
}
else
{
context.Entry(existingBlog).CurrentValues.SetValues(blog);
}
await context.SaveChangesAsync();
}
As etapas aqui são:
- Se Find retorna nulo, o banco de dados ainda não contém o blog com este ID, portanto, chamamos o método Add para marcá-lo para inserção.
- Se o método Find retornar uma entidade, então ela existe no banco de dados e o contexto agora está rastreando a entidade existente.
- Em seguida, usamos SetValues para definir os valores de todas as propriedades dessa entidade para as que vieram do cliente.
- A chamada SetValues indicará a entidade a ser atualizada quando necessário.
Dica
SetValues marcará apenas como modificadas as propriedades que têm valores diferentes para as da entidade controlada. Isso significa que, quando a atualização for enviada, somente as colunas que realmente foram alteradas serão atualizadas. (E se nada tiver mudado, nenhuma atualização será enviada.)
Trabalhando com grafos
Resolução de identidade
Conforme observado acima, o EF Core só pode acompanhar uma instância de qualquer entidade com um determinado valor de chave primária. Ao trabalhar com grafos, o grafo deve ser criado idealmente para que esse invariável seja mantido e o contexto deve ser usado para apenas uma unidade de trabalho. Se o grafo contiver duplicatas, será necessário processar o grafo antes de enviá-lo ao EF para consolidar várias instâncias em uma. Isso pode não ser trivial quando as instâncias têm valores e relações conflitantes, portanto, a consolidação de duplicatas deve ser feita o mais rápido possível no pipeline do aplicativo para evitar a resolução de conflitos.
Todas as entidades novas/todas existentes
Um exemplo de como trabalhar com grafos é inserir ou atualizar um blog junto com sua coleção de postagens associadas. Se todas as entidades no grafo devem ser inseridas ou todas devem ser atualizadas, o processo será o mesmo descrito acima para entidades individuais. Por exemplo, um grafo de blogs e postagens criados assim:
var blog = new Blog
{
Url = "http://sample.com", Posts = new List<Post> { new Post { Title = "Post 1" }, new Post { Title = "Post 2" }, }
};
pode ser inserido da seguinte maneira:
public static async Task InsertGraph(DbContext context, object rootEntity)
{
context.Add(rootEntity);
await context.SaveChangesAsync();
}
A chamada para Adicionar marcará o blog e todas as postagens a serem inseridas.
Da mesma forma, se todas as entidades em um grafo precisarem ser atualizadas, a atualização poderá ser usada:
public static async Task UpdateGraph(DbContext context, object rootEntity)
{
context.Update(rootEntity);
await context.SaveChangesAsync();
}
O blog e todas as suas postagens serão marcadas para serem atualizadas.
Combinação de entidades novas e existentes
Com chaves geradas automaticamente, a atualização pode ser usada novamente para inserções e atualizações, mesmo que o grafo contenha uma combinação de entidades que exigem inserção e aquelas que exigem atualização:
public static async Task InsertOrUpdateGraph(DbContext context, object rootEntity)
{
context.Update(rootEntity);
await context.SaveChangesAsync();
}
A atualização marcará qualquer entidade nos grafos, blogs ou postagens para inserção se ela não tiver um conjunto de valores de chave, enquanto isso, todas as outras entidades serão marcadas para atualização.
Como antes, quando não estiver usando chaves geradas automaticamente, uma consulta e algum processamento podem ser usados:
public static async Task InsertOrUpdateGraph(BloggingContext context, Blog blog)
{
var existingBlog = await context.Blogs
.Include(b => b.Posts)
.FirstOrDefaultAsync(b => b.BlogId == blog.BlogId);
if (existingBlog == null)
{
context.Add(blog);
}
else
{
context.Entry(existingBlog).CurrentValues.SetValues(blog);
foreach (var post in blog.Posts)
{
var existingPost = existingBlog.Posts
.FirstOrDefault(p => p.PostId == post.PostId);
if (existingPost == null)
{
existingBlog.Posts.Add(post);
}
else
{
context.Entry(existingPost).CurrentValues.SetValues(post);
}
}
}
await context.SaveChangesAsync();
}
Gerenciamento de exclusões
A exclusão pode ser difícil de gerenciar, pois muitas vezes a ausência de uma entidade significa que ela deve ser excluída. Uma maneira de lidar com isso é usar "exclusões temporárias" de modo que a entidade seja marcada como excluída em vez de realmente ser excluída. As exclusões então tornam-se equivalentes às atualizações. Exclusões temporárias podem ser implementadas usando filtros de consulta.
Para exclusões verdadeiras, um padrão comum é usar uma extensão do padrão de consulta para realizar uma diferenciação de grafos. Por exemplo:
public static async Task InsertUpdateOrDeleteGraph(BloggingContext context, Blog blog)
{
var existingBlog = await context.Blogs
.Include(b => b.Posts)
.FirstOrDefaultAsync(b => b.BlogId == blog.BlogId);
if (existingBlog == null)
{
context.Add(blog);
}
else
{
context.Entry(existingBlog).CurrentValues.SetValues(blog);
foreach (var post in blog.Posts)
{
var existingPost = existingBlog.Posts
.FirstOrDefault(p => p.PostId == post.PostId);
if (existingPost == null)
{
existingBlog.Posts.Add(post);
}
else
{
context.Entry(existingPost).CurrentValues.SetValues(post);
}
}
foreach (var post in existingBlog.Posts)
{
if (!blog.Posts.Any(p => p.PostId == post.PostId))
{
context.Remove(post);
}
}
}
await context.SaveChangesAsync();
}
TrackGraph
Internamente, os métodos Adicionar, Anexar e Atualizar utilizam a travessia de grafo, com uma análise feita para cada entidade sobre se ela deve ser marcada como Adicionada (para inserir), Modificada (para atualizar), Inalterada (para não fazer nada) ou Excluída (para deletar). Esse mecanismo é exposto por meio da API do TrackGraph. Por exemplo, vamos supor que, quando o cliente envia de volta um grafo de entidades, ele define algum sinalizador em cada entidade indicando como ele deve ser tratado. Em seguida, o TrackGraph pode ser usado para processar este sinalizador:
public static async Task SaveAnnotatedGraph(DbContext context, object rootEntity)
{
context.ChangeTracker.TrackGraph(
rootEntity,
n =>
{
var entity = (EntityBase)n.Entry.Entity;
n.Entry.State = entity.IsNew
? EntityState.Added
: entity.IsChanged
? EntityState.Modified
: entity.IsDeleted
? EntityState.Deleted
: EntityState.Unchanged;
});
await context.SaveChangesAsync();
}
As bandeiras são mostradas apenas como parte da entidade para simplicidade do exemplo. Normalmente, os sinalizadores seriam parte de um DTO ou algum outro estado incluído na solicitação.