Partilhar via


Entidades desconectadas

Uma instância DbContext rastreará automaticamente as entidades retornadas do banco de dados. As alterações feitas nessas entidades serão detetadas 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, em seguida, salvas usando uma instância diferente. Isso geralmente acontece em cenários "desconectados", como um aplicativo Web onde as entidades são consultadas, enviadas ao cliente, modificadas, enviadas de volta ao servidor em uma solicitação e, em seguida, salvas. Neste caso, a segunda instância de contexto precisa saber se as entidades são novas (devem ser inseridas) ou existentes (devem ser atualizadas).

Sugestão

Você pode visualizar a amostra do deste artigo no GitHub.

Sugestão

O EF Core só pode rastrear 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 de curta duração para cada unidade de trabalho, de modo que o contexto comece vazio, tenha entidades anexadas a ele, salve essas entidades e, em seguida, o contexto seja descartado.

Identificação de novas entidades

Cliente identifica novas entidades

O caso mais simples de lidar é quando o cliente informa ao servidor se a entidade é nova ou existente. Por exemplo, muitas vezes a solicitação para inserir uma nova entidade é diferente da solicitação para atualizar uma entidade existente.

O restante desta seção abrange os casos em que é necessário determinar de alguma outra forma se deve ser inserido ou atualizado.

Com chaves geradas automaticamente

O valor de uma chave gerada automaticamente pode muitas vezes ser usado para determinar se uma entidade precisa ser inserida ou atualizada. Se a chave não tiver sido definida (ou seja, ainda tiver o valor padrão CLR de null, zero, etc.), então a entidade deve ser nova e precisa ser inserida. Por outro lado, se o valor da chave foi definido, então ele já deve ter sido salvo anteriormente e agora precisa de atualização. Em outras palavras, se a chave tem um valor, então a entidade foi consultada, enviada ao cliente e agora retornou para ser atualizada.

É fácil verificar se há uma chave não definida 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;

Sugestão

As chaves são definidas assim que as entidades são controladas pelo contexto, mesmo que a entidade esteja no estado Adicionado. Isso ajuda ao percorrer um gráfico de entidades e decidir o que fazer com cada uma, como ao usar a API do TrackGraph. O valor da chave só deve ser usado da maneira mostrada aqui antes de qualquer chamada ser feita para rastrear a entidade.

Com outras teclas

É necessário algum outro mecanismo para identificar novas entidades quando os valores-chave não são gerados automaticamente. Existem duas abordagens gerais a este respeito:

  • Consulta para a 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 passar um flag de um cliente. Em um aplicativo Web, isso geralmente significa fazer solicitações diferentes para ações diferentes ou passar algum estado na solicitação e, em seguida, extraí-la no controlador.

Salvar entidades únicas

Se se souber se é ou não necessária uma inserção ou atualização, entã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 para inserir. 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 deve 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();
}

Os passos aqui são:

  • Se Find retornar null, o banco de dados ainda não contém o blog com essa ID, por isso chamamos Add mark it para inserção.
  • Se Find retornar uma entidade, 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 nessa entidade para aquelas que vieram do cliente.
    • A chamada SetValues marcará a entidade a ser atualizada conforme necessário.

Sugestão

SetValues marcará apenas como modificadas as propriedades que têm valores diferentes daqueles na entidade controlada. Isso significa que, quando a atualização for enviada, apenas as colunas que realmente foram alteradas serão atualizadas. (E se nada tiver mudado, nenhuma atualização será enviada.)

Trabalhar com gráficos

Resolução de identidade

Como observado acima, o EF Core só pode rastrear uma instância de qualquer entidade com um determinado valor de chave primária. Ao trabalhar com gráficos, o gráfico deve idealmente ser criado de modo a que este invariante seja mantido, e o contexto deve ser usado para apenas uma unidade de trabalho. Se o gráfico contiver duplicatas, será necessário processá-lo 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 em seu pipeline de aplicativos para evitar a resolução de conflitos.

Todas as entidades novas/todas as entidades existentes

Um exemplo de trabalho com gráficos é inserir ou atualizar um blog juntamente com sua coleção de postagens associadas. Se todas as entidades no gráfico devem ser inseridas, ou todas devem ser atualizadas, então o processo é o mesmo descrito acima para entidades individuais. Por exemplo, um gráfico de blogs e posts 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 assim:

public static async Task InsertGraph(DbContext context, object rootEntity)
{
    context.Add(rootEntity);
    await context.SaveChangesAsync();
}

A chamada para Adicionar marcará o blog e todos os posts a serem inseridos.

Da mesma forma, se todas as entidades em um gráfico precisam ser atualizadas, então Update pode ser usado:

public static async Task UpdateGraph(DbContext context, object rootEntity)
{
    context.Update(rootEntity);
    await context.SaveChangesAsync();
}

O blog e todos os seus posts serão marcados para serem atualizados.

Combinação de entidades novas e existentes

Com chaves geradas automaticamente, Update pode ser usado novamente para inserções e atualizações, mesmo que o gráfico 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 no gráfico, blog ou postagem, para inserção se não tiver um valor de chave definido, enquanto todas as outras entidades estã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();
}

Tratamento de exclusões

Excluir pode ser difícil de gerir, pois muitas vezes a ausência de uma entidade significa que ela deve ser eliminada. Uma maneira de lidar com isso é usar "exclusões suaves" de modo que a entidade seja marcada como excluída em vez de realmente ser excluída. Eliminar torna-se então o mesmo que atualizar. As exclusões suaves 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 executar o que é essencialmente uma comparaçã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, Adicionar, Anexar e Atualizar utilizam o percurso de grafos com uma determinação feita para cada entidade sobre se ela deve ser marcada como Adicionada (para inserir), Modificada (para atualizar), Inalterada (não fazer nada), ou Eliminada (para eliminar). Este mecanismo é exposto através da API do TrackGraph. Por exemplo, vamos supor que quando o cliente envia de volta um gráfico de entidades, ele define algum sinalizador em cada entidade indicando como ela deve ser tratada. O TrackGraph pode então 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, as bandeiras fariam parte de um DTO ou de algum outro estado incluído na solicitação.