ADO .NET - Verificando a violação de concorrência


Neste artigo eu mostrarei uma forma de verificar e controlar a violação da concorrência quando usamos a concorrência otimista com ADO .NET.

O controle a concorrência de dados previne que um usuário sobrescreva as modificações que outros usuários realizaram no mesmo registro da tabela. Conforme aumenta a quantidade de usuários concorrentes a uma aplicação ADO.NET 2.0 cresce a importância do controle da concorrência com o objetivo de permitir a disponibilidade e a integridade dos dados.

De forma geral existem 2 formas comuns de gerenciar a concorrência no acesso a dados:

1- A concorrência Pessimista - Todas as linhas (registros) que estão sendo modificadas são bloqueadas pelo usuário e ninguém mais pode realizar alterações até que o bloqueio (lock) seja liberado. O bloqueio evita que outros usuários efetuem a leitura ou modificação nos registros até que o primeiro usuário confirme as alterações no banco de dados (commit). Um exemplo muito comum é bloquear todos os registros filhos quando o usuário atualiza o registro pai, neste caso, pode-se estar impedindo o acesso a um grande número de linhas.

Este tipo de concorrência é usada em ambientes com uma grande contenção de dados onde a utilização do bloqueio dos registros é menos oneroso do que usar roll back nas transações e,  além disto,  requer uma conexão contínua com o banco de dados. (Se o bloqueio das linhas for efetuado por um tempo muito longo  haverá um grande impacto negativo no desempenho da  aplicação.)

Este modelo é útil para situações onde a modificação da linha durante a transação pode ser prejudicial como no caso de uma aplicação de controle de estoques onde você quer bloquear um produto até que o pedido seja gerado afim de mudar o estado do item como liberado para em seguida removê-lo do estoque. Se o pedido não for confirmado o bloqueio será liberado de forma que outros usuários poderão consultar o estoque e assim obter um valor real do disponível.

Este modelo não pode ser usado na arquitetura desconectada de dados pois neste modelo as conexões são abertas somente o tempo suficiente para ler ou atualizar os dados, assim os bloqueios não poderão se mantidos por longos períodos.

2- A concorrência Otimista - O bloqueio das linhas não é usado para a leitura dos dados, mas , somente enquanto eles estão sendo atualizados (um tempo médio entre 5 a 50 milisegundos). A aplicação verifica as atualizações feitas por outro usuário desde a sua última leitura. Se outro usuário atualizou a linha depois que o usuário atual leu a linha ocorre uma violação da concorrência e a última atualização não será confirmada.

Este tipo de concorrência é indicada para ambientes com baixa contenção de dados e é usada de forma padrão pela plataforma .NET que privilegia o acesso desconectado aos dados através do uso de DataSets .( Neste modelo de acesso aos dados  o bloqueio de linhas por um período longo é impraticável.)

A abordagem adotada neste artigo vai usar uma coluna do tipo timestamp para gerenciar a violação de concorrência.

Vamos criar uma tabela chamada ViolacaoConcorrencia no banco de dados Northwind.mdf (você pode usar qualquer outro banco de dados ou criar o seu próprio banco de dados no SQL Server)

Recursos usados neste artigo :

Criando o projeto

Vamos criar um novo projeto no Visual C# 2010 Express Edition acionando o menu File -> New Project e a seguir escolhendo o template Console Application;

Informe o nome ViolacaoConcorrenciaOtimista e clique no botão OK;

A seguir abra a janela Database Explorer e clique no ícone Data Connections com o botão direito do mouse e selecione Add Connections;

Na janela Add Connection escolha o Data Source - Microsoft SQL Server Database File (SqlClient) - e informe o local do banco de dados Northwind.mdf;

A seguir clique com o botão direito sobre o item Tables e selecione Add New Table;

A seguir defina a seguinte estrutura para a tabela ViolacaoConcorrencia:

Vamos declarar os seguintes namespaces no inicio do arquivo Programa.cs:

using System;
using System.Windows.Forms;
using System.Data.SqlClient;

A seguir vamos declarar a string de conexão e as variáveis para DataTable e SqlDataAdapter logo após a declaração da classe Program:

private static string sqlConnectString = @"Data Source=.\SQLEXPRESS;AttachDbFilename=C:\dados\Northwind.MDF;Integrated Security=True;Connect Timeout=30;User Instance=True;";

private static DataTable dtA;
private static DataTable dtB;
private static SqlDataAdapter daA;
private static SqlDataAdapter daB;

