Back-End

10 jun, 2014

Entity Framework – Tratando o problema da concorrência de dados – Parte 01

Publicidade

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 de usuários aumenta, os problemas de concorrência também aumentam. A seguir temos uma lista dos problemas de concorrências mais comuns:

  • O usuário A esta tentando ler um registro que esta em uso pelo usuário B para realizar uma atualização ou exclusão;
  • O usuário A e o usuário B estão ambos tentando alterar diferentes campos de um mesmo registro;
  • O usuário A esta tentando atualizar o registro que o usuário B deletou;
  • O usuário A e o usuário B estão ambos tentando deletar o mesmo registro ao mesmo tempo.

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:

  • Fornecer ao usuário com uma cópia somente-leitura do registro;
  • Criar um registro que é um composto das alterações que são feitas em áreas individuais de registro;
  • Aceitar as alterações de um usuário em favor das alterações feitas por outro usuário baseado na ordem de precedência estabelecida por vários grupos definidos;
  • Informar ao usuário que o registro foi deletado e que as alterações serão descartadas;
  • Permitir que o usuário recrie o registro original e faça alterações no mesmo;
  • Permitir que o usuário mescle diferenças entre registros duplicados para criar um novo registro composto;
  • Aceitar a primeira exclusão e garantir que a segunda exclusão não cause uma exceção ou outro problema no banco de dados.

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 (só 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:

  • Otimista: Ao usar a concorrência otimista, quando um usuário acessa um registro ele é colocado em um cache local e, então, o usuário baixa o registro alterado como uma tarefa à parte. Nesta modalidade o servidor de banco de dados usa poucos recursos para servir ao usuário, porque os recursos são liberados imediatamente depois da requisição. Múltiplos usuários podem verificar o registro ao mesmo tempo de forma que não existe o deadlock. De forma geral, a concorrência otimista está focada em um alto desempenho e flexibilidade. Essa modalidade é usada em um ambiente onde os usuários trabalham com um grande número de registros e existe pouca contenção para os registros. De certa forma esta é a única opção disponível para aplicações web e outros tipos de aplicações desconectadas.
  • Pessimista: Ao usar a concorrência pessimista, quando um usuário acessa um registro, o gerenciador do banco de dados bloqueia o registro e o usuário ganha acesso direto ao mesmo durante o tempo de realização da operação CRUD. Como o registro está bloqueado, existem poucas chances de conflito. Porém, esta opção requer uma conexão dedicada por todo o processo, o que significa que esta opção não funciona para um cenário desconectado. Em geral a concorrência pessimista está focada na confiabilidade e segurança, com tempo de modificação de registro de curta duração. Esta opção é usada em um ambiente onde existe uma grande contenção para um número pequeno de registros.

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, que 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:

ef_tpc11

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:

  • 3 Label
  • 3 TextBox -= txtNomeCliente , txtDataCompra e txtQuantidade
  • 2 Button – btnAtualiza e btnCancela

Disponha os controles conforme o leiaute da figura a seguir:

ef_tpc12 (1)

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:

ef_tpc13

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:

ef_tpc14

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:

ef_tpc15

 

Ao final desse processo será criada 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.

ef_tpc16

Na 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:

ef_tpc17

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

ef_tpc18

Definindo o código do formulário AtualizaRegistro

Abra o formulário e no início vamos declarar três 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:

ef_tpc19

Este é nosso ambiente de testes e está 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 está definido para atualizar um campo somente quando houver uma alteração no mesmo. Se você atualizar cada campo toda 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:

ef_tpc10

1. Selecione o formulário para o Usuário 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 Usuário A.

ef_tpc1b

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

3. Selecione o formulário para o Usuário 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:

ef_tpc1c

O banco de dados agora reflete as alterações feitas pelo Usuário A e pelo Usuário 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 Usuário 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 Usuário A sobrescreveu a alteração feita pelo Usuário B:

ef_tpc1d

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.