ADO.NET - Tratando a concorrência de dados (rowversion)


Como posso prevenir que usuários de meu sistema atualizem dados que outros usuários estão editando ?

Como tratar efetivamente a concorrência de dados em aplicações de acesso a dados ?

Nota: Se tiver dúvidas sobre concorrência leia o artigo: ADO.NET - A arquitetura de dados desconectada e a concorrência de dados.

Geralmente uma das primeiras 'soluções' que passam pela cabeça de um desenvolvedor é que ele deve de alguma forma bloquear a tabela ou as tabelas enquanto um usuário estiver atualizando/editando os dados.

Só tem um problema nesta solução:  quando você usa ADO .NET você esta implicitamente usando o paradigma dos dados desconectados e, e isso por si só já torna a solução proposta surrealista e em muitos casos impraticável.

Desta forma em um aplicação como você deve tratar a concorrência de forma que as alterações feitas por outro usuário não sejam sobrepostas ?

Não existe uma receita pronta e única para tratar este problema , existem muitas formas de contorná-lo, e, uma delas é tomar vantagem do tipo de dados embutido no SQL Server ,  estou me referindo ao TimeStamp ou tipo rowversion. ( Não confunda o tipo de dados nativo do SQL Server com a propriedade DataRowView.RowVersion da ADO.NET)

A propriedade DataRowView.RowVersion obtém a versão atual da descrição de um DataRow. Os valores possíveis são:
  • Default - A versão padrão de um DataRowState. Para um valor de Added, Modified ou Deleted o valor padrão é Current. Para um valor Detached a versão é Proposed;
  • Original - A linha contém valores originais;
  • Current - A linha contém valores atuais;
  • Proposed - A linha contém valores Proposed;

Você pode usar a coluna timestamp (rowversion) de uma linha para determinar se qualquer valor na linha foi alterado desde a última vez que ela foi lida.

  1. Se qualquer modificação foi feita na linha o valor de timestamp estará atualizado;
  2. Se nenhuma alteração foi feita na linha , o valor de timestamp é o mesmo que o anteriormente lido;

Você pode obter o valor do timestamp atual para um banco de dados usando @@DBTS.

TimeStamp é um tipo de dado que expõe de forma automática valores únicos e binários em um banco de dados. O timestamp é gerado como um mecanismo de controle de versão de uma linha. O tamanho armazenado é de 8 bytes.
   Cada banco de dados possui um contador que é incrementado para cada operação de  inclusão ou alteração que é realizada na tabela que contém a coluna timestamp.

@@DBTS - retorna o valor do tipo de dado timestamp atual para o banco de dados atual.

Você pode simplificar a cláusula WHERE de sua consulta de atualização dando suporte a colunas timestamp. A coluna timestamp do SQL Server não contém a informação de data e hora , ela contém a data binária que é única em um banco de dados, Incluindo o valor da coluna rowversion em sua consulta SELECT  você terá condições de tratar a concorrência de atualização de forma correta.

Você pode definir a coluna rowversion em sua tabela e a qualquer momento que o conteúdo da linha for alterado, o SQL Server irá modificar o valor da coluna rowversion para aquela linha. Você pode incluir uma coluna rowversion na tabela do seu bando de dados e alterar as consultas de atualização e inclusão de forma a sensibilizar a coluna rowversion.

Vejamos um exemplo:

Eu vou usar o Management Studio 2005 para executar os comandos SQL e criar as tabelas e as consultas no banco de dados Teste.

Nota:  Veja como usar o Management Studio no artigo: .NET  2005 - Usando o SQL Server Management Studio

1- Criando a tabela Produto com a columa do tipo rowversion

Abra o Management Studio e selecionando o banco de dados Teste digite e execute a consulta SQL abaixo:

Após a execução veja que a tabela Produto foi criada com a coluna chamada RowVrsn do tipo rowversion - timestamp e not null.

2- Criando a stored procedure de inclusão dp_Product_ins

Este procedimento armazenado inclui valores na tabela Product.

3- Criando a stored procedure de atualização db_Product_upd

Este procedimento armazenado atualiza a tabela Product e usa a rowversion.

Nota: Têm dúvidas sobre Stored Procedures ? Veja o artigo : VB.NET - Brincando com Stored Procedures no SQL Server

Após criar os procedimentos armazenados com suporte a rowversion você pode usar uma combinação de chave primária e coluna rowversion na cláusula WHERE de suas consultas de atualização e de inclusão para ter certeza de que as alterações feitas por outro usuário não serão sobrepostas.

Você pode examinar o valor de retorno de uma instrução ExecuteNonQuery para verificar se a linha foi atualizada e informar a ocorrência ao usuário. Veja um exemplo de como criar este código abaixo:

Public RowVersion(8) As Byte

Public Sub seuMetodo()

.....
Dim cn As SqlConnection = New SqlConnection(ConfigurationSettings.AppSettings("ConnectionString"))

Dim cmd As SqlCommand = New SqlCommand("usp_SuaStoredProcedure", cn)
cmd.CommandType = CommandType.StoredProcedure

cmd.Parameters.Add("@Codigo", Me.Codigo)
cmd.Parameters.Add("RETURN_VALUE", SqlDbType.Int).Direction = ParameterDirection.ReturnValue

With cmd.Parameters.Add("@RowVersion", SqlDbType.Timestamp)
    .Value = Me.RowVersion
    .Direction = ParameterDirection.InputOutput
End With


Try
     cn.Open()
     cmd.ExecuteNonQuery()

     If CInt(cmd.Parameters("RETURN_VALUE").Value) = -1 Then
         ' a linha foi modificada por outro usuário então lança uma exceção.!
          Throw New ModificadaPorOutroUsuarioException
     End If


     Me.RowVersion = CType(cmd.Parameters("@RowVersion").Value, Byte())

Catch ex As SqlException
     Throw New Exception("erro no método.", ex)
Finally
    cn.Close()
End Try

End Sub

Obs: No exemplo acima estou usando um tratamento de exceção personalizado. Você deve então criar uma classe chamada ModificadaPorOutroUsuarioException com o seguinte código:

Public Class ModificadaPorOutroUsuarioException
                       Inherits System.ApplicationException

Public Sub New()
     MyBase.New("A linha já foi modificada por outro usuário")
End Sub

Public Sub New(ByVal InnerException As Exception)
     MyBase.New("A linha já foi modificada por outro usuário!",InnerException)
End Sub

End Class
 

Creio que com estas dicas você têm todas as condições de planejar o tratamento da concorrência em suas aplicações usando uma técnica apurada e segura.

Eu sei é apenas VB .NET mas eu gosto...


José Carlos Macoratti