VB 6 - Aplicações em ambiente de rede com acesso concorrente


O tema é recorrente, muita gente tem dúvida como distribuir o seu programa VB com acesso ao banco de dados Microsoft Access para rodar em um ambiente multiusuário com acesso concorrente. Neste cenário você tem diversos computadores acessando a sua aplicação em uma rede interna. Um cenário conhecido como cliente-servidor.

A seguir exponho algumas orientações para que a instalação da sua aplicação nesse cenário seja o menos traumática possível tratando assuntos como instalação, bloqueios e o banco de dados. Eu não esgotei o assunto apenas estou trazendo a tona algumas considerações relevantes.

A instalação

Após terminar seu programa, e, gerar o pacote de instalação, instale seu aplicativo em cada computador cliente(estação) para que haja uma cópia do Microsoft Jet e das demais dlls necessárias em cada estação. O banco de dados Access deve ser instalado em uma máquina definida como servidor de arquivos da aplicação e para isso você terá que mapear a unidade de rede desta máquina para que todas as estações possam acessá-la.

O banco de dados

O banco de dados deverá ficar em uma máquina definida como o servidor de arquivos da aplicação pois todas as aplicações das máquinas clientes deverão acessar o mesmo banco de dados que esta no servidor. Neste caso a string de conexão definida na aplicação não pode ser fixa mas configurável de modo a que as aplicações enxerguem o mesmo local na rede para o banco de dados. (O Microsoft Access é um servidor de arquivos e os dados vão trafegar pela rede impactando a sua aplicação por isso use um SQL inteligente e faça consultas que retornem poucos dados.)

Os Bloqueios

Para entender os bloqueios, você deve familiarizar-se com os níveis em que você pode bloquear dados. Com o Microsoft Jet, você pode bloquear dados em três diferentes níveis. Estes níveis variam do mais restritivo ao menos restritivo.

· Modo Exclusivo impede que todos os demais usuários acessem o banco de dados. É o modo mais restritivo.
· Bloqueio de Recordset bloqueia as tabelas subjacentes a um objeto recordset de modo a impedir leituras, ou gravações ou ambas as operações.
· Bloqueio de Página bloqueia a página de 2048 byte (2K) contendo os dados que estão sendo editados. Este é o modo menos restritivo.

Os três modos não são mutuamente exclusivos.

No bloqueio de página somente a página que contém o registro que está sendo editado atualmente é bloqueada. O Microsoft Jet acessa e bloqueia dados em páginas de 2048 bytes. Quando você usa bloqueio de página, que é o nível de bloqueio padrão para objetos Recordset, outros usuários podem ler dados da página bloqueada, mas não podem alterá-la.

Para controlar o bloqueio de página , você especifica o modo de bloqueio usando a propriedade de LockEdits. Nas partes de sua aplicação que usam bloqueios ao nível de página, você tem que determinar que modo de bloqueio usar. O modo de bloqueio, bloqueio pessimista ou bloqueio otimista, determina como o Microsoft Jet bloqueia os dados.

Bloqueio pessimista

Com o bloqueio pessimista, o mecanismo de acesso bloqueia a página que contém o registro editado atualmente assim que o método Edit é chamado, e não libera o bloqueio até que as mudanças no registro sejam confirmadas ou canceladas explicitamente com chamadas ao método Update ou CancelUpdate. O bloqueio pessimista é o padrão para objetos Recordset.

Nota Transações mudam o comportamento que determina quando um bloqueio é liberado.

A vantagem primária do bloqueio pessimista é que depois que você obtém um bloqueio, você sabe que não terá nenhum conflito de bloqueio enquanto o registro estiver bloqueado. Adicionalmente, o bloqueio pessimista é o único modo de garantir que sua aplicação lerá os dados mais atuais, porque um usuário não pode mudar um registro depois que outro usuário começa a editá-lo.

