Entity Framework - Tratando o problema da concorrência de dados - I (início)


A concorrência é o ato de permitir que múltiplas entidades possam realizar múltiplas tarefas em um banco de dados ao mesmo tempo.

Afim de se tornar útil, um banco de dados precisa fornecer algum nível de concorrência para realizar as operações CRUD - Create, Read, Update e Delete.

A questão da concorrência é um tanto complexa e existem diferentes formas de implementá-la; existem diferentes métodos para assegurar que a concorrência atenda aos princípios da Atomicidade, Consistência, Isolamento e Durabilidade (ACID).

Propriedades ACID de Transações
  1. Atomicidade: A execução de uma transação deve ser atômica, ou todas as ações são executadas, ou nenhuma é;
  2. Consistência: Cada transação executada isoladamente deve preservar a consistência do banco de dados;
  3. Isolamento: Cada transação deve ser isolada dos efeitos da execução concorrente de outras transações;
  4. Durabilidade: Toda transação que for finalizada de forma bem-sucedida deve persistir seus resultados em banco mesmo na presença de falhas no sistema;

Neste artigo eu vou tratar a concorrência do ponto de vista do Entity Framework envolvendo a concorrência otimista e pessimista. Veremos como a combinação do Entity Framework, da plataforma .NET e do Microsoft SQL Server pode reduzir a complexidade no tratamento da concorrência.

Visualizando os problemas de concorrência do banco de dados

O motivo para que os bancos de dados sejam tão úteis é que eles permitem que qualquer número de pessoas compartilhem as mesmas informações. Á medida que o número usuários aumenta os problemas de concorrência também aumentam. A seguir temos uma lista dos problemas de concorrências mais comuns:

Esses cenários ocorrem com relativa frequência com apenas dois usuários, assim você pode imaginar o que acontece quando 40 ou 50 usuários estão competindo pelos mesmos registros. Em muitos casos, mesmo para grandes banco de dados, os usuários estão competindo para controlar os mesmos registros e se você não der atenção a esse detalhe pode comprometer a integridade dos seus dados.

Existem muitas situações onde você não precisa se preocupar com a concorrência. Assim o usuário A e o usuário B podem ambos ler o mesmo registro ao mesmo tempo sem problemas. O cenário muda quando a competição envolve operações de atualizações de dados em um ambiente concorrente. Por isso é melhor ter em mente uma lista dos potenciais cenários e seus efeitos no banco de dados de forma que você possa escolher as políticas de tratamento da concorrência que melhor se ajustam à maior parte dos usuários na maior parte do tempo possível.

A seguir temos alguns exemplos de como você pode tratar os problemas de concorrência:

Não existem soluções mágicas para a concorrência. Cada política adotada para tratar a concorrência tem seus prós e seus contras.

Além disso você deve considerar as duas formas de concorrência que o Entity Framework suporta : a pessimista(somente indiretamente) e a otimista.

A seguir temos uma relação abordando ambas as formas e informando os prós e contras de utilizar cada uma delas:

Considerando os problemas da concorrência otimista

A concorrência otimista não depende de bloqueios para garantir a integridade do banco de dados. Sendo a opção preferida pelos desenvolvedores por ser mais flexível e escalável que a concorrência pessimista. O principal ponto de discórdia na concorrência otimista é que você precisa fornecer meios de resolver os conflitos quando eles ocorrerem. Os conflitos ocorrem sempre quando duas partes interagem com um registro ao mesmo tempo. Abaixo temos as formas mais comuns de tratar com este problema:

  1. Rejeitar a alteração completamente e solicitar ao usuário para trabalhar com um novo registro;
  2. Atualizar somente as colunas dos registros que não foram alteradas desde que o registro foi lido e então perguntar ao usuário sobre as colunas que já foram atualizadas;
  3. Atualizar automaticamente as colunas do registro que não foram alteradas desde que o registro foi lido e então perguntar ao usuário sobre qualquer coluna que já tenha sido atualizada;
  4. Perguntar ao usuário sobre cada coluna atualizada do registro para determinar quais os dados manter;
  5. Ignorar o conflito completamente;
  6. Sobrescrever qualquer alteração com todos os dados do registro atualizado depois de solicitar permissão ao usuário;
  7. Sobrescrever automaticamente qualquer alteração com todos os dados do registro atualizado sem solicitar permissão;

