Entity Framework 4 - Compreendendo a Concorrência otimista


Quando você usa o Entity Framework 4 em seu projeto você saberia me dizer quais as opções para tratar a concorrência você tem quando muitas pessoas ou processos estão editando os dados de forma concorrente em sua aplicação ?

Vejamos algumas das possíveis abordagens que poderemos dar para contornar este problema e também colocar como o Entity Framework 4 se comporta em cada uma delas:

Abordagem 1- Ignorar os conflitos de concorrência

Por incrível que parece muitos sistemas de pequeno porte simplesmente ignoram esses conflitos. Quando um usuário salva as suas alterações elas são persistidas para o banco de dados independentemente do que qualquer pessoa esta fazendo ou ja fez.

Os comandos que o Entity Framework usa por padrão podem desempenhar um papel interessante nesta abordagem.

Como uma atualização do EF irá atualizar somente os campos que o usuário editou, é possível que a edição concorrente não irá causar um problema.

Vamos desenhar um cenário possível para este caso:

  1. Imagine que o usuário A obtém um registro;
  2. O usuário A inicia a edição do campo X desse registro;
  3. Enquanto isso o usuário B edita o mesmo registro, altera o campo Y e salva as suas alterações; (cara rápido esse hein ?);
  4. Em seguida o usuário A altera o campo X do registro e salva;

O EF entra em cena emitindo um comando para salvar o campo X que estava sendo editado e não vai alterar o campo Y. Dessa forma ambas as alterações feitas pelos usuários serão salvas e estarão garantidas.

Abordagem 2- Forçando os dados do usuário para o Servidor (ClientWins)

Em um sistema projetado para alertá-lo dos conflitos, o sistema irá alertar quando o usuário for tentar salvar seus dados, indicando que o registro foi modificado desde o tempo em que foi inicialmente recuperado.

Essa abordagem é conhecida como ClientWins e não ignora a concorrência; neste cenário o EF emite um comando para atualizar TODOS os valores da entidade, mesmo aqueles que não tenham sido editados.

O impacto desta abordagem no cenário descrito do item anterior é que o campo X e todos os outros campos no registro do banco de dados serão alterados para refletir a versão do usuário dos dados.

Abordagem 3 - Atualizando os dados do usuário com o Servidor de Dados (StoreWins)

Nesta abordagem, quando o conflito for detectado, os dados do usuário serão atualizados a partir do servidor.

A entidade que esta sendo modificada na aplicação será atualizada para refletir a versão atual do servidor de dados. As alterações feitas na edição serão perdidas.

A aplicação pode alertar o usuário e ela pode aplicar as suas edições para o campo novamente ou aplicação pode fazer isso pelo usuário, pois como as mudanças existem na memória, no controle, ou no texto, os valores podem ser salvos novamente.

Se um processo que não envolva um usuário está fazendo as atualizações, você deve aplicar a lógica que não exige que a interface do usuário.

Você pode descobrir se os dados foram alterados no servidor enquanto o usuário está em editando os mesmos dados da seguinte forma:

1 -) Verificando se existem quaisquer mudanças para o registro

Para fazer isso, seria necessário comparar todos os campos na linha para ver se essa linha foi editada.

O EF suporta este recurso usando a propriedade ConcurrencyMode que pode ser definida como Fixed para qualquer propriedade que você deseja verificar.

2-) Verificando se existem alterações de um campo particular

Aqui você precisa se concentrar em apenas um ou mais campos específicos que indicam se uma alteração foi feita. Para fazer isso pode-se utilizar a propriedade rowversion dos campos do banco de dados embora nem todos os SGBD suportem esta propriedade. (Os valores possíveis para rowversion são: Unchanged, Added, Modified, Deleted e Detached)

3-) Verificando se os campos que estão sendo atualizados sofreram alterações;

Nesta abordagem, enquanto o EF faz essa verificação , para efetuar a verificão dos valores originais e dos atuais de suas entidades, você terá que criar consultas adicionais pois o EF não emite comandos para fazer isso.

Implementando a concorrência otimista

Existem duas formas de permitir a concorrência otimista no Entity Framework.

1- A função Update permite que você 'marque' os campos para verificar a concorrência usando qualquer campo que foi marcado com 'Use Orignal Value' e em seguida, verificar a propriedade RowsAffected.

2- A segunda forma de verificar a concorrência com o EF é quando você não estiver usando procedimentos armazenados para as suas atualizações. Usando este recurso requer duas etapas:

a-) Definir qual a propriedade ou propriedades serão utilizadas para efectuar a verificação de simultaneidade.
b-) Manipular o OptimisticConcurrencyException que é acionada quando a verificação falhar.

Como a concorrência é definida em uma base de propriedade por propriedade no EF, o primeiro passo é identificar a propriedade ou propriedades que você irá usar para verificar a concorrência.

