Entidades desconectadas

Uma instância DbContext automaticamente controlará as entidades retornadas do banco de dados. As alterações feitas a essas entidades serão detectadas quando SaveChanges for chamado e o banco de dados será atualizado conforme o necessário. Veja Salvamento 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 ocorre em cenários "desconectados", por exemplo, um aplicativo da Web onde as entidades são consultadas, enviadas ao cliente, modificadas, enviadas de volta para o servidor em uma solicitação e salvas. Nesse caso, o contexto da segunda instância precisa saber se as entidades são novas (devem ser inseridas) ou existentes (devem ser atualizadas).

Dica

Veja o exemplo deste artigo no GitHub.

Dica

O EF Core só pode controlar uma instância de qualquer entidade com um determinado valor de chave primária. A melhor maneira de evitar que esse seja um problema é usar um contexto de curta duração para cada unidade de trabalho, de modo que o contexto começa vazio, tem entidades anexadas a ele, salva essas entidades e, em seguida, o contexto é descartado.

Identificando novas entidades

O cliente identifica novas entidades

O caso mais simples para lidar com isso é 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 maneira se será feita inserção ou atualização.

Com as 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 tem o valor padrão CLR de nulo, zero etc.), a entidade deverá ser nova e precisa de inserção. Por outro lado, se o valor da chave tiver sido definido, ele já deverá ter sido salvo anteriormente e agora precisa ser atualizado. Em outras palavras, se a chave tem um valor, a entidade foi consultada, enviada para o cliente e agora volta para ser atualizada.

É fácil verificar se há uma chave não definida quando o tipo de entidade é desconhecido:

public static bool IsItNew(Blog blog)
    => blog.BlogId == 0;

No entanto, o EF também tem uma forma interna de fazer isso para qualquer tipo de entidade e o tipo de chave:

public static bool IsItNew(DbContext context, object entity)
    => !context.Entry(entity).IsKeySet;

Dica

As chaves são definidas assim que as entidades são controladas pelo contexto, mesmo se a entidade estiver no estado adicionado. Isso ajuda ao passar um gráfico de entidades e decidir o que fazer com cada, por exemplo, ao usar a API TrackGraph. O valor da chave somente deve ser usado da forma mostrada aqui antes que alguma chamada seja feita para controlar 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 para a entidade
  • Passar um sinalizador do cliente

Para consultar para a entidade, use apenas o método de localização:

public static bool IsItNew(BloggingContext context, Blog blog)
    => context.Blogs.Find(blog.BlogId) == null;

Está além do escopo deste documento mostrar o código completo para passar um sinalizador de um cliente. Em um aplicativo Web, isso geralmente significa fazer solicitações diferentes para diferentes ações, ou passar algum estado na solicitação e, em seguida, extraí-lo no controlador.

Como salvar entidades simples

Se ele for conhecido ou não, uma inserção ou atualização será necessária, em seguida, adicionar ou atualizar pode ser usado de forma apropriada:

public static void Insert(DbContext context, object entity)
{
    context.Add(entity);
    context.SaveChanges();
}

public static void Update(DbContext context, object entity)
{
    context.Update(entity);
    context.SaveChanges();
}

No entanto, se a entidade usar valores de chave gerada automaticamente, o método de atualização poderá ser usado para ambos os casos:

public static void InsertOrUpdate(DbContext context, object entity)
{
    context.Update(entity);
    context.SaveChanges();
}

O método de atualização normalmente marca a entidade para a 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á automaticamente marcada para inserção.

Se a entidade não estiver usando as chaves geradas automaticamente, o aplicativo deverá decidir se a entidade deve ser inserida ou atualizada. Por exemplo:

public static void InsertOrUpdate(BloggingContext context, Blog blog)
{
    var existingBlog = context.Blogs.Find(blog.BlogId);
    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
    }

    context.SaveChanges();
}

As etapas aqui são:

  • Se Localizar retornar nulo, isso significa que o banco de dados ainda não contém o blog com essa ID; portanto, chamamos Adicionar e a marcaremos para inserção.
  • Se Localizar retornar uma entidade, ela existirá no banco de dados e o contexto agora é controlar a entidade existente
    • Em seguida, usamos SetValues para definir os valores de todas as propriedades nessa entidade para os estados que vieram do cliente.
    • A chamada SetValues marcará a entidade para ser atualizada conforme o necessário.