Cada uma dessas soluções para o problema da concorrência apresenta problemas. Não existe uma solução perfeita mas apenas a solução que funciona melhor em uma certa situação com uma aplicação específica.

Implementando a concorrência otimista em uma aplicação

Existem diversos métodos que podemos usar para implementar a concorrência em uma aplicação como verificar a versão da linha que não sofreu alteração. Vejamos a seguir um exemplo de como o Entity Framework trata a concorrência.

Com o objetivo de tornar o exemplo simples e fácil de entender vamos criar um ambiente de teste onde teremos dois usuários: O usuário A e o B.

Neste cenário vamos realizar diversas operações usando esses usuários para simular a concorrência e verificar como uma estratégia particular de concorrência ocorre.

Podemos dessa forma ajustar o código para obter um efeito especial como parte da estratégia de concorrência.

Desenvolvendo um ambiente de teste

Como eu já escrevi, afim de tornar fácil a compreensão de como a concorrência funciona precisamos de um ambiente de testes.

O ambiente de testes deverá permitir dois usuários: A e B.

Usando esses usuários vamos tentar realizar diversas mudanças e ver como uma estratégia de concorrência específica funciona.

Abra o Visual Studio 2012 for windows desktop e no menu FILE clique em New Project;

Selecione a linguagem C# e o template Windows Forms Application e informe o nome TestandoConcorrencia;

Agora no menu PROJECT clique em Add Windows Forms e informe o nome AtualizaRegistro.cs;

Dessa forma temos agora dois formulários em nosso projeto: Form1.cs e AtualizaRegistro.cs.

Selecione o formulário form1.cs e inclua a partir da ToolBox um controle Button (name = btnConcorrencia e Text = Testando a Concorrência) conforme o leiaute abaixo:

A seguir no evento Click do Button inclua o código a seguir :

 private void btnConcorrencia_Click(object sender, EventArgs e)
 {
            // Cria a caixa de Diálogo Usuario A 
            AtualizaRegistro UsuarioA = new AtualizaRegistro();
            UsuarioA.Text = "Usuário A - Atualizar";
            UsuarioA.Show(this);
            // Cria a caixa de Diálogo Usuario B
            AtualizaRegistro UsuarioB = new AtualizaRegistro();
            UsuarioB.Text = "Usuário B - Atualizar";
            UsuarioB.Show(this);
 }

Após isso abra o selecione formulário AtualizaRegistro e inclua a partir da ToolBox os seguintes controles no formulário:

Disponha os controles conforme o leiaute da figura a seguir:

Definindo o modelo de entidades usando o fluxo Model First

No menu PROJECT clique em Add New Item e a seguir selecione o template ADO .NET Entity Data Model informando o nome Concorrencia.edmx e clicando no botão Add;

A seguir selecione a opção Empty model pois iremos criar o nosso modelo definindo as entidades no descritor do EDM;

Fazemos esta escolha pois vamos tratar com classes e objetos e não com linhas/registros de banco de dados , nem com índices, chaves , etc.

Clique no botão Finish;

Agora podemos usar a ToolBox e definir duas entidades : Cliente e Compra , arrastando o objeto Entity para o descritor e definindo as propriedades escalares conforme a figura abaixo:

Nota: Para incluir uma nova propriedade escalar clique com o botão direito do mouse sobre a entidade e selecione Add -> Scalar Property;

Vamos definir as entidade Cliente e Compra com as seguintes propriedades:

Cliente Compra
Id Id
nome ( Type = String ) DataCompra (Type = DateTime)
Quantidade (Type = Decimal )

