Como criar uma Aplicação Multiusuário com ADO ?


Acho que esta pergunta já deve ter povoado seus pensamentos. E a resposta ?

Ora ! a resposta ? quem precisa dela ?

Na verdade para muitos desenvolvedores que antes programavam em Clipper ( o velho e bom Clipper ...)  ou outra linguagem padrão Xbase e estão migrando para uma ferramenta Visual ( VB é claro..) a pergunta é frequente . O problema é que a resposta , pelo menos uma reposta que não seja apenas teórica , é rara. 

Vamos tentar ( eu disse tentar... ) mostrar de forma clara e detalhada como usar o VB e ADO para criar uma aplicação que seja multiusuário (Não confunda com ambiente Cliente/Servidor). Espero que após ler este artigo até o fim ( ele vai ser disponibilizado por etapas...) você seja capaz de ter essa pergunta respondida.

O que é uma aplicação desenvolvida para um ambiente Multiusuário ?

Uma aplicação desenvolvida para um ambiente multiusuário deverá permitir o acesso de mais de um usuário ao sistema e controlar todas as implicações advindas da concorrência que isto pode acarretar. Ou seja se eu estiver acessando o cadastro de clientes para incluir um novo cliente a aplicação tem que levar em conta que outro usuário poderá estar fazendo a mesma coisa no mesmo momento. Isto sem falar em tentar editar um registro que acabou de ser excluído por outro usuário , e por ai vai...

É claro que o comportamento do meu sistema vai depender de diversos fatores , entre os quais destacamos a base de dados com a qual vamos trabalhar. Nosso estudo de caso irá usar uma base Access , um arquivo mdb , pois creio ser a mais usada neste ambiente.

A teoria sobre o comportamento do Access quando trabalhamos num ambiente multiusuário é vastíssima e não temos a intenção de teorizar mas de praticizar (uau! que neologismo...). Por isto não espere profundas análises neste artigo , vamos focar o prático do dia a dia. (Nada de camarão , ficamos com o arroz com feijão.)  

O banco de dados e as tabelas

Neste artigo eu estou supondo que você saiba o que é um banco de dados e o que é uma tabela , e também que você saiba distinguir uma tabela de um banco de dados. Embora o Access tenha uma interface muito amigável e torne bem mais fácil a tarefa de criar banco de dados e tabelas com seus relacionamentos e tudo mais , não pense que é só sentar na frente do micro e ir criando seu banco de dados e tabelas que sua aplicação já vai pode estar funcionando. Creio que o fundamental de uma aplicação comercial com banco de dados é justamente a análise do sistema com um banco de dados enxuto e tabelas normalizadas. 

Dependendo do tamanho da aplicação , a utilização de uma ferramenta Case irá tornar o trabalho ainda muito mais produtivo e fácil. Se você quer criar um cadastro de clientes que envolve uma tabela apenas tudo bem , mas quando você começa a usar duas , três , .., dez tabelas , a coisa vai complicando.

Então , para estes casos , é preciso ir com calma no desenho e criação da base de dados , pois ela vai ser o fundamento da sua aplicação. Já pensou em depois de construir uma casa , estando na fase de acabamento , ter que mexer no alicerce ? Pois é mais o menos isto que acontece com aplicações com tabelas mal desenhadas e não normalizadas.

O inicio - Desenhando a base de dados

Definindo o nosso esquema de trabalho:

Tabela Produtos

Tabela Categorias

Tabela Vendas

Relacionamento entre as tabelas

Descrevendo as tabelas do sistema:

A tabela Produtos contém de forma simplificada informações sobre o produto que iremos tratar : que tal CD´s. Sim você possui uma loja que vende CD´s de música e vai criar uma aplicação multiusuário para cadastrar o seu produto. O formulário da aplicação deverá permitir ao usuário informar o Nome do Produto , a Categoria e se o produto esta ou não em promoção.

Esqueci o Código do Produto ? Não !!! Este campo (ProdutoID) é definido como sendo do tipo Autonumeração , então o próprio banco de dados irá cuidar de incrementar o Código do produto não permitindo assim codigos duplicados.