Dica

SetValues somente marcará como modificadas as propriedades que têm valores diferentes para aqueles na entidade controlada. Isso significa que, quando a atualização é enviada, somente as colunas que realmente foram alteradas serão atualizadas. (E, se nada foi alterado, nenhuma atualização será enviada).

Como trabalhar com gráficos

Resolução de identidade

Conforme observado acima, o EF Core só pode controlar uma instância de qualquer entidade com um determinado valor de chave primária. Ao trabalhar com elementos gráficos, o gráfico deve ser criado idealmente de modo que essa constante seja mantida e o contexto deve ser usado para apenas uma unidade de trabalho. Se o gráfico contiver duplicatas, será necessário processar o gráfico antes de enviá-lo ao EF para consolidar várias instâncias em uma. Isso pode não ser trivial onde as instâncias têm valores e relações conflitantes, de modo que consolidar duplicatas deve ser feito assim que possível em seu pipeline de aplicativo para evitar a resolução de conflitos.

Todas as entidades novas ou existentes

Um exemplo de como trabalhar com elementos gráficos é inserir ou atualizar um blog junto com sua coleção de postagens associadas. Se todas as entidades no gráfico tiverem que ser inseridas, ou todas tiverem que ser atualizadas, o processo será o mesmo descrito acima para entidades únicas. Por exemplo, um gráfico de blogs e postagens criados desta forma:

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 void InsertGraph(DbContext context, object rootEntity)
{
    context.Add(rootEntity);
    context.SaveChanges();
}

A chamada para adicionar marcará o blog e todas as postagens a serem inseridas.

Da mesma forma, se todas as entidades em um gráfico precisarem ser atualizados, atualização pode ser usado:

public static void UpdateGraph(DbContext context, object rootEntity)
{
    context.Update(rootEntity);
    context.SaveChanges();
}

O blog e todas as suas postagens serão marcados para serem atualizados.

Combinação de entidades novas e existentes

Com as chaves geradas automaticamente, a atualização pode novamente ser usada para inserções e atualizações, mesmo se o gráfico contiver uma mistura de entidades que exigem inserção e as que precisam de atualização:

public static void InsertOrUpdateGraph(DbContext context, object rootEntity)
{
    context.Update(rootEntity);
    context.SaveChanges();
}

A atualização marcará qualquer entidade no gráfico, blog ou postagem para inserção se não tiver um conjunto de valores de chave, enquanto todas as outras entidades estejam marcadas para atualização.

Como antes, quando não estiver usando as chaves geradas automaticamente, uma consulta e algum processamento poderão ser usados:

public static void InsertOrUpdateGraph(BloggingContext context, Blog blog)
{
    var existingBlog = context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefault(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);
            }
        }
    }

    context.SaveChanges();
}

Tratamento de exclusões

A exclusão pode ser complicada de lidar porque geralmente a ausência de uma entidade significa que ela deve ser excluída. Uma maneira de lidar com isso é usar "exclusões a quente", de modo que a entidade seja marcada como excluída, em vez de ser excluída de fato. Exclui e, em seguida, torna-se o mesmo que as atualizações. As 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 executar o que é essencialmente uma diferença de gráfico. Por exemplo:

public static void InsertUpdateOrDeleteGraph(BloggingContext context, Blog blog)
{
    var existingBlog = context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefault(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);
            }
        }
    }

    context.SaveChanges();
}

TrackGraph

Internamente, adicionar, anexar e atualizar usam percurso de gráfico com uma determinação feita para cada entidade como se ela deve ser marcada como adicionada (para inserir), modificada (para atualizar), inalterada (não fazer nada) ou excluída (para excluir). Esse mecanismo é exposto por meio da API TrackGraph. Por exemplo, vamos supor que, quando o cliente envia de volta um gráfico de entidades, ele define alguns sinalizadores em cada entidade indicando como ela deve ser tratada. O TrackGraph pode ser usado para processar esse sinalizador:

public static void 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;
        });

    context.SaveChanges();
}

Os sinalizadores são mostrados apenas como parte da entidade para manter a simplicidade do exemplo. Normalmente, os sinalizadores devem fazer parte de um DTO ou algum outro estado incluído na solicitação.