VB 2005 - Introdução a concorrência de dados


Para aplicações que usam a plataforma .NET, o acesso aos dados é feito usando-se a ADO.NET. Se considerarmos que mais da metade das aplicações usam o acesso a algum banco de dados e que as informações que estas aplicações gerenciam precisam ser tratadas de forma segura, concluímos que devemos considerar cenários críticos que podem afetar a integridade dos dados em nossas aplicações.

Quando múltiplos usuários tentam efetuar uma alteração nos dados de sua aplicação ao mesmo tempo ocorre o que é conhecido por concorrência de dados.

Se você considerar que a  ADO.NET elegeu como modelo principal de acesso a dados a forma desconectada (DataSets) percebe a importância de compreender bem o problema para criar mecanismos para tratá-lo de uma forma consistente.  Quando ocorre o conflito de dados durante a concorrência é preciso estabelecer um controle com o objetivo de prevenir a perda de informações durante o acesso simultâneo de diversos usuários.

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 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 arquitetura de componentes de dados desconectados requer um controle de concorrência otimista em ambientes com múltiplos usuários. Este modelo trabalhar da seguinte forma:

Nota: Você pode diminuir a probabilidade de violação da concorrência atualizando o conjunto de dados imediatamente antes de atualizar os registros mas deve levar em consideração que este mecanismo aumenta a carga de rede e no banco de dados.

Os objetos DataAdapaters (1.1) e TableAdapter (2.0) fornecem a propriedade ContinueUpdateError que você pode definir para True para impedir que a exceção DBConcurrentcyException seja disparada quando ocorrer uma violação de concorrência durante a atualização de dados via comando Update.

Ao Eliminar estas exceções é possível efetuar múltiplas atualizações - algumas dais quais possuem a violação de concorrência - para continuar sem a intervenção do usuário. Quando uma violação ocorre , o DataAdapter define o valor da propriedade DataRow.RowError da sua DataTable fonte para algo parecido com "Concurrency violation: the UpdateCommand affected 0 of 1 records".

A estratégia em usar esta propriedade pode ser útil em um cenário onde temos somente um DataTable pois atualizar tabelas relacionadas requer a criação de 3 DataTables temporários para cada DataSet.DataTable. Definir o valor da propriedade ContinueUpdateError para True em um cenário com mais de uma tabela relacionada não é uma boa prática de programação visto que resolver o conflito de violação de concorrência para tabelas relacionadas não é uma tarefa das mais fáceis.

Então qual estratégia para controlar a concorrência de dados devemos adotar ?

A resposta é : Depende.

Antes de você começar a escrever o código para controlar a concorrência de dados você deverá definir em conjunto com seu cliente a estratégia de tratamento para as situações de concorrência de dados. Para ajudá-lo nesta tarefa temos a seguir as  principais questões envolvidas nesta especificação:

- Será permitido a todos os usuários decidir se sobrescrevem as alterações feitas por outros usuários ?
- A decisão de sobrescrever alterações feitas por outros usuários estarão restritas a determinados perfis de usuários ? (Neste caso todas as tabelas deverão possuir uma coluna identificar o usuário que incluiu ou modificou a linha por último.)
- Que informação deverá ser apresentado ao usuário para que ele possa tomar a decisão de sobrescrever as alterações ? (Em muitos casos os usuários necessitam visualizar as alterações feitas nas linhas pelos demais usuários, e, isto requer um carga no servidor.)
- Serão permitidas atualizações em mais de uma entidade de dados sem salvar os dados para as entidades individuais no servidor ?
- Deverá uma única violação de concorrência impedir ou desfazer todas as mudanças associadas com a atualização ? (Desfazer todas as alterações requer uma transação do lado do cliente que não é fácil de implementar com os componentes de dados.)
- Será permitido aos usuários recuperar ou gerar novamente  linhas deletadas por outros usuários ?

Após uma definição do que deve ser feito você pode decidir o que implementar mas de forma geral você ficará com as seguintes opções:

- First-win (o primeiro ganha) - A última alteração é removida. (Equivale a definir ContinueUpdateError como True)
- Last-win (o último ganha) - A última alteração é sempre aplicada.
- Ask-the-user (pergunte ao usuário) - O usuário escolhe o que fazer. (Equivale a definir ContinueUpdateError como False e implementar a lógica para exibir e conciliar as linhas em conflito)

Só falta eu falar sobre como você pode verificar e testar as violações de concorrência. Vamos lá...

Nota: Lembrando que eu estou considerando a concorrência otimista.

Existem diversas formas de testar a violação de concorrência e eu não pretendo esgotar o assunto, vamos abordar as técnicas mais usadas:

1- Usar DataSets - Os DataSets mantêm valores originais das linhas para que o Adapter possa efetuar a verificação de concorrência otimista durante a atualização do banco de dados. (Veremos isso mais adiante)

2- Copiar os dados originais - Ao manter uma cópia dos valores dos dados originais durante a recuperação de dados, você pode efetuar uma verificação dos valores atuais do banco de dados conferindo se eles equivalem com os valores originais armazenados. Se não forem equivalentes temos uma violação de concorrência.