A desvantagem do bloqueio pessimista é que a página inteira que contém o registro é bloqueada ao longo da duração do procedimento que edita e bloqueia o registro. Se você está dando ao usuário acesso aos dados por uma interface de usuário, outro problema surge quando o usuário começa a editar um registro, bloqueia-o, e afasta-se do computador por um período de tempo. Isto não só faz com que o registro que o usuário está editando fique bloqueado, mas possivelmente outros registros que residem dentro da página bloqueada.

Nota: Você pode fixar o tipo de bloqueio para um Recordset no argumento lockedits do método OpenRecordset. Você também pode usar a propriedade LockEdits para atribuir o tipo de bloqueio depois que você tenha aberto um Recordset. Se você atribuir a propriedade LockEdits para True, o bloqueio pessimista é habilitado. Se você atribuir LockEdits para False, o bloqueio otimista é habilitado.

Bloqueio otimista

Com o bloqueio otimista, a mecanismo de acesso bloqueia a página só quando você tenta confirmar as mudanças no registro com o método Update. Um vez que o bloqueio só acontece quando sua aplicação tenta confirmar as mudanças, você minimiza o tempo que o bloqueio permanece ativo; esta é a vantagem principal do bloqueio otimista.

A desvantagem do bloqueio otimista é que quando um usuário começa a editar um registro, você não pode estar seguro que a atualização terá sucesso. Uma atualização que depende de bloqueio otimista falha se outro usuário muda um registro que o primeiro usuário está editando.

Imagine um cenário onde os usuários Macoratti e Miriam estão editando o mesmo registro. Macoratti começa editando o registro de cliente de Clientes com bloqueio otimista. Porque o bloqueio otimista de Macoratti não é bloqueio de fato, nada impede que Miriam tente editar o mesmo registro. Miriam começa a editar o mesmo registro de Clientes.

 Infelizmente, Miriam não tem nenhuma idéia de que Macoratti já está editando o registro, e também não tem a visão mais atualizada dos dados. Quando Macoratti tenta salvar suas mudanças, ele recebe uma mensagem de erro porque Miriam também está editando o registro.

Usando bloqueio otimista em seu código

1 abra um Recordset tipo table ou dynaset nos dados que você quer editar.

2 mova para um registro.

3 habilite o bloqueio otimista atribuindo False à propriedade LockEdits do Recordset.

4 use o método Edit para permitir edição do registro (o registro ainda não está bloqueado).

5 faça mudanças no registro.

6 use o método Update para finalizar as mudanças no registro (isto tenta bloquear o registro).

7 cheque para ver se o método Update teve sucesso. Se não teve sucesso, tente novamente.

Bloqueios otimistas mudam para bloqueios pessimista quando você usa transações. Porque uma transação mantém um bloqueio de gravação até que você finalize-a, o bloqueio pessimista acontece até mesmo se a propriedade de LockEdits para False.

Também, é possível para o método Update falhar com bloqueio otimista. Só porque o método Edit não criou um bloqueio de gravação, não significa que o método Update não tenha criado um bloqueio de gravação. Em outras palavras, um usuário poderia ter aberto um Recordset com bloqueio pessimista, e a atualização de um segundo usuário para os mesmos dados falha mesmo que o segundo usuário esteja usando bloqueio otimista.

O código seguinte implementa o bloqueio pessimista. Atualiza o número de unidades em estoque para um determinado produto na tabela Produtos.

Function UpdateUnitsInStock(strProduct As String, intUnitsInStock As Integer, intMaxTries As Integer)
Dim dbs As Database
Dim rstProducts As Recordset
Dim blnError As Boolean
Dim intCount As Integer
Dim intLockCount As Integer
Dim intChoice As Integer
Dim intRndCount As Integer
Dim i As Integer
 
On Error GoTo ErrorHandler
 
' Abre o banco de dados especificado na constante
' STR_DBPATH em modo compartilhado.
Set dbs = OpenDatabase(STR_DBPATH)
 
' Abre a tabela para edição.
Set rstProducts = dbs.OpenRecordset("Products",  dbOpenDynaset)