A tabela Categoria deverá indicar a que categoria o produto pertence. No nosso caso vamos pensar nesta categoria como o tipo de música do produto ( CD ) : Vamos cadastrar somente 3 Tipos : Rock , MPB e Jazz. O campo CategoriaID identifica o código da categoria e é do tipo Autonumeração e chave primária não permitindo duplicações neste item.

A tabela Vendas contém o código do produto , a quantidade e o percentual das vendas. O código do produto ( ProdutoID) esta indexado e permite a duplicação.

Os relacionamentos

Temos um relacionamento entre o campo CategoriaID (tabela Categorias)  e o campo ProdutoCategoria (tabela Produtos) . O relacionamento do tipo um-para-muitos: (ver abaixo)

 

O Outro relacionamento é entre o campo ProdutoID (tabela Produtos) e o campo ProdutoID (Vendas). O relacionamento é do tipo um-para-muitos. (ver abaixo)

Por que criamos este relacionamentos ??? Bem , ao impor a integridade referencial deixamos a cargo do Banco de dados verificar , no caso dos campos relacionados , a validade da inclusão dos registros. No  nosso caso somente poderemos incluir na tabela Produtos uma categoria que exista previamente cadastrada na tabela Categorias. Com isso evitamos registros orfãos. Se você tentar informar uma categoria que não esteja cadastrada ocorrerá um erro que cabe a você tratar e direcionar através de uma mensagem amigável ao usuário. ( Algo do tipo : Se toca cara... )

Criando a interface com o usuário

Vamos criar agora a interface com o usuário. Nosso projeto terá dois formulários básicos:  o formulário principal ( frmprincipal) e o formulário de dados ( frmadados).

1-) O formulário principal - Abaixo temos o jeitão do formulário principal:

 

O formulário principal contém um controle PictureBox que servirá como container , i.e, conterá os demais controles que comporão o que pode ser chamado de menu da aplicação.

O controle Label exibe uma explicação ao usuário de que ele deve clicar duas vezes para gerar duas instâncias ( no mínimo ) do formulário de dados.

Os ícones - Gravar Dados e Sair - são inseridos em controles Image e permitem o acesso ao formulário de dados e encerrar o sistema.

 

2-) O formulário de dados - O formulário de dados permitirá a manutenção  de dados de nossa tabela de produtos.

Nele temos as opções : Editar , Incluir, Excluir , Refresca e Sair. Veja figura abaixo:

 

O formulário de dados contém labels de para identificar os campos que serão exibidos em controles TextBox ( Código , Nome Produto, Categoria ) e o controle checkbox estará informando se o produto esta em promoção.

Os botões de comando - Editar, Incluir, Excluir, Refresca e sair - permitem a operação com os registros da tabela.

A navegação pelo banco de dados é feita pelos botões de comando com os sinais: |< , <   e > >| )

Como funciona ?

Nosso objetivo é mostrar como funciona uma aplicação multiusuário. Para fazer os testes, o ideal seria ter realmente uma aplicação instalada em uma rede , mas para podermos permitir os testes em uma máquina local vamos criar duas ou mais instâncias de nossa aplicação para simular o ambiente multiusuário.

Vejamos o código do formulário principal:

Private Sub Image1_Click()
'Abre uma instância dodo formulario de dados

 Dim frmNew As frmSingleRec
 Static intFormContador As Integer

 On Error GoTo trata_erros

 intFormContador = intFormContador + 1

 Set frmNew = New frmDados
 Load frmNew
 frmNew.Caption = "Formulários de Dados Multiusuário, Instancia => #" & intFormContador
 frmNew.Show

Exit Sub
trata_erros:
    MsgBox Err.Description
End Sub

Quando clicamos pela primeira vez em Gravar Dados temos uma instância do formulário frmDados criada e exibida ao usuário. A partir dai toda vez que clicarmos esta opção iremos estar criando uma nova instância do formulário simulando com isto a concorrência de um ambiente multiusuário. ( Uma outra forma de você fazer isto sem criar várias instâncias de um mesmo formulário seria abrir o banco de dados com Access ,e ,  deixando-o aberto iniciar o aplicativo gerando uma instância do formulário)

Private Sub Form_Unload(Cancel As Integer)
  Dim i As Integer

  On Error GoTo trata_erros

  While Forms.Count > 1
  i = 0
     While Forms(i).Caption = Me.Caption
        i = i + 1
     Wend
     Unload Forms(i)
 Wend

  Unload Me
  End