3- Usar timeStamps - Você inclui uma coluna de timestamp em cada tabela que é sempre retornada em cada consulta feita no banco de dados. Ao ocorrer uma tentativa de atualização o valor da coluna é comparada com o valor original da linha modificada, se o valores forem iguais a atualização é executada e a coluna timestamp atualizada com data e hora atuais, caso contrário ocorreu uma violação de concorrência.

Para encerrar veremos a seguir a utilização dos datasets na concorrência otimista.

DataSet - Fornece um representação relacional em memória de dados , sendo um conjunto completo de dados que incluem tabelas que contém dados,  restrições de dados e relacionamentos entre as tabelas. O acesso é desconectado.

- Quando você usa um DataSet frequentemente você também usa um DataAdapter  para interagir com sua fonte de dados.
- Quando você usa um DataSet você pode empregar um DataView para aplicar ordenamento e filtragem nos dados do DataSet.
- O DataSet também pode ser herdado para criar um DataSet fortemente tipado com o objetivo de expor tabelas, linhas e colunas como propriedades de objetos fortemente tipados.

Você já sabe que os DataSets adotam o paradigma desconectado de dados, e , se você ainda não parou para pensar, este modelo trás a tona um problema que é o tratamento da violação da concorrência de dados. Imagine o seguinte cenário:

- O João preenche um DataSet com os registros de um banco de dados e efetua alterações no modo desconectado;
- Ao mesmo tempo a Maria preenche outro DataSet com os mesmo dados e esta atualizando os mesmos registros em outra aplicação;
- Como você pode determinar quais registros foram alterados quando o João tentar aplicar as alterações feitas ao banco de dados ?

Vejamos como a ADO.NET faz o tratamento da verificação para os casos de conflito de dados e como ela trata a violação de concorrência.

Você pode usar o assistente de configuração para o DataAdatper para gerar todos os objetos Command em um DataAdatper e um TableAdapter. isto facilita muito a vida do programador embora se você não se sentir confortável com isto pode criar todo o código para fazer o mesmo serviço.

Nota: Acompanhe o artigo : VB.NET  2005 - Editando dados em um DataSet - Veja como incluir, atualizar e excluir dados em um dataset tipado e em um dataset não tipado.

Se você ainda não notou perceba que por padrão o assistente de configuração para DataAdapter e TableAdapter gera os comandos SQL que fazem a verificação para a concorrência otimista. Neste tipo de concorrência  estamos supondo que não acontecerá a edição simultânea de registros e desta forma o bloqueio do registro é feito apenas quando da atualização do registro. Para ver este comportamento em ação faça o seguinte:

Para verificar a violação de concorrência, o assistente cria instruções SQL que verificam se o registro que esta prestes a ser atualizado ou deletado não foi alterado desde que ele foi carregado no DataSet. O assistente faz isto incluindo uma cláusula WHERE na instrução SQL para verificar se o que esta sendo atualizado ou excluído coincide com o que foi carregado e colocado no DataSet.

Os objetos DataTable podem armazenar múltiplas versões para cada linha e se você der uma espiada em um DataSet na memória você verá mais de um objeto DataTable sendo que cada um deles e constituído de objetos DataRow;  sendo que cada registro na tabela pode existir mais de uma vez, possuindo assim diferentes DataRowVersion.

Existem quatros valores possíveis para DataRowVersion mas a que atua na concorrência otimista são os valores Current e Original.

Quando o adaptador preenche o DataSet com registros , a cada DataRow é atribuído um DataRowVersion de Original. Se você editar o registro , as alterações serão armazenadas em uma nova versão do registro e teremos outro DataRowVersion com o valor Current.

Neste momento você possui a versão Original, que foi carregada do banco de dados, e a versão Current, representando as alterações que você realizou.

Vejamos agora como o processo funciona:

Como isso é feito ? 

Vamos examinar o código gerado pelo assistente para o comando UPDATE que contém a cláusula WHERE ;

Para Visualizar o código gerado selecione o TableAdapter e na janela Properties  selecione UpdateCommand ;

Examinando o código gerado notamos que a cláusula WHERE examina o valor existente para cada campo no banco de dados verificando se eles coincidem com os valores que foram originalmente carregados, ou seja , a consulta toma os valores a partir da DataRowVersion com status Original e verifica se este valores são os que ainda existem no banco de dados. Se eles existirem , os valores para o registro da DataRowVersion com status Current são usados para atualizar a tabela.

Neste exemplo vamos supor que após alterar o nome de Macoratti para Pedro para o primeiro registro e antes de chamar o método Update algum outro usuário também alterou o nome para João.

Quando você for chamar o método Update a consulta irá tentar alterar o campo Nome para Pedro onde o Nome era Macoratti e não haverá mais o registro com o valor Macoratti então a ADO.NET reconhece que o registro foi alterado ou deletado e a atualização vai falhar e irá lançar uma exceção não tratada.

Naturalmente você deverá exibir uma mensagem mais útil informando os valores antigos e os novos valores para o campo que esta sendo atualizado.

Até o próximo artigo onde pretendo mostrar um exemplo prático de tratamento de concorrência de dados.

Até breve ...


José Carlos Macoratti