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