A propriedade ConcurrencyMode é usada para marcar uma propriedade para a verificação da concorrência e pode ser encontra na janela Properties. As opções disponíveis são None , a padrão , e Fixed como mostra a figura abaixo:

Ao definir a propriedade ConcurrencyMode como Fixed, você garante que o valor da propriedade é incluída no predicado where quando uma entidade é atualizada ou deletada durante a chamada do método SaveChanges().

Como o Entity Framework utiliza a propriedade ConcurrencyMode

Quando o Object Services prepara um comando Update ou Delete , ele usa quaisquer propriedades marcadas para a verificação da concorrência como um filtro no comando junto com a chave de identificação (identity Key).

Obs: É o Object Services que gerencia todas as alterações feitas nos objetos e gera e executa as instruções T-SQL que irão realizar as operações de inclusão, alteração e exclusão contra a fonte de dados. Tudo isso é feito através da chamada do método SaveChanges do ObjectContext (equivalente ao SubmitChanges do LINQ to SQL).

Com a propriedade ConcurrencyMode de uma RowVersion de um campo definida para Fixed, sempre que o campo for atualizado , o comando Update irá procurar o campo usando a sua EntityKey e sua propriedade RowVersion.

Entendendo a exceção ConcurrencyException

Ao trabalhar com o Entity Framework em aplicações n-camadas onde você está desanexando e anexando entidades de um contexto de objeto, você pode encontrar a exceção:

Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. Refresh ObjectStateManager entries.

Esta exceção esta lhe dizendo que nenhuma linha foi atualizada e existem muitas razões para isso ocorra.

Uma razão simples é que ao anexar entidades a um contexto de objeto, você precisa usar o método AddObject() se a entidade for recém criada (e não tem uma EntityKey), ao passo que você pode usar o método Attach() se a entidade já existir e estiver sendo atualizada.

Para contornar o problema basta verificar se a entidade foi recém-criada para saber qual método usar. O trecho de código a seguir mostra como fazer isso.

No código abaixo, na versão C# e VB .NET, temos que "EntityContainerName" é o nome de seu recipiente Entity Framework e "EntitySetName" é o nome do conjunto de entidades para o qual você está adicionando a entidade:

if (entity.EntityKey == null || entity.EntityKey.IsTemporary)
{
   this.ObjectContext.AddObject("EntityContainerName.EntitySetName", entity);
}
else
{
   this.ObjectContext.Attach(entity);
}
If entity.EntityKey Is Nothing OrElse entity.EntityKey.IsTemporary Then
    Me.ObjectContext.AddObject("EntityContainerName.EntitySetName", entity)
Else
    Me.ObjectContext.Attach(entity)
End If

Note que você precisa ter cuidado ao adicionar as entidades que estão relacionados a objetos de outra entidade, porque Object Services tenta adicionar os objetos relacionados também.

O método ObjectContext.Refresh permite que você atualize as entidades no contexto do banco de dados.

Você pode usar Refresh para forçar as suas atualizações em uma abordagem ClientWins ou StoreWins.

O Refresh tem dois parâmetros:
- O primeiro é RefreshMode, que tem as opções RefreshMode.ClientWins e RefreshMode.StoreWins;
- O segundo parâmetro pode ser uma única entidade ou um IEnumerable de entidades;

Onde IEnumerable pode ser algo como uma lista ou um array, ou mesmo um IQueryable (LINQ para Entidades de consulta) ou ObjectQuery.

Ex: context.Refresh(RefreshMode.ClientWins, aObjeto)

Se o RefreshMode é ClientWins, uma consulta será executada no banco de dados para obter os valores atuais do servidor para a entidade. Em seguida, ele vai atribuir os valores nos valores originais da entidade. Isso fará com que a entidade pense que começou com os valores de servidor e vai construir comandos para proceder a atualização conforme o método SaveChanges for chamado novamente.

using (var context = new MacorattiEntities())
{
     var con = context.Contatos.FirstOrDefault(c => c.Nome == "Macoratti" && c.Email == "macoratti@yahoo.com");
     con.Titulo = "Sr.";
   try
   {
      context.SaveChanges();
   }
   catch (OptimisticConcurrencyException)
   {
   
  //Atualiza a entidade contato,usando ClientWins;
      context.Refresh(RefreshMode.ClientWins, con);
     
//chama SaveChanges novamente
      context.SaveChanges();
    }
 }

Com o valor StoreWins todos os valore atuais e originais da entidade serão substituídos com os dados do servidor. O usuário irá perder as alterações feitas e em vez dos dados em cache serão sincronizados com o banco de dados. As entidades que foram atualizadas terão um estado de Unchaged e serão ignoradas pelo comando SaveChanges ate que sejam editadas novamente.

E assim apresentei uma visão geral do tratamento da concorrência e de suas opções de tratamento usando o Entity Framework.

Aguarde em breve mais artigos com mais detalhes sobre esse assunto...

Eu sei é apenas EF, mas eu gosto...

Referências:

José Carlos Macoratti