Vamos definir uma associação entre as entidades Cliente e Compra. Para isso clique com o botão direito sobre a entidade Cliente e a seguir em Add New -> Association;

Na janela Add Association defina a seguinte associação 1 para muitos entre as entidades:

Ao final desse processo será criado a propriedade ClienteId na entidade Compra e as propriedades de navegação Compra na entidade Cliente e Cliente na entidade Compra.

Para criar o banco de dados a partir das entidades clique com o botão direito do mouse sobre o descritor e no menu suspenso clique em Generate DataBase from Model;

Na janela próxima janela de diálogo você pode selecionar um banco de dados existente para criar as tabela ou pode clicar em Add New Connection e criar um banco de dados novo.

Ao final será gerado o arquivo Concorrencia.edmx.sql com o script para gerar as tabelas no banco de dados selecionado;

Basta clicar no ícone para executar o script e assim gerar as tabelas no banco de dados:

No nosso exemplo eu criei o banco de dados Macoratti.mdf no SQL Server LocalDB. Na janela DataBase Explorer podemos conferir o resultado final:

Definindo o código do formulário AtualizaRegistro

Abra o formulário e no início vamos declarar 3 variáveis globais :

String NomeAnterior = "";

DateTime DataCompraAnterior = new DateTime();

Decimal QuantidadeAnterior = new Decimal();

Nessas variáveis vamos armazenar os valores obtidos a partir do banco de dados usando o Entity data Model representado pelo contexto criado:

No evento Load do formulário digite o código a seguir:

 private void AtualizaRegistro_Load(object sender, EventArgs e)
 {
            ConcorrenciaContainer context = new ConcorrenciaContainer();
            // Obtém os registros de compras
            var _DataCompra = from DC in context.Compras select DC;

            // Adiciona os dados do primeiro registro ao formulário
            txtNomeCliente.Text = _DataCompra.First().Cliente.nome;
            txtDataCompra.Text = _DataCompra.First().DataCompra.ToShortDateString();
            txtQuantidade.Text = _DataCompra.First().Quantidade.ToString();

            // Salva os valores anteriores em variáveis de memória
            NomeAnterior = _DataCompra.First().Cliente.nome;
            DataCompraAnterior = _DataCompra.First().DataCompra;
            QuantidadeAnterior = _DataCompra.First().Quantidade;
 }

Este código cria uma instância do contexto das nossas entidades e realizar uma consulta obtendo o primeiro registro da tabela Compras armazenando os valores nas caixas de texto do formulário e nas variáveis globais definidas no formulário.

No evento Click do botão Atualiza inclua o código a seguir que chama a rotina ExibirDados():

 private void btnAtualiza_Click(object sender, EventArgs e)
 {
      ExibirDados();
 }

Abaixo temos o código da rotina ExibirDados() que obtém o primeiro registro da tabela Compras e exibe em um caixa de mensagem:

 private void ExibirDados()
 {
            // Cria o contexto the context.
            ConcorrenciaContainer context = new ConcorrenciaContainer();
            // Obtem os registros de compras
            var _DataCompra = from DC in context.Compras select DC;

            // Salva os novos valores
            NomeAnterior = _DataCompra.First().Cliente.nome;
            DataCompraAnterior = _DataCompra.First().DataCompra;
            QuantidadeAnterior = _DataCompra.First().Quantidade;

            // Exibe o conteúdo do primeiro registro
            StringBuilder Saida = new StringBuilder();          

            Saida.Append(_DataCompra.First().Cliente.nome +
                         "\r\n" + _DataCompra.First().DataCompra.ToShortDateString() +
                         "\r\n" + _DataCompra.First().Quantidade);

           

            MessageBox.Show(Saida.ToString());
  }

O código do botão Cancela é visto a seguir:

 private void btnCancela_Click(object sender, EventArgs e)
 {
       Close();
 }