With rstProducts
  ' Atribui o tipo de bloqueio como pessimista.
  ' Atribuir False a LockEdits causaria
  ' o uso de bloqueio otimista.
   .LockEdits = True
   .FindFirst "ProductName = " & Chr(34) &   strProduct & Chr(34)
   If .NoMatch Then
      UpdateUnitsInStock = ERR_NOMATCH
      GoTo CleanExit
   End If
 
   ' Tenta editar o registro. Se um erro de
   ' bloqueio acontece, o manipulador de erros
   ' tenta solucioná-lo. Porque este procedimento
   ' usa bloqueio pessimista, ocorrem erros se
   ' você tentar editar um registro.
   .Edit
    ![UnitsInStock] = intUnitsInStock
    .Update
End With
 
CleanExit:
 
rstProducts.Close
dbs.Close
Exit Function
 
ErrorHandler:
 
Select Case Err
 
Case 3197
  ' Dados no recordset foram alterados após
  ' a sua abertura. Tente editar um registro
  ' novamente. Isto irá automaticamente
  ' atualizar o Recordset para exibir os
  ' dados mais recentes.
   Resume

Case 3260 ' O registro está bloqueado.
  intLockCount = intLockCount + 1
  ' Tentando acessar o registro novamente.
  ' Dê ao usuário as opções de Cancel ou
  ' Retry.
   If intLockCount > 2 Then
    intChoice = MsgBox(Err.Description & " Retry?", vbYesNo + vbQuestion)
          If intChoice = vbYes Then
              intLockCount = 1
          Else
             UpdateUnitsInStock = ERR_RECORDLOCKED
             Resume CleanExit
           End If
    End If
    ' Chama o Windows.
  DoEvents
   ' Dá um pequeno intervalo, fazendo-o
  ' mais longo a cada falha de acesso.
 
intRndCount = intLockCount ^ 2 * Int(Rnd * 3000 + 1000)
 
For i = 1 To intRndCount: Next i
   Resume ' Tenta editar novamente.
   Case Else ' Erro antecipado.
        MsgBox "Error " & Err & ": " & Error, vbOKOnly
         UpdateUnitsInStock = FAILED
         Resume CleanExit
End Select
End Function

Bloqueando Registros

Uma das perguntas mais comuns dos desenvolvedores quando começam sua primeira aplicação multiusuários com o Microsoft Jet é:

Como eu bloqueio um registro?

A resposta simples é: Você não o faz.

Você viu que o Microsoft Jet lê, grava, e bloqueia dados em uma página de cada vez, não em um registro por vez. Dependendo do tamanho do registro, a página pode conter mais de um registro.

O problema que surge é que quando você bloqueia um registro, você bloqueia todos os registros naquela página. Isto pode causar um problema em aplicações de alta concorrência que têm que prover acesso aberto a registros específicos.

Você pode usar várias estratégias para contornar este problema. Vejamos:

Uma estratégia é adicionar colunas à tabela usando os tipos de dados Texto até que o registro fique maior que 1024 bytes. Usar o tipo de dados Texto não é recomendado para esta implementação porque é um tipo de dados de tamanho variável, e você tem que preencher os campos explicitamente com dados para obter um formato de tamanho fixo.

Esta técnica pode causar degradações de desempenho porque o tamanho do banco de dados aumentará se todo registro ocupar 2K de espaço no disco.

O modo recomendado é usar o tipo de dados CHAR da SQL DDL que é um tipo de dados de tamanho fixo. Com este método, você não tem que preencher o campo com dados como você faz com os métodos descritos nos parágrafos precedentes. Então, usar o tipo de dados CHAR é o modo mais fácil e garantido para implementar esta solução.

Outra estratégia é usar bloqueio otimista onde quer que seja possível. Embora o bloqueio otimista não elimine bloqueio de página , minimiza a quantia de tempo que um registro é bloqueado e baixa a possibilidade de que um registro não desejado também seja bloqueado.