No método Main() inclua o código abaixo:

 static void Main(string[] args)
        {
         
  // Constroi uma instrução SELECT e UPDATE; As instruções DELETE e INSERT
            // não necessitam dessa solução

          
 string textoSelect = "SELECT Id, Campo1, Versao FROM ViolacaoConcorrencia";
            string textoUpdate = "UPDATE ViolacaoConcorrencia SET Campo1 = @Campo1 " +
                                            "WHERE Id = @Id AND Versao = @Versao";


         
  // Cria a tabela A e preenche com o schema
            dtA = new DataTable("TabelaA");
            daA = new SqlDataAdapter(
textoSelect, sqlConnectString);
          
 daA.RowUpdated += new SqlRowUpdatedEventHandler(da_RowUpdated);
            daA.FillSchema(dtA, SchemaType.Source);
            dtA.Columns["Versao"].ReadOnly = false;
            daA.Fill(dtA);

        
   // Cria o comando de atualização e define os parâmetros
            daA.UpdateCommand = new SqlCommand(textoUpdate,daA.SelectCommand.Connection);
            daA.UpdateCommand.CommandType = CommandType.Text;
            daA.UpdateCommand.Parameters.Add("@Id", SqlDbType.Int, 0, "Id");
            daA.UpdateCommand.Parameters["@Id"].SourceVersion = DataRowVersion.Original;
            daA.UpdateCommand.Parameters.Add("@Campo1", SqlDbType.NVarChar, 50,"Campo1");
            daA.UpdateCommand.Parameters["@Campo1"].SourceVersion = DataRowVersion.Current;
            daA.UpdateCommand.Parameters.Add("@Versao", SqlDbType.Timestamp, 0,"Versao");
            daA.UpdateCommand.Parameters["@Versao"].SourceVersion = DataRowVersion.Original;

    
       // Cria a tabela B e preenche com o schema
            dtB = new DataTable("TabelaB");
            daB = new SqlDataAdapter(
textoSelect, sqlConnectString);
            daB.RowUpdated += new SqlRowUpdatedEventHandler(da_RowUpdated);
            daB.FillSchema(dtB, SchemaType.Source);
            dtB.Columns["Versao"].ReadOnly = false;
            daB.Fill(dtB);

           
// Cria o comando de atualização e define os parâmetros
            daB.UpdateCommand = new SqlCommand(textoUpdate, daB.SelectCommand.Connection);
            daB.UpdateCommand.CommandType = CommandType.Text;
            daB.UpdateCommand.Parameters.Add("@Id", SqlDbType.Int, 0, "Id");
            daB.UpdateCommand.Parameters["@Id"].SourceVersion = DataRowVersion.Original;
            daB.UpdateCommand.Parameters.Add("@Campo1", SqlDbType.NVarChar, 50,"Campo1");
            daB.UpdateCommand.Parameters["@Campo1"].SourceVersion =    DataRowVersion.Current;
            daB.UpdateCommand.Parameters.Add("@Versao", SqlDbType.Timestamp, 0, "Versao");
            daB.UpdateCommand.Parameters["@Versao"].SourceVersion = DataRowVersion.Original;

        
   // Saida do resultado das tabelas
            Console.WriteLine("---INICIAL---");
            SaidaTabela(dtA);
            SaidaTabela(dtB);

          
 // Atualiza Tabela A
            dtA.Rows.Find(2)["Campo1"] += " (new.A)";
            AtualizaTabela(daA, dtA);

            /
/ Saida da tabela
            Console.WriteLine("\n---DEPOIS DE ATUALIZAR TABELA A---");
            SaidaTabela(dtA);
            SaidaTabela(dtB);

          
 // Atualiza Tabela B
            dtB.Rows.Find(2)["Campo1"] += " (new.B)";
            AtualizaTabela(daB, dtB);

          
 // Saida da tabela

            Console.WriteLine("\n---DEPOIS DE ATUALIZAR TABELA B---");
            SaidaTabela(dtA);
            SaidaTabela(dtB);

            Console.WriteLine("\nPressione qualquer tecla para continuar.");
            Console.ReadKey();
        }

Acima temos o código que testa a violação da concorrência realizando atualizações em duas tabelas A e B que foram criadas usando o objeto DataTable.

O código acima usa duas rotinas:

O código do método SaidaTabela() é dado a seguir:

 static void SaidaTabela(DataTable dt)
        {
            Console.WriteLine("\n---{0}---", dt.TableName);
            foreach (DataRow row in dt.Rows)
            {
                Console.WriteLine("{0}\t{1}\t{2}",row["Id"], row["Campo1"], Convert.ToBase64String(row.Field<byte[]>("Versao")));
            }
        }