Exit Sub
trata_erros:
  MsgBox Err.Description
End Sub

Vamos executar o aplicativo e criar duas instâncias do formulário para entrada de dados. A seguir vamos simular as situações de concorrência que podem acontecer no dia a dia de um sistema multiusuário:

Situação 1 - Um usuário A esta editando o registro 1 . Neste exato momento em outra estação o  usuário B resolveu excluir o registro 1.  O que deve acontecer ?  

- O sistema deve estar pronto para avisar ao usuário B que o registro 1 esta bloqueado para edição.

O código responsável por isto é dado a seguir:

Private Sub cmdexcluir_Click()

If MsgBox("O registro será excluido definitivamente. Continua ?", vbYesNoCancel + vbExclamation, "Confirma Exclusão") <> vbYes Then Exit Sub

On Error Resume Next
mrsPrimary.Delete

Select Case Err.Number
Case 0: 'exclusao foi um sucesso
If cmdMoveNext.Enabled Then
   cmdMoveNext_Click
Else
   If cmdMovePrevious.Enabled Then cmdMovePrevious_Click
End If
Case -2147217864
  MsgBox "Esta linha já foi excluida por outro usuário!", vbInformation
  mrsPrimary.CancelUpdate
Case -2147467259
   MsgBox "As alterações feitas não podem ser salvas no momento. O registro encontra-se bloqueado pelo por outro usuario." & vbCr & "Voce pode cancelar as alteracoes ou tentar salvar mais tarde...", vbExclamation, "Erro de gravacao"
  Exit Sub
Case Else
   MsgBox "O registro nao pode ser excluido." + vbCrLf + Err.Description
   mrsPrimary.CancelUpdate
End Select
Exit Sub

Trata_Erro:
Exibe_Erros (Err.Description)

End Sub

Aqui interceptamos os possíveis erros que ocorreriam neste caso e exibimos a mensagem ao usuário. No caso o erro -2147467259 ocorre .

- Situação 2 - O usuário A e o usuário B estão editando o mesmo registro ( no caso o registro 1) . O usuário A terminou de fazer suas alterações e salvou-as. Em seguida o usuário B termina de fazer suas alterações e tenta salvá-las. O que ocorre ?

- O sistema não deve permitir que o usuário B salve as alterações , deve avisá-lo que o registro acabou de ser alterado por outro usuário.

 - O código responsável por essa façanha é o seguinte:

Private Sub cmdsalvar_Click()
'Salva

Dim vFieldArray(), x As Integer, intUpdateError As Integer, strErrorMessage As String, oError As Error
Dim blnAdd As Boolean

On Error Resume Next 'limpa o objeto error

blnAdd = mrsPrimary.EditMode = adEditAdd
mrsPrimary.ActiveConnection.Errors.Clear

Screen.MousePointer = vbHourglass
mrsPrimary.Update
Screen.MousePointer = vbNormal

intUpdateError = Err.Number 'armazena os erros(O objeto error sera resetado pelo proxima linha)

On Error GoTo TrataErros

Select Case intUpdateError
Case 0:
  If mrsPrimary.ActiveConnection.Errors.Count = 0 Then 'nao ocorreu nenhum erro
      Modo_Edicao (Navegacao)
      Atualiza_Botoes_Navegacao_Posicao
   If blnAdd Then
       mrsPrimary.Resync adAffectCurrent 'exibe os valores padrao
       mrsPrimary.Move 0 'forca uma atualiacao dos controles para exibir os dados
    End If
 Else
 For Each oError In mrsPrimary.ActiveConnection.Errors
 If oError.Number = -2147217864 Then
    strErrorMessage = "Este registro já foi recentemente alterado por outro usuário ! "
    MsgBox strErrorMessage
    Exit Sub
 Else
    strErrorMessage = strErrorMessage & oError.Description & vbCr
 End If
Next

If mrsPrimary.ActiveConnection.Errors.Count = 1 Then strErrorMessage = "Os seguintes erros" & _

  IIf(mrsPrimary.ActiveConnection.Errors.Count > 1, "", " foram ") & " definidos pelo provedor : " & vbCr &   strErrorMessage MsgBox strErrorMessage ' exibe todos os erros

End If