Executando o projeto e clicando no botão - Testando a Concorrência - teremos a exibição da instância dos formulário para cada usuário:

Este é nosso ambiente de testes e esta pronto para ser usado.

Testando a concorrência padrão

A boa notícia é que você não precisa se preocupar em implementar o suporte à concorrência padrão pois ela já está disponível como parte do Entity Framework.  Apesar disso é bom você saber como ela funciona e vamos aproveitar o ambiente que criamos para mostrar isso.

Selecione o formulário AtualizaRegistro e inclua um novo botão de comando a partir da ToolBox com o texto - Testando a concorrência padrão - e name = btnConcorrenciaPadrao;

A seguir inclua o código a seguir no evento Click deste botão de comando:

 private void btnConcorrenciaPadrao_Click(object sender, EventArgs e)
 {
            // cria o contexto
            ConcorrenciaContainer context = new ConcorrenciaContainer();           

            // Realiza as atualizações
            if (NomeAnterior != txtNomeCliente.Text)
                context.Compras.First().Cliente.nome = txtNomeCliente.Text;

            if (DataCompraAnterior.ToShortDateString() != txtDataCompra.Text)
                context.Compras.First().DataCompra = Convert.ToDateTime(txtDataCompra.Text);

            if (QuantidadeAnterior.ToString() != txtQuantidade.Text)
                context.Compras.First().Quantidade = Convert.ToDecimal(txtQuantidade.Text);

            context.SaveChanges();
            // Exibe os dados
            ExibirDados();
 }

Observe que o código esta definido para atualizar um campo somente quando houver uma alteração no mesmo. Se você atualizar cada campo toda a vez mesmo quando não houver alteração o Entity Framework irá atualizar todo o registro. O último usuário a realizar alterações sobreporá as mudanças feitas todas as outras alterações feitas.

Vamos ver então como funciona :

Execute o projeto e clique no botão - Testando a Concorrência. Teremos as duas instâncias do formulário AtualizaRegistro conforme mostra a figura a seguir:

1- Selecione o formulário para o Usuario A e altere a quantidade de 1.99 para 2.99. Clique no botão - Testando a Concorrência Padrão:

Você verá uma caixa de diálogo contendo o valor atual do banco de dados. Note que o valor da  quantidade agora é 2.99 o que confere com o valor atualizado pelo Usuario A.

2- Clique no botão OK para fechar a caixa de mensagem;

3- Selecione o formulário para o Usuario B e altere o campo Data da Compra para 12/01/2014. Clique no botão Testando a Concorrência Padrão;

Você verá caixa de mensagem exibindo o valor atual do banco de dados:

O banco de dados agora reflete as alterações feitas pelo Usuario A e pelo Usuario B. As mudanças foram mescladas em um novo registro que não tem conflito embora nenhum dos dois formulários exibam as informações atualizadas neste momento.

Se fecharmos a caixa de mensagem e selecionarmos o formulário para o Usuario A alterando a data para 28/01/2014 após clicar no botão Testando a Concorrência Padrão veremos que o valor data alterada pelo Usuario A sobrescreveu a alteração feita pelo Usuario B:

 

Este é o comportamento padrão da concorrência oferecida pelo Entity Framework.

 

Na continuação irei abordar outros aspectos envolvidos na concorrência com Entity Framework como o tratamento da concorrência para alterações de dados.

Romanos 7:4 Assim também vós, meus irmãos, fostes mortos quanto à lei mediante o corpo de Cristo, para pertencerdes a outro, àquele que ressurgiu dentre os mortos a fim de que demos fruto para Deus.

Romanos 7:5 Pois, quando estávamos na carne, as paixões dos pecados, suscitadas pela lei, operavam em nossos membros para darem fruto para a morte.

Romanos 7:6 Mas agora fomos libertos da lei, havendo morrido para aquilo em que estávamos retidos, para servirmos em novidade de espírito, e não na velhice da letra.

Referências:


José Carlos Macoratti