O código do método AtualizaTabela() e visto abaixo:

  static void AtualizaTabela(SqlDataAdapter da, DataTable dt)
        {
            Console.WriteLine("\n=> Atualiza({0})", dt.TableName);
            try
            {
                da.Update(dt);
                Console.WriteLine("=> Atualizado com sucesso.");            
            }
            catch (DBConcurrencyException ex)
            {
                // se o timestamp não confere ocorre um erro
                Console.WriteLine("=> EXCEÇÃO : {0}", ex.Message);
                dt.RejectChanges();
            }
        }

O método RowUpdate() do DataAdapter possui o seguinte código:

 static void da_RowUpdated(object sender, SqlRowUpdatedEventArgs e)
        {
            Console.WriteLine("=> DataAdapter.RowUpdated event. Status = {0}.", e.Status);

            // verifica se uma operação insert ou update esta sendo realizada
            if (e.Status == UpdateStatus.Continue && (e.StatementType == StatementType.Insert || e.StatementType == StatementType.Update))
            {
                // Cria um objeto command para retornar o timestamp atualizado
                String sqlGetRowVersion = "SELECT Versao FROM ViolacaoConcorrencia WHERE Id = " + e.Row["Id"];
                SqlConnection conn = new SqlConnection(sqlConnectString);
                SqlCommand cmd = new SqlCommand(sqlGetRowVersion, conn);

                // Define o timestamp para o novo valor no data source e
                // chama o método AcceptChanges

                conn.Open();
                e.Row["Versao"] = (Byte[])cmd.ExecuteScalar();
                conn.Close();
                e.Row.AcceptChanges();
            }
        }

Como funciona ???

A solução cria um DataTable chamado TabelaA e a preenche com o esquema e os dados da tabela no banco de dados ViolacaoConcorrencia no banco de dados Northwind.mdf.

A coluna timestamp é só de leitura. Um manipulador de eventos chamado da_RowUpdated é criado para o evento RowUpdated do DataAdapter. O manipulador de eventos da_RowUpdated verifica se um erro não ocorre (Status = Continue) e se uma linha foi ou inserida ou atualizada.

Para essas linhas, o valor atual da coluna timestamp é recuperado da tabela ViolacaoConcorrencia no banco de dados e utilizado para atualizar a linha no DataTable TabelaA.

O comando update é criado para o DataAdapter- a cláusula WHERE do comando de atualização verifica a correspondência tanto do campo Id (chave primária utilizada para localizar o registro) e do campo Versão (coluna timestamp usado para determinar se os dados foram alterados desde a última leitura )

Da mesma forma a solução cria um DataTable chamado TabelaB e preenche e o configura exatamente da mesma maneira como a TabelaA no parágrafo anterior. Juntos os objetos DataTable TabelaA e TabelaB irão simular dois usuários simultaneamente interagindo com os mesmos dados.

O conteúdo dos objetos DataTable TabelaA e TabelaB são exibidos na saída do console para mostrar o estado inicial.

O registro com o id = 2 no DataTable TabelaA é modificado, atualizado para a tabela ViolacaoConcorrencia e a saída para o console mostra o efeito da mudança. O manipulador de eventos da_RowUpdated recupera o valor atualizado para o campo timestamp e atualiza o valor na linha do DataTable atualizado.

O registro com o id = 2 no DataTable TabelaB é modificado. É feita uma tentativa para atualizar a modificação do na tabela ViolacaoConcorrencia. O comando update do DataAdapter não pode encontrar um registro correspondente tanto para o Id e o campo timestamp por causa da atualização anterior feita no DataTable TabelaA.

O manipulador RowUpdated não processa a lógica de atualização timestamp para a linha porque o Status tem o valor ErrorsOccurred em vez de Continue.

Pode fazer sentido em algumas situações atualizar o conteúdo de DataTable TabelaB neste momento. O bloco catch para o método Update() do DataAdapter gera o erro para o console. O conteúdo dos objetos DataTable TabelaA e TabelaB são exibidos na saída do console.

O resultado obtido pode ser visto na figura abaixo:

Pegue o projeto completo aqui ViolacaoConcorrenciaOtimista.zip

Rom 8:1 Portanto, agora nenhuma condenação há para os que estão em Cristo Jesus.

Rom 8:2 Porque a lei do Espírito da vida, em Cristo Jesus, te livrou da lei do pecado e da morte.

Rom 8:3 Porquanto o que era impossível à lei, visto que se achava fraca pela carne, Deus enviando o seu próprio Filho em semelhança da carne do pecado, e por causa do pecado, na carne condenou o pecado.

Rom 8:4 para que a justa exigência da lei se cumprisse em nós, que não andamos segundo a carne, mas segundo o Espírito.

Referências:


José Carlos Macoratti