Partilhar via


Como tratar conflitos de simultaneidade (EF6)

A simultaneidade otimista envolve a tentativa otimista de salvar sua entidade no banco de dados na esperança de que os dados não sejam alterados desde que a entidade foi carregada. Se os dados forem alterados, uma exceção será gerada e você deverá resolver o conflito antes de tentar salvar novamente. Este tópico aborda como lidar com essas exceções no Entity Framework. As técnicas mostradas neste tópico se aplicam igualmente a modelos criados com o Code First e com o EF Designer.

Essa postagem não é o lugar apropriado para uma discussão aprofundada sobre simultaneidade otimista. As seções a seguir pressupõem que você tenha um pouco de conhecimento sobre resolução de simultaneidade e mostram padrões para tarefas comuns.

Muitos desses padrões usam os tópicos discutidos em Como Trabalhar com Valores de Propriedade.

Resolver problemas de simultaneidade quando você está usando associações independentes (em que a chave estrangeira não é mapeada para uma propriedade em sua entidade) é muito mais difícil do que quando você está usando associações de chave estrangeira. Portanto, se você vai fazer a resolução de simultaneidade em seu aplicativo, é recomendável que você sempre mapeie chaves estrangeiras para suas entidades. Todos os exemplos a seguir pressupõem que você esteja usando associações de chave estrangeira.

Um DbUpdateConcurrencyException é gerado por SaveChanges quando uma exceção de simultaneidade otimista é detectada ao tentar salvar uma entidade que usa associações de chave estrangeira.

Resolução de exceções de simultaneidade otimistas com Recarregamento (o banco de dados vence)

O método Reload pode ser usado para substituir os valores atuais da entidade com os valores agora no banco de dados. Em seguida, a entidade normalmente é entregue ao usuário de alguma forma e ele deve tentar fazer suas alterações novamente e salvar novamente. Por exemplo:

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);
    blog.Name = "The New ADO.NET Blog";

    bool saveFailed;
    do
    {
        saveFailed = false;

        try
        {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;

            // Update the values of the entity that failed to save from the store
            ex.Entries.Single().Reload();
        }

    } while (saveFailed);
}

Uma boa maneira de simular uma exceção de simultaneidade é definir um ponto de interrupção na chamada SaveChanges e modificar uma entidade que está sendo salva no banco de dados usando outra ferramenta, como o SQL Server Management Studio. Você também pode inserir uma linha antes do SaveChanges para atualizar o banco de dados diretamente usando SqlCommand. Por exemplo:

context.Database.SqlCommand(
    "UPDATE dbo.Blogs SET Name = 'Another Name' WHERE BlogId = 1");

O método Entries em DbUpdateConcurrencyException retorna as instâncias DbEntityEntry para as entidades que não foram atualizadas. (Atualmente, essa propriedade sempre retorna um único valor para problemas de simultaneidade. Ela pode retornar vários valores para exceções gerais de atualização.) Uma alternativa para algumas situações pode ser obter entradas para todas as entidades que talvez precisem ser recarregadas do banco de dados e recarregar chamadas para cada uma delas.

Como resolver exceções de simultaneidade otimistas à medida que o cliente vence

O exemplo acima que usa Reload, às vezes, é chamado de ganhos de banco de dados ou ganhos do repositório porque os valores na entidade são substituídos por valores do banco de dados. Às vezes você pode querer fazer o contrário e substituir os valores no banco de dados pelos valores atualmente na entidade. Isso às vezes é chamado de vitória do cliente e pode ser feito obtendo os valores atuais do banco de dados e definindo-os como os valores originais para a entidade. (Consulte Como Trabalhar com Valores de Propriedade para obter informações sobre valores atuais e originais.) Por exemplo:

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);
    blog.Name = "The New ADO.NET Blog";

    bool saveFailed;
    do
    {
        saveFailed = false;
        try
        {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;

            // Update original values from the database
            var entry = ex.Entries.Single();
            entry.OriginalValues.SetValues(entry.GetDatabaseValues());
        }

    } while (saveFailed);
}

Resolução personalizada de exceções de simultaneidade otimistas

Talvez você queria combinar os valores atualmente no banco de dados com os valores atualmente na entidade. Isso geralmente requer alguma lógica personalizada ou interação do usuário. Por exemplo, você pode apresentar um formulário para o usuário que contém os valores atuais, os valores no banco de dados e um conjunto padrão de valores resolvidos. Em seguida, o usuário editaria os valores resolvidos conforme necessário e seriam esses valores resolvidos que seriam salvos no banco de dados. Isso pode ser feito usando os objetos DbPropertyValues retornados de CurrentValues e GetDatabaseValues na entrada da entidade. Por exemplo:

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);
    blog.Name = "The New ADO.NET Blog";

    bool saveFailed;
    do
    {
        saveFailed = false;
        try
        {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;

            // Get the current entity values and the values in the database
            var entry = ex.Entries.Single();
            var currentValues = entry.CurrentValues;
            var databaseValues = entry.GetDatabaseValues();

            // Choose an initial set of resolved values. In this case we
            // make the default be the values currently in the database.
            var resolvedValues = databaseValues.Clone();

            // Have the user choose what the resolved values should be
            HaveUserResolveConcurrency(currentValues, databaseValues, resolvedValues);

            // Update the original values with the database values and
            // the current values with whatever the user choose.
            entry.OriginalValues.SetValues(databaseValues);
            entry.CurrentValues.SetValues(resolvedValues);
        }
    } while (saveFailed);
}

public void HaveUserResolveConcurrency(DbPropertyValues currentValues,
                                       DbPropertyValues databaseValues,
                                       DbPropertyValues resolvedValues)
{
    // Show the current, database, and resolved values to the user and have
    // them edit the resolved values to get the correct resolution.
}

Resolução personalizada de exceções de simultaneidade otimistas usando objetos

O código acima usa instâncias DbPropertyValues para transmitir valores atuais, de banco de dados e resolvidos. Às vezes, pode ser mais fácil usar instâncias do tipo de entidade para isso. Isso pode ser feito usando os métodos ToObject e SetValues de DbPropertyValues. Por exemplo:

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);
    blog.Name = "The New ADO.NET Blog";

    bool saveFailed;
    do
    {
        saveFailed = false;
        try
        {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;

            // Get the current entity values and the values in the database
            // as instances of the entity type
            var entry = ex.Entries.Single();
            var databaseValues = entry.GetDatabaseValues();
            var databaseValuesAsBlog = (Blog)databaseValues.ToObject();

            // Choose an initial set of resolved values. In this case we
            // make the default be the values currently in the database.
            var resolvedValuesAsBlog = (Blog)databaseValues.ToObject();

            // Have the user choose what the resolved values should be
            HaveUserResolveConcurrency((Blog)entry.Entity,
                                       databaseValuesAsBlog,
                                       resolvedValuesAsBlog);

            // Update the original values with the database values and
            // the current values with whatever the user choose.
            entry.OriginalValues.SetValues(databaseValues);
            entry.CurrentValues.SetValues(resolvedValuesAsBlog);
        }

    } while (saveFailed);
}

public void HaveUserResolveConcurrency(Blog entity,
                                       Blog databaseValues,
                                       Blog resolvedValues)
{
    // Show the current, database, and resolved values to the user and have
    // them update the resolved values to get the correct resolution.
}