Finalmente, se nenhum destas abordagens é aceitável, você pode considerar a mudança da sua aplicação para um ambiente cliente/servidor que use um servidor com suporte a bloqueio de registros, ou implementar seu próprio esquema customizado de bloqueio como descrito na seção seguinte.

Implementando um esquema customizado de bloqueio

Às vezes bloqueio de página não é um bloqueio apropriado, e o bloqueio otimista pode não ser uma solução satisfatória. Neste caso, você pode querer considerar seu próprio esquema de bloqueio. Seu código pode sobrepor-se aos bloqueios do Microsoft Jet e pode identificar quando bloquear e desbloquear um registro.

Você pode usar uma tabela de bloqueios para identificar quando um registro é bloqueado. A tabela de bloqueios armazena o valor chave do registro, o estado de bloqueio (bloqueado ou não-bloqueado), e o nome do usuário que tem o registro bloqueado.

A implementação de um esquema de bloqueio próprio requer muito tempo de design, implementação, e teste. Em muitos casos, não pode duplicar a funcionalidade que é obtida no Microsoft Jet.

Por exemplo, até mesmo se você implementar um bloqueio de registro único, seria muito difícil de tratar dados em um Recordset que está baseado em mais de uma tabela, porque você teria que identificar todas as tabelas que contêm registros que têm que ser bloqueados. Esquemas próprios de bloqueios são muito atraentes quando afetam só algumas tabelas e não estão baseados em um modelo de dados com relações e uniões complexas.

Verificando Erros em Bloqueios de Página

Se você está usando bloqueio de página, seu código tem que verificar se uma operação que tenta fazer um bloqueio teve sucesso. Como com os exemplos prévios, você deve desabilitar o tratamento de erros, tente a operação que iniciará um bloqueio, confira os erros, e finalmente, re-habilite o tratamento de erros.

A maioria dos erros de bloqueios de página em aplicações multiusuário que seu código irá encontrar será um dos seguintes três erros. Estes não são os únicos erros - só o mais comuns.

Código de erro e causa Texto e resposta sugerida

3186 Couldn't save - Este erro ocorre quando um usuário tenta atualizar  uma página que contém um bloqueio de leitura  colocado por outro usuário. Para tratar este erro  espere por um período pequeno de tempo e tente salvar o registro novamente. Opcionalmente  você pode informar aos usuários o problema e lhes permitir indicar se querem ou não tentar a operação novamente.

3197 a Microsoft Jet - Este erro acontece quando você usa o método Edit ou o método Update e outro usuário mudou o registro atual desde que você abriu o recordset ou  estão tentando mudar os registros desde a sua última leitura do registro.

Se este erro acontece quando você usa o método Update, então você está usando o bloqueio otimista e o registro mudou desde que você usou o método Edit. Informe o usuário que outra pessoa mudou os dados. Você pode querer exibir os dados atuais e dar ao usuário a escolha de modificar as atualizações feitas pelo outro usuário ou cancelar a edição.

3260 Couldn't update; Este erro acontece quando você usa o método Update ou Edit e a página que contém o registro atual está bloqueada.

Este erro também acontece quando você usa o método Update para salvar um registro em uma página bloqueada. Esta situação pode acontecer quando o usuário está tentando salvar um registro novo ou quando você está usando bloqueio otimista e outro usuário  bloqueia a página. Para tratar este erro, espere por um intervalo pequeno de tempo e então  tente salvar o registro novamente. Opcionalmente você pode informar aos usuários do problema e lhes permitir indicar se eles querem ou não

Principais erros relacionados a banco de dados.

Naturalmente você deve estar preparado para prever os erros potenciais e planejar
o seu tratamento, para isso devera conhecer os principais erros relacionados ao 
seu ambiente de trabalho.

Relacionar aqui todos os erros tratáveis do Visual Basic seria impossível , mas 
vamos tentar listar os principais fornecendo o seu número e descrição, vamos lá:
-------------------------------------------------------------------------
número	Mensagem
-------------------------------------------------------------------------
Erros genéricos.