Case 3640 + vbObjectError 'registro alterado por outro usuario
   If MsgBox("Outro usuario alterou ester registro deste de que voce comeceou a edição.Se salvar o   registro vai sobrescrever as alterações anteriores." & vbCr & vbCr & " Confirma gravação ?", vbExclamation +   vbYesNoCancel, "Confligto na gravação") = vbYes   

Then
   'forca uma sobrescrita dos dados devemos armazenar os dados em um buffer
 
  ReDim vFieldArray(mrsPrimary.Fields.Count - 1)
    For x = 0 To mrsPrimary.Fields.Count - 1
      vFieldArray(x) = mrsPrimary.Fields(x).Value
    Next x

    mrsPrimary.CancelUpdate
    mrsPrimary.Resync adAffectCurrent

    For x = 0 To mrsPrimary.Fields.Count - 1 'salva as alteracoes no banco de dados
          If mrsPrimary.Fields(x).Value <> vFieldArray(x) Then mrsPrimary.Fields(x) = vFieldArray(x)
    Next x

    mrsPrimary.Update
    Atualiza_Botoes_Navegacao_Posicao (EditMode)

    Modo_Edicao (Navegacao)
    Atualiza_Botoes_Navegacao_Posicao
 Else 'usuario escolheu nao sobrescrever os dados
    mrsPrimary.CancelUpdate
    mrsPrimary.Resync adAffectCurrent 'exibe os ultimos dados
    mrsPrimary.Move 0
 End If
Case -2147467259
 MsgBox "As alterações feitas não podem ser salvas no momento. O registro encontra-se bloqueado pelo por outro usuario." & vbCr & "Voce pode cancelar as alteracoes ou tentar salvar mais tarde...", vbExclamation, "Erro de gravacao"
Exit Sub
Case Else:
  MsgBox Err.Description + vbCr & "(Origem: cmdsalvar_Click)", vbExclamation, "Error"
End Select

Exit Sub
TrataErros:
If Err.Number = -2147217885 Then ' A chave para este registros foi alterado ou excluida
MsgBox "Se for usar o Access 97 ele nao atualiza o campo autonumeracao apos a atualizacao. Para exibir o codigo do produto atualizado pressione o botao atualizar...", vbInformation
  

 If cmdatualizar.Visible Then 

      cmdatualizar.SetFocus
  Else
    Exibe_Erros (Err.Description)
 End If

End Sub

O código do botão para editar os registros - Editar - é mostrado a seguir , nele estamos prevendo o caso do registro que esta sendo editado ter sido excluído por outro usuário.

Private Sub cmdeditar_Click()
On Error GoTo Trata_Erro

mrsPrimary.Resync adAffectCurrent
'pega o ultimo dado para editar
mrsPrimary.Move 0 'atualiza os controles

Modo_Edicao (Editando)
Atualiza_Botoes_Navegacao_Posicao (EditMode)

Exit Sub

Trata_Erro:
Select Case Err.Number
    Case -2147217885 'a linha foi excluida
          MsgBox "Esta linha foi excluida por outro usuario...", vbInformation
Case Else
          Exibe_Erros (Err.Description)
End Select

End Sub

Voce já percebeu que para tratar os conflitos no ambiente multiusuário basta prever e tratar os erros que serão gerados pela aplicação. Basta planejar com cuidado e testar com paciência , não há segredo nenhum no código envolvido. Você somente vai ter que saber os possíveis erros gerados em tempo de execução pelos conflitos , tratá-los através de uma interface amigável e de forma transparente ao usuário.

Creio que você já teve uma noção de como lidar com a programação em ambiente multiusuário. É obvio que não esgotei o assunto , na verdade nem arranhei , mas o que foi mostrado até aqui o capacitará a adaptar o código as suas necessidades além de corrigir alguns bugs presentes no meu exemplo. :-)

Para pegar uma cópia do projeto clique no link -> Adomulti.zip  

Veja os Destaques e novidades do SUPER DVD Visual Basic (sempre atualizado) : clique e confira !

Quer migrar para o VB .NET ?

Quer aprender C# ??

Quer aprender os conceitos da Programação Orientada a objetos ?

Quer aprender o gerar relatórios com o ReportViewer no VS 2013 ?

Referências:


José Carlos Macoratti