3005	Database name' isn't a valid database name.
3006	Database 'name' is exclusively locked.
3008	Table 'name' is exclusively locked.
3009	Couldn't lock table 'name'; currently in use.
3010	Table 'name' already exists.
3015	'Index name' isn't an index in this table.
3019	Operation invalid without a current index.
3020	Update or CancelUpdate without AddNew or Edit.
3021	No current record.
3022	Duplicate value in index, primary key, or relationship. Changes were unsuccessful.
3023	AddNew or Edit already used.
3034	Commit or Rollback without BeginTrans.
3036	Database has reached maximum size.
3037	Can't open any more tables or queries.
3040	Disk I/O error during read.
3044	'Path' isn't a valid path.
3046	Couldn't save; currently locked by another user.


Erros relacionados a bloqueio de registros

3027	Can't update.  Database or object is read-only.
3158	Couldn't save record; currently locked by another user.
3167	Record is deleted.
3186	Couldn't save; currently locked by user 'name' on machine 'name'.
3187	Couldn't read; currently locked by user 'name' on machine 'name'.
3188	Couldn't update; currently locked by another session on this machine.
3189	Table 'name' is exclusively locked by user 'name' on machine 'name'.
3197	Data has changed; operation stopped.
3260	Couldn't update; currently locked by user 'name' on machine 'name'.
3261	Table 'name' is exclusively locked by user 'name' on machine 'name'.
3356	The database is opened by user 'name' on machine 'name'.

Erros relacionados a Permissões

3107	Record(s) can't be added; no Insert Data permission on 'name'.
3108	Record(s) can't be edited; no Update Data permission on 'name'.
3109	Record(s) can't be deleted; no Delete Data permission on 'name'.
3110	Couldn't read definitions; no Read Definitions permission for table or query 'name'.
3111	Couldn't create; no Create permission for table or query 'name'.
3112	Record(s) can't be read; no Read Data permission on 'name'.
-------------------------------------------------------------------------

Coleção de Erros do DBEngine

O VB fornece um nível especial de proteção para erros de acesso aos dados
através da geração do objeto Error , o qual armazena as informações 
de erro além do objeto Err. Existe portanto uma coleção Errors
que armazena esses erros.

Para visualizar a coleção de erros podemos usar a rotina:

For x=0 to DBEngine.Errors.Count -1
   MsgBox DBEngine.Errors(x).Description
next

Você pode querer testar um registro para ver se está bloqueado. O procedimento seguinte testa se o registro atual está em uma página bloqueada. Para fazer isto, atribui o bloqueio no Recordset para pessimista e tenta editar o registro. Se o registro está bloqueado, um erro é gerado. Veja um exemplo de código abaixo:

Function RecordLocked(rst As Recordset) As Boolean

 

Dim blnLock As Boolean

On Error GoTo ErrorHandler

 

' Salva o valor atual da propriedade LockEdits.

blnLock = rst.LockEdits

 

' Atribui bloqueio pessimista.

rst.LockEdits = True

 

' Tenta editar o registro. Isto gera

' o erro 3197 se o registro estiver bloqueado.

rst.Edit

RecordLocked = False

rst.CancelUpdate

 

' Restaura o valor original da propriedade LockEdits.

rst.LockEdits = blnLock

Exit Function

ErrorHandler:

 

Select Case Err

Case 3197:

   Resume Next

Case Else

   RecordLocked = True

   Resume Next

   Exit Function

End Select

End Function

Nota Embora você pode usar o código acima para testar um registro e ver se ele está bloqueado, não há nenhuma garantia que o registro permanecerá no estado informado pela função. Se você quer editar um registro, não chame uma função a parte para ver se o registro está bloqueado. Ao invés, tente editar o registro e tratar qualquer erro que possa surgir, como foi mostrado anteriormente no procedimento UpdateUnitsInStock.

Se você pretende mesmo usar a tecnologia ADO , então deve estudar com cuidado a implementação de bloqueio e o tratamento da concorrência em sua aplicação VB6.

Até o próximo artigo VB...
referências:

		José Carlos Macoratti