CRUD em AdvPL – Parte 04

Introdução

No post anterior, vimos alguns detalhes e possibilidades de melhoria no código do programa Agenda. Agora, vamos implementar algumas delas, e avaliar as implementações realizadas, e ver outras possibilidades.

Validação no GET

Existem várias formas de consistir ou validar se as informações necessárias para inserir ou alterar um registro da agenda estão sendo fornecidas, e dependendo da informação, podemos inclusive verificar se elas estão corretas.

Uma delas é realizar a consistência sob demanda, para cada campo informado no formulário. Conseguimos fazer isso através da cláusula “VALID” no comando @ ..  GET

Por exemlo, vamos alterar o GET do campo “UF” para o fonte abaixo:

@ 95,60 GET oGet7 VAR cUF PICTURE "!!" SIZE CALCSIZEGET(2) ,12 VALID VldUf(cUF) OF oPanelCrud PIXEL

Agora, vamos inserir no final do fonte a função de validação que acabamos de inserir a chamada para verificar o valor informado no GET:

// Release 1.1 
// Exemplo de validação do campo cUF ( Estado ou Unidade da Federação )

STATIC Function VldUf(cUF)
If Empty(cUF)
  // Estado nao informado / vazio / em branco 
  Return .T.
Endif
If cUF $ "AC,AL,AP,AM,BA,CE,DF,ES,GO,MA,MT,MS,MG,PA,PB,PR,PE,PI,RJ,RN,RS,RO,RR,SC,SP,SE,TO"
  // Estado digitado está na lista ? Beleza
  Return .T.
Endif

// Chegou até aqui ? O estado informado não é válido. 
// Mostra a mensagem e retorna .F., nao permitindo 
// que o foco seja removido do campo 
MsgSTop("Unidade da Federação inválida ou esconhecida : ["+cUF+"] - "+;
       "Informe um valor válido ou deixe este campo em branco.",;
       "Erro na Validação")

Return .F.

Quando você abrir o formulário de dados para incluir ou alterar um registro, uma vez que você dê foco no campo UF, somente será possível remover o foco do campo e executar qualquer outra ação neste formulário quando, ou o campo UF estiver vazio, ou preenchido com a sigla correta de um dos 27 estados do Brasil, contando com o Distrito Federal.

Criação de Novo ID na Inclusão

Como eu havia comentado no post anterior, se dois usuários abem ao mesmo tempo a tela de inclusão de novo registro na Agenda, ambas iriam pegar o último número da base + 1, e mostrá-lo na tela, sem permitir edição, e este valor fatalmente ficaria duplicado na base de dados se ambos os usuários confirmassem a inclusão.

Uma das formas mais “primitivas” de se evitar isso é criar uma função para retornar um novo ID para ser utilizado, e apenas fazer isso no momento efetivo de gravar um novo registro na agenda. Vamos ao fonte:

STATIC Function GetNewID()
Local cNewId
DBSelectArea("AGENDA")
DbsetOrder(1)
DBGobottom()
cNewId := StrZero( val(AGENDA->ID) + 1 , 6 )
Return cNewId

No exemplo acima, ainda temos um problema de concorrência: Se duas instâncias do programa confirmam uma inclusão de registro na agenda no mesmo instante, pode acontecer de ambos os processos pegarem o mesmo ID.

Para resolvermos esta questão, sem usar outro mecanismo para gerar o novo ID, a forma mais simples disso ser feito é utilizando um recurso de seção crítica ou MUTEX (Exclusão Mútua), para garantir  que nunca mais de um processo tempo realize as etapas de obter um novo número e incluir um novo registro ao mesmo tempo.

Assim, caso dois programas executassem a etapa de confirmação da Inclusão re registro, apenas um inciaria o processo, pegando o último número da base, somando uma unidade, utilizando este número na inclusão do novo registro, e somente quando a inclusão estivesse completa, o processo que ficou aguardando a finalização do primeiro pegaria o último número atualizado, para gerar um novo identificador.

O problema óbvio desse tipo de implementação é justamente o fato de você não mais permitir que duas inclusões ocorram em paralelo, apenas uma inclusão por vez pode ser feita. Quando falamos de uma agenda de contatos pessoal, onde praticamente não há concorrência, a geração de dois IDs iguais pode simplesmente não ocorrer, ou acontecer muito esporadicamente. Agora, quando falamos de sistemas transacionais com grandes volumes de dados, precisamos tomar cuidado quanto optamos por tornar um acesso a uma operação sequencial. Sem possibilidade de paralelismo, fatalmente ele vai atingir um limite de operações por segundo usando um processo único.

MUTEX em AdvPL

Existem algumas formas de implementar ou emular um semáforo ou um MUTEX em AdvPL. Algumas considerações básicas sobre cada implementação é o quando “custa” de recursos para a implementação, e qual é o escopo ou abrangência dela.

Semáforo em Disco

Uma forma simples de implementar um semáforo de escopo global, que pode ser visto e compartilhado entre vários servidores de aplicação em um ambiente, é usar as funções de baixo nível de arquivos do AdvPL (FCreate, FOpen, FClose), e criar um arquivo no disco em uma pasta a partir do ROOTPATH do ambiente.

Ao criar um arquivo com a função FCreate(), mesmo que nada seja escrito nele, em caso de sucesso na operação, o arquivo criado permanece aberto em modo exclusivo pelo processo que o criou, podendo ser fechado explicitamente pela função FClose() ou implicitamente no final do processo — inclusive caso o processo seja finalizado de forma elegante (fim da rotina) ou em caso de erro.  Vamos a um exemplo:

User Function Mutex1()
Local nHnd 

nHnd := fCreate('\semaforo\mutex1.lck')

If nHnd >= 0 
   // ------------------------------
   // Arquivo criado com sucesso e aberto em modo exclusivo. 
   // Mesmo que o arquivo já exista no disco, se ele não estiver
   // em uso ou aberto por nenhuma rotina, a função FCreate() 
   // consegue recriar o arquivo. 
   // Desta forma, o que você rodar dentro desse bloco de fonte
   // não será executado por mais de um usuário ao mesmo tempo 

   MsgInfo("Bloqueio obtido. Continue o programa para soltar o bloqueio")

   // Aqui deve rodar o código que não pode rodar 
   // ao mesmo tempo por mais de um usuário 

   FClose(nHnd)
   MsgInfo("Bloqueio liberado.")

Else

   MsgStop("Nao é possível criar/abrir o arquivo de bloqueio.")

Endif

Return

Comportamento esperado

Ao subir um SmartClient rodando o programa U_MUTEX1, ele deve conseguir fazer o bloqueio. Ao aparecer a janela com a mensagem  “Arquivo Bloqueado”, inicie uma segunda instância do SmartClient executando o mesmo programa. Você deve receber a mensagem “Não é possível criar/abrir o arquivo de bloqueio.” Somente será possível um outro SmartClient obter o bloqueio após o primeiro programa ter fechado o arquivo com FClose() ou ter terminado.

Detalhes da implementação

O diretório usado como “Raiz” ou RootPath do ambiente deve ser visível e o mesmo para um determinado ambiente ou Environment em execução no Protheus Server. Inclusive, quando usados mais de um serviço de Protheus Server, o RootPath do ambiente deve ser compartilhado com os demais Slaves do Protheus usando o sistema de compartilhamento de arquivos do sistema operacional em uso. Logo, o escopo dessa implementação global entre ambientes que acessam o mesmo RootPath. 

O peso dessa implementação é um ponto importante. O Excesso de acesso ao sistema de arquivos em disco do servidor pode gerar fila de acesso a disco, caso este recurso seja usado muitas vezes por segundo, para realizar bloqueios curtos de operações concorrentes. Recomenda-se evitar o uso deste tipo de semáforo.

Outro ponto é que este semáforo é do tipo “espera ocupada”, isto é, se você não conseguiu criar o arquivo, pois outro processo está acessando o mesmo em modo exclusivo, você vai ter que repetir a operação quantas vezes for necessário até que você tenha o bloqueio. Isso aumenta mais ainda o custo da implementação, pois o seu processo poderia estar fazendo outra coisa ao invés de tentar até conseguir o bloqueio do arquivo.

Lock Virtual nomeado no DBAccess

Caso sua aplicação use o DBAccess, existe uma forma de criar um bloqueio nomeado virtual, usando as funções TCVLock() e TCVunlok(). O bloqueio é compartilhado entre todas as conexões com o DBAccess feitas para o mesmo ambiente / DSN. Da mesma forma que usamos o sistema de arquivos, podemos usar um lock nomeado no DBAccess.

User Function Mutex2()
Local nHTop

nHTop := tcLink()
IF nHTop < 0 
   MsgStop("Falha de conexão com DBAccess -- Erro "+cValToChar(nHTop))
   return
Endif

If TCVLock('MUTEX2')
  // ------------------------------
  // Bloqueio nomeado criado no DBaccess em memória

  MsgInfo("Bloqueio obtido. Continue o programa para soltar o bloqueio")

  // Aqui deve rodar o código que não pode rodar 
  // ao mesmo tempo por mais de um usuário 

  TCVUnlock('MUTEX2')

  MsgInfo("Bloqueio liberado.")

Else

  MsgStop("Nao foi possível adquirir o bloqueio.")

Endif

TCUnlink(nHTop)
Return

Comportamento esperado

De forma similar ao outro bloqueio, ambos são modelos de espera ocupada ou bloqueio ativo. Não há tempo de espera para obter o bloqueio, caso o identificador nomeado de bloqueio esteja em uso por outro processo, a função TCVLock() retorna .F. imediatamente.

Este bloqueio é um pouco mais leve do que o bloqueio em Disco, pois usa a conexão de rede entre o Protheus Server e o DBAccess. Essa solução é mais interessante do que usar o bloqueio com o arquivo em disco, e atende com louvor a necessidade da Agenda.

Bloqueio usando Framework do ERP

O Framework do ERP Microsiga possui uma função de lock nomeado genérica para ser usada para bloqueios de escopo global, onde parametrizamos inclusive se o escopo do bloqueio deve ser restrito a empresa e filial do usuário atual do sistema. As funções se chamam LockByName() e UnlockByName(), estão documentadas na TDN, e internamente elas utilizam funções internas do servidor de licenças do ERP Microsiga para gerenciar a lista de bloqueios nomeados na memória do license server. Porém, este tipo de bloqueio exige que a aplicação AdvPL em uso esteja utilizando as funções do Framework do ERP Microsiga relacionadas a criação de um contexto de execução do ERP, como a função RpcSetEnv() ou o comando PREPARE ENVIRONMENT, ou que você execute a sua aplicação a parir do Menu do ERP — onde o processo já têm um contexto de execução preparado para você usar as funções do Framework.

Outros MUTEX em AdvPL

Existe um bloqueio global, em memória, com escopo apenas do servidor de aplicação, usando as funções AdvPL GlbLock() e GlbUnlock() — porém este bloqueio não é nomeado. Desse modo, ele somente pode ser usado por uma rotina. Após 2016 foram criadas novas funções de Bloqueio com o mesmo escopo, porém são bloqueios nomeados, usando as funções GlbNmLock() e GlbNmUnlock(). Estas funções estão documentadas na TDN, em uma seção dedicada a funções de sincronismo, vide link nas referências no final do post.

Conclusão

Um processo de melhoria contínua de um sistema normalmente é baseado no crescimento deste sistema, e das adequações que devem ser feitas nos programas para eles suportarem este crescimento. Como a melhor solução é aquela que atende a sua necessidade, com o crescimento do sistema, as necessidades podem mudar, ou surgirem novas. Quanto mais conhecimento da linguagem e da plataforma você tiver, mais fácil será avaliar entre as possibilidades de implementação, qual delas que melhor lhe atende.

No próximo post desta sequência, vamos ver como usar os índices criados no programa Agenda para realizar buscas de dados usando o índice, com exemplos e as respectivas explicações 🙂

Agradeço novamente a audiência, e desejo a todos TERABYTES DE SUCESSO !!! 

Referências

 

 

 

CRUD em AdvPL – Parte 02

Introdução

Continuando de onde paramos na Parte 01, onde vimos as funções de preparação de ambiente de dados, janela principal, inicialização de componentes e os modos da máquina de estados da Agenda, vamos agora para o “miolo” do programa, a função MANAGENDA(), responsável por realizar todas as ações e mudanças de estado da aplicação.

Função MANAGENDA()

 

STATIC Function ManAgenda(oDlg,aBtns,aGets,nAction,nMode)
Local nI , nT
Local cNewId
Local lVolta

If nAction == 1
  // Inclusao
  // Limpa todos os valores de tela,
  // habilitando os campos para edição
  ClearGets(aGets , .T. )

  // Se eu vou incluir, pega o Ultimo ID e soma 1
  DBSelectArea("AGENDA")
  DbsetOrder(1)
  DBGobottom()

  // Coloca o novo ID no primeiro GET
  cNewId := StrZero( val(AGENDA->ID) + 1 , 6 )
  EVAL( aGets[1][2]:bSetGet , cNewId )
  aGets[1][2]:Disable()

  // Joga o foco para o nome
  aGets[2][2]:SetFocus()

  // Seta que o modo atual é Inclusao
  nMode := 1
  AdjustMode(oDlg,aBtns,aGets,nMode)

ElseIf nAction == 2

  // Alteração
  // Se a alteração está habilitada, eu estou posicionado
  // em um registro para alterar . Apenas habilita os GETS, sem mexer no conteuido 
  EnableGets(aGets,.T.)

  // Nao permite alterar o ID
  aGets[1][2]:Disable()

  // Joga o foco para o nome
  aGets[2][2]:SetFocus()

  // Seta que o modo atual é Alteração
  nMode := 2
  AdjustMode(oDlg,aBtns,aGets,nMode)

ElseIf nAction == 3

  // Exclusao
  // Se a exclusão está habilitada, eu estou posicionado em um registro

  // Seta que o modo atual é Exclusão
  nMode := 3
  AdjustMode(oDlg,aBtns,aGets,nMode)

ElseIf nAction == 4

  // Consulta
  DBSelectArea("AGENDA")

  // Primeiro verifica se tem alguma coisa para ser mostrada
  If BOF() .and. EOF()
    MsgStop("Não há registros na Agenda.","Consultar")
    Return .F.
  Endif

  // Consulta em ordem alfabética, inicia no priemiro registro
  DbsetOrder(2)
  DBGotop()

  // Coloca os dados do registro na tela
  ReadRecord(aGets)

  // Seta que o modo atual é Consulta
  nMode := 4
  AdjustMode(oDlg,aBtns,aGets,nMode)

ElseIf nAction == 5 // Confirma

  IF nMode == 1
    // Confirmando uma inclusao
    DBSelectArea("AGENDA")
    DBAppend() // Inicia uma inserção
    // Coloca o valor dos GETs nos respectivos campos do Alias
    PutRecord(aGets)
    // Solta o lock pego automaticamente na inclusao
    // fazendo o flush do registro
    DBRUnlock()
    // Volta ao modo inicial
    nMode := 0
    AdjustMode(oDlg,aBtns,aGets,nMode)
  ElseIF nMode == 2
    // Confirmando uma alteração
    DBSelectArea("AGENDA")
    If DbrLock(recno())
      // Caso tenhha obtido o LOCK para alteração do registro
      // Coloca o valor dos GETs nos respectivos campos do Alias
      PutRecord(aGets)
      // Solta o lock obtido para alteração
      DBRUnlock()
      // Retorna ao modo de consulta
      nMode := 4
      AdjustMode(oDlg,aBtns,aGets,nMode)
    Else
      // Nao conseguiu bloqueio do registro
      // Mostra a mensagem e permanece no modo de alteração
      MsgStop("Registro não pode ser alterado, está sendo usado por outro usuário")
    Endif
  ElseIF nMode == 3
    // Confirmando uma exclusão
    If DbrLock(recno())
      // Apaga o registro ( marca para deleção )
      DBDelete()
      // E tenta posicionar no registro anterior
      DbSkip(-1)
      // Se nao tem mais registros visiveis, volta ao estado inicial
      If BOF() .and. EOF()
        MsgStop("Não há mais registros para visualização")
        // Volta ao modo inicial
        nMode := 0
        AdjustMode(oDlg,aBtns,aGets,nMode)
      Else
        // Retorna ao modo de consulta
        nMode := 4
        AdjustMode(oDlg,aBtns,aGets,nMode)
      Endif
    Else
      // Nao conseguiu bloqueio do registro
      // Mostra a mensagem e volta para o modo de consulta
      MsgStop("Registro não pode ser apagado, está sendo usado por outro usuário")
      nMode := 4
      AdjustMode(oDlg,aBtns,aGets,nMode)
    Endif
  Else
    // Confirmação somente é habilitada para inclusão, alteração e exclusão
    UserException("Unexpected Mode "+cValToChaR(nMode))
  Endif

ElseIf nAction == 6 // Voltar / Abandonar operação atual

  lVolta := .T.

  IF nMode == 1
    // Pergunta se deseja cancelar a inclusão
    // Avisa que qualquer dado digitado será perdido
    lVolta := MsgYesNo("Deseja cancelar a inclusão ? Os dados digitados serão perdidos.")
  ElseIF nMode == 2
    // Pergunta se deseja cancelar a alteração
    // Avisa que qualquer dado digitado será perdido
    lVolta := MsgYesNo("Deseja cancelar a alteração ? Os dados digitados serão perdidos.")
  Endif

  If lVolta
    IF nMode == 2 .or. nMode == 3
      // Se eu estava fazendo uma alteração ou exclusão,
      // eu devo voltar para o modo de consulta
      nMode := 4
      AdjustMode(oDlg,aBtns,aGets,nMode)
    Else
      // Qualquer outro cancelamento, volta ao estado inicial
      nMode := 0
      AdjustMode(oDlg,aBtns,aGets,nMode)
    Endif
  Endif

ElseIf nAction == 7 // Consulta - Primeiro Registro

  DbSelectArea("AGENDA")
  Dbgotop()
  // Coloca na tela o conteudo do registro atual
  ReadRecord(aGets)

ElseIf nAction == 8 // Consulta - Registro anterior

  DbSelectArea("AGENDA")
  DbSkip(-1)
  IF BOF()
    // Bateu no início do arquivo
    MsgInfo("Não há registro anterior. Você está no primeiro registro da Agenda")
  ELSE
    // Coloca na tela o conteudo do registro atual
    ReadRecord(aGets)
  Endif

ElseIf nAction == 9 // Consulta - Próximo Registro

  DbSelectArea("AGENDA")
  DbSkip()
  IF Eof()
    // Bateu no final do arquivo
    MsgInfo("Nao há próximo registro. Você está no últmo registro da Agenda")
    // Reposiciona no ultimo registro 
    DBgobottom()
  Endif
  // Coloca na tela o conteudo do registro atual
  ReadRecord(aGets)

ElseIf nAction == 10 // Consulta - Últmio Registro

  DbSelectArea("AGENDA")
  DBGoBottom()
  // Coloca na tela o conteudo do registro atual
  ReadRecord(aGets)
Else

  UserException("Unexpected Action "+cValToChaR(nAction))

Endif

// Atualiza os componentes da tela
oDlg:Refresh()

Return

Conforme você vai lendo o código, perceba que existem várias funções auxiliares sendo chamadas. Durante muitos pontos da rotina as mesmas operações — com pequenas variações — são necessárias. Para evitar ou minimizar a necessidade de código duplicado, é elegante isolar funcionalidades comuns em funções, e chamá-las durante o processamento. Vamos ver com uma lupa algumas destas funções auxiliares:

STATIC Function ReadRecord(aGets)
Local nI , nT := len(aGets)
Local nPos , cValue
For nI := 1 to nT
  nPos := Fieldpos( aGets[nI][1] )
  cValue := FieldGet(nPos)
  EVAL( aGets[nI][2]:bSetGet, cValue)
Next
Return

A função acima (READRECORD) recebe o Array de componentes TGET da Interface, que foi criado com três colunas: A primeira com o nome do campo na base de dados, a segunda com uma referência ao objeto TGET de interface criado para editar este valor em memória, e a terceira coluna contém o valor inicial do campo para abrir um formulário vazio.

A função READRECORD parte da premissa que a tabela da Agenda está aberta, e posicionada em um registro. No loop, a função identifica o número da posição do campo no ALIAS aberto — função FIELDPOS — passando como parâmetro o nome do campo — guardado na primeira coluna do Array — então ela recupera o valor do campo usando a função FIELDGET(), guardando temporariamente o conteúdo do campo na variável cValue, e por fim ela utiliza a propriedade bSetGet do objeto TGET, para enviar o valor atual para a interface, executando o clobo de código do componente, informando o valor a ser atualizado.

STATIC Function PutRecord(aGets)
Local nI , nT := len(aGets)
Local nPos , cValue
For nI := 1 to len(aGets)
  cValue := EVAL( aGets[nI][2]:bSetGet )
  nPos := Fieldpos(aGets[nI][1])
  Fieldput(nPos, cValue)
Next
Return

Já a função PUTRECORD() faz o contrário da GETRECORD() — ela lê os valores da memória dos objetos TGET da Intterface, e atualiza os valores do banco de dados com os valores obtidos. Usamos também a propriedade bSetGet, mas no EVAL não informamos nenhum valor, para dessa forma apenas recuperar o valor atual do componente, e a função FIELDPUT() para atualizar o valor no registro atual da base de dados. Vale lembrar que é premissa para esta função ser bem sucedida, que o ALIAS da Agenda esteja aberto, posicionado em um registro, e o bloqueio do registro já tenha sido previamente obtido.

STATIC Function EnableGets(aGets,lEnable)
Local nI , nT := len(aGets)
For nI := 1 to nT
  If lEnable
    aGets[nI][2]:Enable()
  Else
    aGets[nI][2]:Disable()
  Endif
Next
Return

A função auxiliar ENABLEGETS() receve o array de componentes como parâmetro, e um valor booleano em lEnable, indicando se os componentes devem ser habilitados — para permitir a edição dos valores — ou desabilitados — para impedir foco e edição dos valores.

STATIC Function ClearGets(aGets,lEnable)
Local nI , nT := len(aGets)
For nI := 1 to nT
  EVAL( aGets[nI][2]:bSetGet , aGets[nI][3] )
  If lEnable
    aGets[nI][2]:Enable()
  Else
    aGets[nI][2]:Disable()
  Endif
Next
Return

Já a função CLEARGETS() possui mais de uma tarefa. Ao ser chamada, ela sempre vai resetar o conteúdo de todos os TGET para o seu valor inicial (em branco) armazenado na terceira coluna do aGets, e também vai habilitar a edição dos dados — ou desabilitar — de acordo com o segundo parâmetro LENABLE.

STATIC Function SetNavBtn(aBtns,lEnable)
IF lEnable
  aBtns[7]:Show() // Primeiro
  aBtns[8]:Show() // Anterior
  aBtns[9]:Show() // Proximo
  aBtns[10]:Show() // Ultimo
Else
  aBtns[7]:Hide() // Primeiro
  aBtns[8]:Hide() // Anterior
  aBtns[9]:Hide() // Proximo
  aBtns[10]:Hide() // Ultimo
Endif
Return

A função SetNavBtn() serve para habilitar ou desabilitar os botões de navegação. Como todos os botões, ao serem criados, foram colocados dentro de um Array, em uma sequência pré-definida, neste caso eu posso endereçar cada um deles diretamente pelo número do elemento do array, e mostrar ou esconder os botões conforme a necessidade.

STATIC Function DisableOPs(aBtns)
aBtns[1]:Disable() // Inclusao
aBtns[2]:Disable() // Alteracao
aBtns[3]:Disable() // Exclusao
aBtns[4]:Disable() // Consulta / Navegação
return

Em vários pontos do programa também foi necessário desligar todos os botões de ações do Menu (exceto o “Sair”). Para isso, foi criada a função DisableOPs.

Agora, a última e não menos importante, a função ADJUSTMODE(). Uma vez que exista uma transição entre um modo da aplicação, a entrada em um novo modo deve mudar o estado dos componentes envolvidos, como os botões e os GETS da Interface. É exatamente isso que ela faz, de acordo com o modo (nMode) atualmente setado.

STATIC Function AdjustMode(oDlg,aBtns,aGets,nMode)
If nMode == 0
  // Modo inicial - Seta Título original da Janela
  oDlg:CTITLE("CRUD - Agenda")
  // Modo incial habilita apenas inclusao e consulta
  // Alteração e Exclusao somente serao habilitados 
  // quando a interface estiver mostrando um registro 
  aBtns[1]:Enable() // Inclusao
  aBtns[2]:Disable() // Alteracao
  aBtns[3]:Disable() // Exclusao
  aBtns[4]:Enable() // Consulta / Navegação
  // Esconde Confirmar e Voltar
  aBtns[5]:Hide() // Confirma
  aBtns[6]:Hide() // Volta
  // Esconde botoes de navegação
  SetNavBtn(aBtns,.F.)
  // Limpa todos os valores de tela , desabilitando os GETS
  ClearGets(aGets , .F. )

ElseIf nMode == 1

  // Ajusta botoes baseado no modo atual ( Inclusao )  
  oDlg:CTITLE("CRUD - Agenda (Inclusão)")
  // Desliga todas as operaçòes
  DisableOPs(aBtns)
  // Mostra Confirmar e Voltar
  aBtns[5]:Show() // Confirmar
  aBtns[6]:Show() // Voltar
  // Esconde botoes de navegação
  SetNavBtn(aBtns,.F.)

ElseIF nMode == 2

  // Ajusta botoes baseado no modo atual ( Alteração )
  oDlg:CTITLE("CRUD - Agenda (Alteração)")
  // Desliga todas as operaçòes
  DisableOPs(aBtns)
  // Mostra Confirmar e Voltar
  aBtns[5]:Show() // Confirmar
  aBtns[6]:Show() // Voltar
  // Esconde botoes de navegação
  SetNavBtn(aBtns,.F.)

ElseIF nMode == 3

  // Ajusta botoes baseado no modo atual ( Exclusão )
  oDlg:CTITLE("CRUD - Agenda (Exclusão)")
  // Desliga todas as operaçòes
  DisableOPs(aBtns)
  // Mostra Confirmar e Voltar
  aBtns[5]:Show() // Confirmar
  aBtns[6]:Show() // Voltar
  // Esconde botoes de navegação
  SetNavBtn(aBtns,.F.)

ElseIF nMode == 4

  // Ajusta botoes baseado no modo atual ( Consulta )
  oDlg:CTITLE("CRUD - Agenda (Consulta)")
  aBtns[1]:Enable() // Inclusao
  aBtns[2]:Enable() // Alteracao
  aBtns[3]:Enable() // Exclusao
  aBtns[4]:Disable() // Consulta / Navegação
  // Esconde Confirmar e Voltar
  aBtns[5]:Hide() // Confirmar
  aBtns[6]:Hide() // Voltar
  // Mostra botoes de navegação apenas na consulta 
  SetNavBtn(aBtns,.T.)

Endif

Return

Para cada modo que entrou em vigor, setado pela função MANAGENDA(), a interface é reconfigurada para atender as premissas e possíveis ações que podem ser executadas quando a interface está neste modo.

Conclusão

Calma que ainda têm a Parte 03 — risos — onde vamos entrar em cada uma das ações executadas pela função MANAGENDA, e explicar alguns porquês, por hora você pode entrar no GITHUB e baixar o fonte AGENDA.PRW, disponível ni Link https://github.com/siga0984/Blog/blob/master/Agenda.prw , ou baixar ele daqui mesmo do BLOG, Agenda — Ele está em formato MS-WORD (extensão .DOCX), mas dentro dele têm o fonte original AGENDA.PRW

Referências

Todas as funções AdvPL utilizadas neste exemplo estão documentadas no site oficial da TOTVS – TDN, nos liks:

 

 

CRUD em AdvPL – Parte 01

Introdução

Para quem não conhece o termo, CRUD é um acrônimo para Create, Read, Update and Delete — usado para referenciar um programa ou interface capaz de criar, consultar, atualizar e apagar dados. Neste post, vamos ver como criar uma agenda simples de contatos em AdvPL, usando apenas as funções básicas da linguagem AdvPL, partido de um ambiente configurado para conectar em um DBMS através do DBACCESS (RPODB=TOP ou RPODB=SQL).

FrameWork x No FrameWork

Quando você usa o módulo Configurador do ERP (SIGACFG), você consegue criar uma tabela customizada com muitos campos, validações, relacionamentos com outras tabelas, consultas dos relacionamentos e afins, utilizando uma interface criada e dedicada para esta tarefa, e com meia dúzia de linhas de código, você cria uma USER FUNCTION para ser chamada de dentro do Menu do ERP, chamando por exemplo uma função MBROWSE ou AXCADASTRO, e em alguns minutos você têm uma interface pronta para utilizar de forma completa um Browse de dados, com pesquisa, filtros e afins.

Agora, quando você resolve fazer uma interface de manutenção de dados sem utilizar nenhuma destas funcionalidades prontas do Framework do ERP, podem ser necessárias muitas linhas de código para se criar algo elegante. Este é o caso deste programa de exemplo, que até o momento está com aproximadamente 750 linhas, com quase nenhuma validação.

Interface utilizada e modelagem do programa

O programa de Agenda foi escrito para ser executado a partir de um SmartClient, onde os dados são visualizados e editados em uma única janela do tipo DIALOG, montada com um painel lateral e um painel de conteúdo, onde o painel lateral possui os botões para as ações básicas do programa (Inserir, Alterar, Excluir, Consultar), e no painel de conteúdo, são mostrados todos os campos da Agenda, e de acordo com a operação sendo realizada, os botões correspondentes as ações que podem ser feitas com as informações presentes na tela — como por exemplo “Confirmar” a operação atual, ou “Voltar” (desistir) de completar a operação.

Basicamente, o programa — primeira versão — foi escrito como uma máquina de estado, usando AdvPL estruturado — sem orientação a objetos — baseada em uma conexão de interface persistente, e acesso a dados feito diretamente pelo mesmo processo. A ideia é evoluir este fonte nos próximos posts, até chegarmos a um modelo orientado a objetos com processamento separado da interface e sem necessidade de persistência de conexão.

Objetivos

Como o fonte em si tem objetivo didático, cada post dessa sequência vai entrar com uma lupa em um pedaço do código, para esmiuçar o papel de cada parte, e no final juntamos tudo em uma aplicação funcional, além de tentar mostrar os motivos que levaram ao código ser escrito desta forma.

Acesso a Dados pelo DBAccess

Visto que o programa é na prática uma agenda básica de contatos, nada mais adequado do que persistir seus dados em um SGDB. Para tal, utilizamos a RDD “TOPCONN” no AdvPL, que permite a criação de tabelas em um Banco de Dados homologado, e emular o acesso ISAM a estas informações — usando as funções já conhecidas de quem programou em Clipper e AdvPL — DBGotop(), DBGobottom(), DbSkip() e afins.

Primeiramente, na abertura da aplicação, será chamada uma função para, entre outras coisas, estabelecer uma conexão com o Banco de Dados configurado no ambiente atual do Protheus Server, testar a existência da tabela de Agenda, criar a tabela e seus índices — caso algum destes não exista — e abrir a tabela em modo compartilhado, junto com seus índices. A tabela será mantida aberta até que o usuário finalize o programa.

Para esta missão, foi criada a função abaixo:

STATIC Function OpenAgenda()
Local nH
Local cFile := "AGENDA"
Local aStru := {}

// Conecta com o DBAccess configurado no ambiente
nH := TCLink()

If nH < 0
  MsgStop("DBAccess - Erro de conexao "+cValToChar(nH))
  QUIT
Endif

If !tccanopen(cFile)
  // Se o arquivo nao existe no banco, cria
  aadd(aStru,{"ID" ,"C",06,0})
  aadd(aStru,{"NOME" ,"C",50,0})
  aadd(aStru,{"ENDER" ,"C",50,0})
  aadd(aStru,{"COMPL" ,"C",20,0})
  aadd(aStru,{"BAIRR" ,"C",30,0})
  aadd(aStru,{"CIDADE","C",40,0})
  aadd(aStru,{"UF" ,"C",02,0})
  aadd(aStru,{"CEP" ,"C",08,0})
  DBCreate(cFile,aStru,"TOPCONN")
Endif

If !tccanopen(cFile,cFile+'1')
  // Se o Indice por ID nao existe, cria
  USE (cFile) ALIAS (cFile) EXCLUSIVE NEW VIA "TOPCONN"
  INDEX ON ID TO (cFile+'1')
  USE
EndIf

If !tccanopen(cFile,cFile+'2')
  // Se o indice por nome nao existe, cria
  USE (cFile) ALIAS (cFile) EXCLUSIVE NEW VIA "TOPCONN"
  INDEX ON NOME TO (cFile+'2')
  USE
EndIf

// Abra o arquivo de agenda em modo compartilhado

USE (cFile) ALIAS AGENDA SHARED NEW VIA "TOPCONN"

If NetErr()
  MsgStop("Falha ao Abrir a Agenda em modo compartilhado.")
  QUIT
Endif

// Liga o filtro para ignorar registros deletados 
SET DELETED ON

// Abre os indices, seleciona ordem por ID
// E Posiciona no primeiro registro 
DbSetIndex(cFile+'1')
DbSetIndex(cFile+'2')
DbSetOrder(1)
DbGoTop()

Return .T.

A estrutura da tabela foi criada no array aStru, apenas caso seja identificado que a tabela não exista no SGDB em uso. O programa cria a tabela e os índices caso estes elementos não existam, e continua o processo de inicialização da aplicação. Uma vez que esta etapa da entrada do programa seja executada, apenas no final do programa precisamos limpar este contexto. Para isso, foi criada a função CloseAgenda(), logo abaixo:

STATIC Function CloseAgenda()

DBCloseAll() // Fecha todas as tabelas
Tcunlink() // Desconecta do DBAccess

Return

Função Principal – User Function Agenda

Tão simples quanto isso,a função Principal ficou bem enxuta, porém ela depende de um conjunto de funções criadas para trabalhar com diferentes estados da interface.

User Function Agenda()
Local oFont
Local oDlg
Local cTitle := "CRUD - Agenda"

// Define Formato de data DD/MM/AAAA
SET DATE BRITISH
SET CENTURY ON

// Usa uma fonte Fixed Size
oFont := TFont():New('Courier new',,-14,.T.)

// Cria a janela principal da Agenda como uma DIALOG
DEFINE DIALOG oDlg TITLE (cTitle) ;
  FROM 0,0 TO 350,800 ;
  FONT oFont ;
  COLOR CLR_BLACK, CLR_LIGHTGRAY PIXEL

// Ativa a janela principal
// O contexto da Agenda e os componentes são colocados na tela
// pela função DoInit()

ACTIVATE DIALOG oDlg CENTER ;
  ON INIT MsgRun("Aguarde...","Iniciando AGENDA", {|| DoInit(oDlg) }) ;
  VALID CanQuit()

// Fecha contexto da Agenda
CloseAgenda()

return

A função principal apenas prepara o ambiente, cria a caixa de diálogo, e deixa todo o resto para ser criado internamente na inicialização da Janela, feita pela função DoInit(), para usar o diálogo atual para criar uma interface multifuncional. O exemplo usa uma fonte mono-espaçada “Courier New”, tamanho de 16 pixels, definida na janela de diálogo, e usada implicitamente por todos os componentes nela inseridos. Vejamos abaixo o fonte da função DoInit():

STATIC Function doInit(oDlg)
Local oPanel
Local oPanelCrud
Local oBtn1,oBtn2,oBtn3,oBtn4,oBtn5,oBtn6
Local oSay1,oSay2,oSay3,oSay4,oSay5,oSay6,oSay7,oSay8
Local oGet1,oGet2,oGet3,oGet4,oGet5,oGet6,oGet7,oGet8
Local aGets := {}
Local aBtns := {}
Local nMode := 0

Local cID := Space(6)
Local cNome := Space(50)
Local cEnder := Space(50)
Local cCompl := Space(20)
Local cBairro := Space(30)
Local cCidade := Space(40)
Local cUF := Space(2)
Local cCEP := Space(8)

CursorArrow() ; CursorWait()

// Abre contexto de dados da agenda
If !OpenAgenda()
  oDlg:End()
  Return .F.
Endif

@ 0,0 MSPANEL oPanelMenu OF oDlg SIZE 70,600 COLOR CLR_WHITE,CLR_GRAY
oPanelMenu:ALIGN := CONTROL_ALIGN_LEFT

@ 0,0 MSPANEL oPanelCrud OF oDlg SIZE 700,600 COLOR CLR_WHITE,CLR_LIGHTGRAY
oPanelCrud:ALIGN := CONTROL_ALIGN_ALLCLIENT

// Cria os botões no Painel Lateral ( Menu )

@ 05,05 BUTTON oBtn1 PROMPT "Incluir" SIZE 60,15 ;
  ACTION ManAgenda(oDlg,aBtns,aGets,1,@nMode) OF oPanelMenu PIXEL
aadd(aBtns,oBtn1) // Botcao de Inclusao

@ 20,05 BUTTON oBtn2 PROMPT "Alterar" SIZE 60,15 ;
  ACTION ManAgenda(oDlg,aBtns,aGets,2,@nMode) OF oPanelMenu PIXEL
aadd(aBtns,oBtn2) // Botao de alteração

@ 35,05 BUTTON oBtn3 PROMPT "Excluir" SIZE 60,15 ;
  ACTION ManAgenda(oDlg,aBtns,aGets,3,@nMode) OF oPanelMenu PIXEL
aadd(aBtns,oBtn3) // Botão de exclusão

@ 50,05 BUTTON oBtn4 PROMPT "Consultar" SIZE 60,15 ;
  ACTION ManAgenda(oDlg,aBtns,aGets,4,@nMode) OF oPanelMenu PIXEL
aadd(aBtns,oBtn4) // Botão de Consulta - Navegação

@ 65,05 BUTTON oBtn5 PROMPT "Sair" SIZE 60,15 ;
  ACTION oDlg:End() OF oPanelMenu PIXEL

// -----------------------------------------------------------------------------
// Desenha os componentes a partir do Painel de Manutenção
// Say sempre 3 linhas abaixo do GET
// -----------------------------------------------------------------------------

@ 5+3,05 SAY oSay1 PROMPT "ID" RIGHT SIZE 50,12 OF oPanelCrud PIXEL
@ 20+3,05 SAY oSay2 PROMPT "Nome" RIGHT SIZE 50,12 OF oPanelCrud PIXEL
@ 35+3,05 SAY oSay3 PROMPT "Endereço" RIGHT SIZE 50,12 OF oPanelCrud PIXEL
@ 50+3,05 SAY oSay4 PROMPT "Complemento" RIGHT SIZE 50,12 OF oPanelCrud PIXEL
@ 65+3,05 SAY oSay5 PROMPT "Bairo" RIGHT SIZE 50,12 OF oPanelCrud PIXEL
@ 80+3,05 SAY oSay6 PROMPT "Cidade" RIGHT SIZE 50,12 OF oPanelCrud PIXEL
@ 95+3,05 SAY oSay7 PROMPT "UF" RIGHT SIZE 50,12 OF oPanelCrud PIXEL
@ 110+3,05 SAY oSay8 PROMPT "CEP" RIGHT SIZE 50,12 OF oPanelCrud PIXEL

@ 5,60 GET oGet1 VAR cID PICTURE "@!" SIZE CALCSIZEGET(6) ,12 OF oPanelCrud PIXEL
@ 20,60 GET oGet2 VAR cNome PICTURE "@!" SIZE CALCSIZEGET(50),12 OF oPanelCrud PIXEL
@ 35,60 GET oGet3 VAR cEnder PICTURE "@!" SIZE CALCSIZEGET(50),12 OF oPanelCrud PIXEL
@ 50,60 GET oGet4 VAR cCompl PICTURE "@!" SIZE CALCSIZEGET(20),12 OF oPanelCrud PIXEL
@ 65,60 GET oGet5 VAR cBairro PICTURE "@!" SIZE CALCSIZEGET(30),12 OF oPanelCrud PIXEL
@ 80,60 GET oGet6 VAR cCidade PICTURE "@!" SIZE CALCSIZEGET(40),12 OF oPanelCrud PIXEL
@ 95,60 GET oGet7 VAR cUF PICTURE "!!" SIZE CALCSIZEGET(2) ,12 OF oPanelCrud PIXEL
@ 110,60 GET oGet8 VAR cCEP PICTURE "@R 99999-999" SIZE CALCSIZEGET(9),12 OF oPanelCrud PIXEL

// Acrescenta no array de GETS o nome do campo
// o objeto TGET correspondente
// e o valor inicial ( em branco ) do campo

aadd( aGets , {"ID" , oGet1 , space(6) } )
aadd( aGets , {"NOME" , oGet2 , space(50) } )
aadd( aGets , {"ENDER" , oGet3 , space(50) } )
aadd( aGets , {"COMPL" , oGet4 , space(20) } )
aadd( aGets , {"BAIRR" , oGet5 , space(30) } )
aadd( aGets , {"CIDADE" , oGet6 , space(40) } )
aadd( aGets , {"UF" , oGet7 , space(2) } )
aadd( aGets , {"CEP" , oGet8 , space(8) } )

// Cria os Botões de Ação sobre os dados
@ 130,60 BUTTON oBtnConf PROMPT "Confirmar" SIZE 50,15 ;
  ACTION ManAgenda(oDlg,aBtns,aGets,5,@nMode) OF oPanelCrud PIXEL

aadd(aBtns,oBtnConf) // [5] Botão de Confirmaçáo

@ 130,125 BUTTON oBtnCanc PROMPT "Voltar" SIZE 50,15 ;
  ACTION ManAgenda(oDlg,aBtns,aGets,6,@nMode) OF oPanelCrud PIXEL

aadd(aBtns,oBtnCanc) // [6] Botão de Cancelamento

// Cria os Botões de Navegação Livre
@ 150,60 BUTTON oBtnFirst PROMPT "Primeiro" SIZE 50,15 ;
  ACTION ManAgenda(oDlg,aBtns,aGets,7,@nMode) OF oPanelCrud PIXEL
aadd(aBtns,oBtnFirst) // [7] Primeiro

@ 150,125 BUTTON oBtnPrev PROMPT "Anterior" SIZE 50,15 ;
  ACTION ManAgenda(oDlg,aBtns,aGets,8,@nMode) OF oPanelCrud PIXEL
aadd(aBtns,oBtnPrev) // [8] Anterior

@ 150,190 BUTTON oBtnNext PROMPT "Próximo" SIZE 50,15 ;
  ACTION ManAgenda(oDlg,aBtns,aGets,9,@nMode) OF oPanelCrud PIXEL
aadd(aBtns,oBtnNext) // [9] Proximo

@ 150,255 BUTTON oBtnLast PROMPT "Último" SIZE 50,15 ;
  ACTION ManAgenda(oDlg,aBtns,aGets,10,@nMode) OF oPanelCrud PIXEL
aadd(aBtns,oBtnLast) // [10] Último

// Seta a interface para o estado inicial
// Habilita apenas inserção e consulta 
nMode := 0
AdjustMode(oDlg,aBtns,aGets,nMode)

Return .T.

A função de inicialização cria os painéis de Menu e Conteúdo do CRUD, bem como todos os @….GET que serão usados para mostrar e editar conteúdo dos registros da agenda em um layout de formulário – um registro por vez.

O conceito de máquina de estado é aplicado na Interface, onde todas as ações disparam a mesma função (MANAGENDA), passando como parâmetros o Array de Botões e Objetos de GET (implicitamente por referência — característica do Array em AdvPL), a ação que está sendo disparada, e o estado atual do componente, este sim explicitamente por referência.

Cada chamada de uma ação (MANAGENDA) verifica qual opção foi disparada, executa uma determinada ação, e ao terminar, pode mudar o estado dos botões e dos conteúdos dos campos da tela.

O estado atual da interface é armazenado na varoável nMode, passada por referência para a função MANAGENDA(). Existem quatro modos de interface :

nMode = 0 : Estado inicial, apenas os botões de Inclusão  e Consulta estão habilitados, o conteúdo do registro em tela está vazio, os botões de navegação de registro e confirmação/cancelamento estão escondidos.

Crud - Init

nMode = 1 : Estado de inclusão : Todos os botões de ação são desabilitados, a tela é mostrada com um registro em branco, apenas o campo ID é mostrado com o próximo número sequencial de Identificação obtido na base de dados somando mais um no último valor encontrado, foco no campo NOME, botões de navegação escondidos, apenas botões de confirmação e cancelamento habilitados.

Crud - Incluir

Como todos os botões do menu — exceto o “Sair” — foram desativados, não é possível mudar de modo (Alteração ou Exclusão, por exemplo) a não ser que o usuário conforme ou cancele a inclusão, usando os botões na parte de baixo do formulário. Após uma inserção, a interface volta sempre para nMode = 0 — modo inicial.

nMode = 2 : Modo de alteração. Somente é habilitada a alteração quando você está dentro do modo de Consulta. A alteração abre os campos para edição no registro que está sendo atualmente consultado.

Crud - Alterar

Após confirmar ou cancelar uma alteração, a interface volta sempre para o modo de consulta (nMode = 4) , mostrando o conteúdo do registro atual.

nMode = 3 : Modo de Exclusão — somente é habilitado quando você está no modo de consulta, e posicionado em um registro. O modo de exclusão apenas habilita os botões de Confirmar e Voltar, para você efetivar ou não a deleção do registro atual da agenda. Sempre após a exclusão, a interface volta ao modo de consulta.

Crud - Excluir

nMode = 4 – Consulta — Somente é possível entrar no modo de consulta após inserir pelo menos um registro na Agenda. A consulta traz no formulário o primeiro registro em ordem alfabética cadastrado na Agenda, e habilita os botões de navegação “Primeiro” , “Anterior“, “Próximo” e “Último“, para permitir navegar nos registros da Agenda. Uma vez que exista uma registro sendo mostrado no painel, as opções de “Alterar” e “Excluir”  são habilitadas.

crud - Consulta

Conclusão

Calma que ainda é cedo, o próximo post da sequência vai mostrar a mágica das mudanças de estado e da manutenção de dados feita dentro da função MANAGENDA(). E, assim que a sequência deste post estiver completa, o fonte inteiro será disponibilizado para Download !

Desejo a todos TERABYTES de sucesso !!!! Curtiu, foi bom para você ? Então faça como eu e compartilhe !!!

Referências

 

Dicas valiosas de programação – Parte 02

Introdução

Continuando na linha de boas práticas e afins, vamos abordar nesse post mais uma dica de ouro, válida para qualquer aplicação que pretende ser escalável, porém com foco no AdvPL

Procure fazer transações curtas

Parece simples, e se olharmos bem, não é assim tão complicado. Primeiro, vamos conceituar uma transação. Na linguagem AdvPL, temos os comandos BEGIN TRANSACTION e END TRANSACTION, que por baixo utilizam recursos do Framework do ERP Microsiga, junto com recursos da linguagem AdvPL, para identificarmos e protegermos uma parte do código responsável por realizar inserção ou atualização de dados em uma tabela.

Quando utilizamos um Banco de Dados relacional no AdvPL, através do DBAccess, proteger a parte do fonte AdvPL que faz alterações no banco de dados é importante, pois evita que, caso ocorra um erro (de qualquer origem)  durante a execução deste trecho, e ele não chegue ao final da transação, nenhuma das operações realizadas com os dados desde o BEGIN TRANSACTION são realmente efetivadas no Banco de Dados, mas sim descartadas. Isso evita por exemplo dados órfãos na base de dados, ou registros incompletos ou inconsistentes.

Por que transações curtas ?

Cada alteração de um registro de qualquer tabela no AdvPL, exige que seja feito um LOCK (ou bloqueio) explícito no registro, para que o mesmo possa ser alterado, e este bloqueio, uma vez adquirido, somente pode ser solto no final da transação. Quanto maior (mais longa) for a duração da transação, por mais tempo este registro permanecerá bloqueado para os demais processos do sistema.

Erros comuns que devem ser evitados

  • “Não abrirás interface com o usuário no meio de uma transação”

Antes de abrir uma transação, a aplicação deve realizar todas as validações possíveis nos conteúdos que estão sendo manipulados. Se qualquer um deles não passar na validação, o programa nem abre a transação. O objetivo da transação é pegar os dados em memória e transferi-los para as tabelas pertinentes, sem interrupções. Ao abrir uma tela ao usuário no meio de uma transação, á transação somente continua depois que o usuário interagir com a interface. Se o operador do sistema submeteu uma operação, virou as costas e foi tomar café, os registros bloqueados pelo processo dele assim permanecerão até que ele volte. Isso pode prejudicar quaisquer outros processos que precisem também atualizar estes mesmos registros.

  •  “Evitarás processamentos desnecessários dentro da transação”

Eu adoro o exemplo do envio de e-mail. Imagine que, durante a gravação de um pedido, exista a necessidade de emissão de um e-mail para inciar um WorkFlow ou um aviso ao responsável pelo setor de compras que um item vendido atingiu um estoque mínimo. Como já visto anteriormente, a solução mais elegante é delegar o envio desse e-mail para um outro processo, preferencialmente notificado de forma assíncrona. Se, em último caso, você precisa que esse email seja disparado na hora, e isso impacta seu processo, não dispare esse email com a transação aberta, lembre-se do tempo que os registros da sua transação vão ficar bloqueados … Grave em um dos campos de STATUS que o envio do e-mail está pendente, e finalize a transação. Então, tente enviar o e-mail, e ao obter sucesso, bloqueie novamente o registro e atualize apenas o campo de STATUS.

  • “Evitarás DEADLOCK”

Ao realizar atualizações concorrentes (mesmo registro tentando ser atualizado por mais de um processo), procure estabelecer uma ordem de obtenção dos bloqueios dos registros, para não fazer a sua aplicação entrar em deadlock — duas transações, que vamos chamar de A e B, já tem cada uma um registro bloqueado de uma determinada tabela. A transação A têm o lock do registro 1, e a transação B têm o lock do registro 2, porém a transação A precisa do lock do registro 2, e a transação B precisa do lock do registro 1, e enquanto nenhuma delas desistir, uma fica esperando a outra e as duas transações “empacam” (efeito conhecido por DEADLOCK).

Se as duas transações ordenassem os registros por uma chave única em ordem crescente, e tentasse obter os locks nesta ordem , o primeiro processo a falhar ainda não teria conseguido pegar o bloqueio de nenhum registro de valor superior ao que o outro processo já tenha obtido, ele apenas esperaria um pouco e tentaria pegar o lock novamente, o que será possível assim que a transação que já tem o lock seja finalizada.

Conclusão

O arroz com feijão é apenas isso. Mas apenas isso já tira um monte de dor de cabeça de DBAs e Administradores de Sistemas, por que mesmo com todo o ferramental disponível, rastrear quem está segurando o lock de quem, em ambientes que passam de centenas de conexões, tome abrir monitor do DBAccess, do Protheus, do SGDB, e caçar a thread que está esperando uma liberação de registro, e a outra que está segurando o lock, não é uma tarefa fácil …

Desejo a todos TERABYTES de SUCESSO 😀

Referências

Entendendo e minimizando deadlocks

DEADLOCK. In: WIKIPÉDIA, a enciclopédia livre. Flórida: Wikimedia Foundation, 2018. Disponível em: <https://pt.wikipedia.org/w/index.php?title=Deadlock&oldid=51816852>. Acesso em: 15 abr. 2018.

 

 

 

 

Protheus no Linux – Parte 03

Introdução

No post anterior, instalamos na VM Ubuntu um Protheus Server, instância única, usando um c-Tree Server DLL (ou BoundServer). Agora, vamos instalar um banco de dados MYSQL e a UnixODBC nesta mesma VM.

Instalando

Após iniciar a VM do Ubuntu Linux — montada no primeiro post –, executamos os comandos abaixo, para instalar o MYSQL e a UNIXODBC, respectivamente:

sudo apt-get install mysql-server mysql-client
sudo apt-get install libmyodbc unixodbc-bin unixodbc

Com estes comandos, nesta versão do Sistema Operacional, o MySQL 5.5 será instalado. Durante a instalação, será perguntada uma senha do usuário “root” do MySQL. Insira uma senha e guarde-a, ela será necessária nas etapas posteriores.

Criando a base no MySQL

Após a instalação, o banco MYSQL já deve estar no ar, mas sem nenhuma base de dados. Utilize o comando “mysql” para acessar o interpretador de comandos do MySql, com a sintaxe abaixo:

mysql -u root -p

Uma vez dentro do interpretador de comandos do MySql, execute os comandos abaixo para criar a sua base de dados. Troque o conteúdo de ‘usuariodebanco’ para um nome de usuário que você queira criar no banco de dados para ter acesso a essa base. Você pode dar o mesmo nome do usuário que você criou para o sistema operacional. E, no lugar de ‘senha’, coloque a senha que você quer atribuir a este usuário, para ele acessar o Banco de Dados.

create database envp11mysql;
grant all on envp11mysql.* to 'usuariodebanco' identified by 'senha';

Mysql Create DB

Configurando a UnixODBC

Para não acessarmos diretamente a .so ( shared object library ) do banco de dados, vamos configurar a UnixODBC no Linux para o Mysql. Primeiro, vamos entrar em “root” mode no Linux, com o comando abaixo:

sudo su

Agora, vamos ver exatamente onde foram instaladas as libs ODBC do MySQL, usando o comando abaixo:

find / -name 'lib*odbc*.so'

O resultado esperado deve ser bem próximo de:

/usr/lib/x86_64-linux-gnu/odbc/libodbcdrvcfg2S.so
/usr/lib/x86_64-linux-gnu/odbc/libodbcnnS.so
/usr/lib/x86_64-linux-gnu/odbc/libodbcdrvcfg1S.so
/usr/lib/x86_64-linux-gnu/odbc/libodbctxtS.so
usr/lib/x86_64-linux-gnu/odbc/liboraodbcS.so
/usr/lib/x86_64-linux-gnu/odbc/libodbcmyS.so
/usr/lib/x86_64-linux-gnu/odbc/libmyodbc.so
/usr/lib/x86_64-linux-gnu/odbc/libodbcpsqlS.so
/usr/lib/x86_64-linux-gnu/odbc/liboplodbcS.so
/usr/lib/x86_64-linux-gnu/odbc/libodbcminiS.so

Os arquivos que nos interessam são os dois em destaque:

libmyodbc.so = MySQL Driver API 
libodbcmyS.so = MySQL Driver Setup

Agora, vamos criar a configuração de instalação da UnixODBC. Usando o editor de arquivos texto no Linux, crie o arquivo odbcinst.ini na pasta /etc/

sudo vi /etc/odbcinst.ini

O conteúdo do arquivo deve ser o seguinte:

[odbc_mysql]
Description     = ODBC for MySQL
Driver          = /usr/lib/x86_64-linux-gnu/odbc/libmyodbc.so
Setup           = /usr/lib/x86_64-linux-gnu/odbc/libodbcmyS.so
UsageCount      = 1

Agora, vamos ver a onde está a configuração de Sockets do MySQL, usando o comando abaixo:

mysqladmin -u root -p version

O resultado deve ser parecido com este aqui:

mysqladmin  Ver 8.42 Distrib 5.5.49, for debian-linux-gnu on x86_64
Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Server version          5.5.49-0ubuntu0.14.04.1
Protocol version        10
Connection              Localhost via UNIX socket
UNIX socket             /var/run/mysqld/mysqld.sock
Uptime:                 23 min 17 sec


Threads: 1  Questions: 583  Slow queries: 0  Opens: 189  
Flush tables: 1  Open tables: 41  Queries per second avg: 0.417

O que nos interessa é a informação em negrito: O caminho do Unix Socket em uso pelo Banco de Dados. Agora, baseado no que já temos até agora, vamos criar a configuração de ODBC efetivamente, apontando para os drivers.

vi /etc/odbc.ini

Podemos partir do seguinte conteúdo:

[envp11mysql]
Description = DSN para Banco P11 no MySQL
Driver      = odbc_mysql
Server      = localhost
Port        = 3306
Socket      = /var/run/mysqld/mysqld.sock
Database    = envp11mysql
Option      = 3
ReadOnly    = No

Na prática, os nomes das seções nos arquivos de configuração somos nós que definimos. No arquivo odbcinst.ini, cada seção configura um driver de banco diferente. No arquivo odbc.ini, cada seção corresponde a uma entrada de DSN da Odbc, que usa um determinado driver.

Após criar e editar os arquivos, vamos efetivar o registro das informações na UnixODBC, inicialmente instalando o Driver que configuramos, usando o comando abaixo.

odbcinst -i -d -f /etc/odbcinst.ini

Agora, vamos instalar a nossa configuração de DSN como “System DSN”, usando o comando abaixo:

odbcinst -i -s -l -f /etc/odbc.ini

Agora, usando o comando abaixo, podemos consultar quais DSN de sistemas estão instaladas:

odbcinst -s -q

E, finalmente, podemos testar a conexão com o MySQL via UnixODBC, usando o comando abaixo, trocando “MYSQLUSER” pelo usuário que nós criamos para o banco envp11mysql , e “MYSQLUSERPASSWORD” trocando pela senha utilizada.

isql -v myodbc_mysql_dsn MYSQLUSER MYSQLUSERPASSWORD

O resultado esperado é :

 

+---------------------------------------+
| Connected!                            |
|                                       |
| sql-statement                         |
| help [tablename]                      |
| quit                                  |
|                                       |
+---------------------------------------+
SQL>

 

Para sair do interpretador de comandos SQL, use a instrução “quit”. Com isso, já temos o MySQL instalado, um banco criado, e a UnixODBC devidamente configurada.

Referências

“Kafee Talk – How to setup and configure MySQL with unixODBC under Ubuntu 14.04”. Disponível em <http://www.kaffeetalk.de/how-to-setup-and-configure-mysql-with-unixodbc-under-ubuntu-14-04/>. Acesso em 16 de Julho de 2016.

Conclusão

Sim, foi mais simples e mais rápido do que imaginávamos. E, com mais um ou dois passos, você configura o MySQL no Linux para aceitar conexões TCP remotas, e pode instalar uma ODBC no Windows, e usar o MYSQL no Linux — Basta editar o arquivo /etc/mysql/my.cnf , localizar a configuração bind-address, e trocar ela para 0.0.0.0 😉 Mas, o foco é usarmos o MySQL no Linux, com o DBAccess também no Linux. E esta etapa será abordada no próximo post dese assunto !!!

Agradeço a todos pela audiência, e lhes desejo TERABYTES DE SUCESSO 😀 

Acesso a dados – DBAccess

Introdução

Nos posts anteriores dos tópicos de acesso a dados, abordamos o mecanismo ISAM e o mecanismo relacional, e vimos alguma coisa do DBAccess da TOTVS, o Gateway de acesso a dados relacionais usado pelo Protheus Server. Hoje vamos aprofundar um pouco o conhecimento sobre esse Gateway, na forma de um FAQ, com muitas perguntas bem cabeludas …rs… seguidas das respectivas respostas.

O que é o DBAccess ?

O DBAccess é um gateway de acesso a dados para bancos de dados relacionais, utilizado pelo Protheus Server, para fornecer uma camada de abstração de acesso a arquivos usando a abordagem ISAM em um SGDB relacional, e uma camada de acesso de consulta relacional através de Queries.

Por que o DBAccess foi construído ?

As versões de ERP da Microsiga, antes do Protheus, originalmente foram concebidos para acessar os formatos de dados nativos do Clipper, que eram DBFNTX e DBFCDX, onde a aplicação acessava diretamente os dados. Com a utilização do driver do ADS Client no Clipper, foi possível utilizar o ADS Server, um SGDB ISAM com arquitetura client-server para o armazenamento dos dados do ERP.

Com o surgimento e popularização dos Bancos de Dados relacionais para baixa plataforma, onde os SGDBs relacionais permitiam mais recursos, consultas mais fáceis e mais rápidas, e novas funcionalidades, além de estender a capacidade do acesso a dados ISAM/xBase, houve a necessidade da Microsiga criar um Gateway de acesso a dados relacionais, que permitisse a execução do código legado (escrito em uma abordagem ISAM) em um SGDB relacional, e permitir acesso a novas funcionalidades disponíveis apenas no ambiente relacional.

Onde entra o TOPConnect nisso ?

O TopConnect foi a primeira geração de Gateways de acesso a dados, sua primeira versão foi concebida antes do Protheus, através de uma parceria comercial com uma empresa estrangeira de desenvolvimento de software. Ela permitia aplicações em Clipper acessar bancos de dados DB2/400 (IBM AS400), Sybase, Microsoft SQL 6.5, Sybase, PostgreSQL, Informix, DB2 UDB, Oracle 6, e MySQL. O TOPConect era escrito em C, e para cada SGDB existia uma versão específica do Gateway.

A Microsiga optou por desenvolver uma tecnologia própria de acesso, sendo assim desenvolvido o TOPConnect 4. Usando C++ com uma abstração de acesso a dados, o mesmo executável possuía as implementações para usar qualquer um dos SGDBs homologados. Como o Gateway é atrelado a versão de produtos do ERP Microsiga, a partir do Protheus 10 ele passou a chamar-se DBACcess, incorporando as implementações para as novas versões dos SGDBs. Com a fundação da TOTVS, o DBAccess passou a ser responsabilidade do Depto de Tecnologia da TOTVS.

O DBAccess consome muito recurso ?

Como um Gateway de acesso a dados, seu consumo de recursos de Memória é diretamente proporcional ao número de conexões, versus o número total de tabelas e queries abertas, versus o tamanho de registro da tabela. Seu consumo de CPU e Rede é diretamente proporcional à quantidade de requisições realizadas pela aplicação AdvPL ao gateway. O consumo é relativamente pequeno por tabela aberta, mas considerando um ambiente de 3000 conexões, onde cada pode abrir e manter abertas mais de 100 tabelas e queries, o consumo de memória pode atingir ou ultrapassar 4GB de RAM.

Como o DBAccess acessa o SGDB ?

O Acesso ao banco Oracle é feito via OCI (Oracle Client Interface), e todos os demais bancos são acessados via ODBC, usando a interface ODBC fornecida pelo fabricante do SGDB. É necessário que a ODBC ou OCI do SGDB em questão esteja instalada no equipamento onde o DBAccess será utilizado.

Por que o DBAccess não têm balanceamento de carga ?

Bem, como o acesso aos dados é feito apenas pelo SGDB, ele sempre será o destino final de todas as conexões. Então, não faz muito sentido “balancear” as conexões do DBAccess. Porém, em ambientes com mais de 2 mil conexões, pode ser muito interessante você usar mais de uma instância de DBAccess em máquinas distintas. Esta topologia é chamada de “DBAccess Distribuído”. Nesta topologia, um ou mais serviços de Protheus apontam para um DBAccess, e todos os DBACcess devem usar uma ODBC apontando para um único SGDB, além de cada DBAccess apontar para uma instância única, nomeada de “master”, que exerce o papel de servidor de locks de registro e locks virtuais.

O que acontece se eu colocar 2 DBAccess apontando para o mesmo Database ?

Se eles não estiverem configurados na topologia de “DBAccess distribuído”, cada um deles vai olhar para o seu próprio controle de emulação de Lock ISAM de registros. Neste caso, pode haver invasão de lock (duas conexões de instâncias distintas vão conseguir fazer RecLock() no mesmo registro), o que pode levar a quebra de integridade dos dados do registro, pois somente a última atualização irá prevalecer), e pode causar DeadLock em transações no SGDB.

Por que o DBAccess não usa o Lock do Banco de Dados ?

O mecanismo de bloqueio de registros do SGDB é de uso intrínseco do Banco de Dados, e não oferece a flexibilidade exigida para o mecanismo de acesso ISAM emulado. Mesmo se fosse construído um mecanismo de emulação direta no SGBD, a quantidade de IOs e instruções para emular isso no SGDB inivabilizariam seu uso por questões de desempenho. O mecanismo de Locks ISAM e Locks Virtuais é feito na memória do DBAccess, de forma muito rápida e eficiente.

Por quê existem os campos R_E_C_N_O_ e D_E_L_E_T_ ?

Como o TOPConnect surgiu devido a necessidade de executar um código escrito originalmente para engine ISAM, precisamos destes campos para emular o comportamento original do ISAM: Uma coluna interna para registrar a numeração sequencial de inserção (ordem física e identificador único de registro), e o campo “D_E_L_E_T” para marcar os registros que foram “marcados para deleção permanente” através da função DbDelete().

E por quê existe a coluna R_E_C_D_E_L_ em algumas tabelas ?

Quando foi implementado o conceito de criação de índice único nas tabelas do ERP, no Protheus 8 se eu não me engano, caso a tabela possua um índice de chave única definido pelo ERP, eu não posso ter um registro “ativo” na base com a mesma chave única. Porém, como as tabelas do DBAccess trabalham com o conceito de deleção “lógica” de registros, marcando os registros a serem eliminados fisicamente usando a função DbDelete(), eu posso ter um ou mais registros marcados para deleção, com a mesma chave única. Por exemplo, eu crio a tabela TESTE com a coluna CHAVE, e crio um índice de chave única com a coluna CHAVE. Se eu usar apenas esta coluna na minha chave única, se eu inserir um registro “000001”, depois deletá-lo ( campo D_E_L_E_T_ está com um “*”), e depois tentar inserir outro registro com “000001”, o SGDB não vai deixar …

Então, quando o ERP solicita ao DBAccess a criação de um índice de chave única, o DBAccess acrescenta na tabela a coluna de controle “R_E_C_D_E_L_”, coloca ela como último campo da chave única, e as colunas R_E_C_D_E_L_ de todos os registros marcados para deleção ( D_E_L_E_T_ = ‘*’) são alimentadas com o conteúdo do R_E_C_N_O_, e todos os registros não marcados para deleção ( ativos ) ficam com este campo ZERADO. Deste modo, eu posso ter um e apenas um registro não marcado para deleção com uma determinada chave única, mas eu posso ter um ou mais registros com o campo chave com este mesmo valor, caso eles estejam marcados para deleção.

Cada vez que o Protheus pede ao DBAccess para marcar um registro como “Deletado” através da função DbDelete(), o DBAcccess verifica se a tabela possui a coluna R_E_C_D_E_L_ , e atualiza ao mesmo tempo a coluna “D_E_L_E_T_” com ‘*’ e o R_E_C_D_E_L_ com o R_E_C_N_O_. Caso um registro seja recuperado, isto é, seja removida a marca de deleção, usando a função DbRecall(), a coluna R_E_C_D_E_L_ é atualizada para “0” zero. Deste modo, se você tentar inserir um registro ativo com uma chave duplicada, o SGDB não deixa fazer a inserção, e se você tentar desmarcar um registro deletado que tenha uma chave, e já existir um registro ativo (não marcado para deleção) com a mesma chave, o SGDB não permite a recuperação do registro, pois isto viola a chave única definida.

O que é mais rápido : Uma consulta ISAM ou por QUERY ?

Normalmente o acesso por Queries é mais rápido, sendo visivelmente mais rápido em leituras de registros sequencialmente. Um acesso de leitura no resultset de uma Query reflete os dados obtidos no momento que a Query foi executada. Já a leitura por acesso ISAM emulado retorna cada registro sob demanda, no momento que o registro é posicionado. A consulta por Query permite JOINS, para busca de dados de tabelas relacionadas. NO ISAM, você é obrigado a posicionar manualmente cada tabela relacionada e realizar a busca sob demanda.

Verdade que o DBSeek() é lento no DBAccess ?

Um DBSeek() no DBAccess procura posicionar no primeiro registro da ordem informada cujos campos que compõe a ordem do índice em uso que foram informados na instrução atendam a condição de busca. Se você faz um DBSeek() para posicionar no primeiro registro imediatamente superior a ordem desejada ( ou “SoftSeek” ) , DBSeek( cChave , .T. ) … o DBAccess pode submeter internamente várias Queries ao SGDB até achar o registro que satisfaça esta condição. Trocar um DBSeek() simples, que apenas verifica se um registro existe ou não, e trocar isso por uma Query, fatalmente vai ser mais lento, pois a abertua de uma Query para pegar apenas um registro vai usar 3 IOs, enquanto o DBSeek() faz apenas um. No caso da busca pelo último registro de uma determinada chave, uma Query pode ser mais rápida. Por exemplo, para saber qual foi a última data de um determinado evento, onde a tabela é indexada pelo código do evento mais a data, ao invés de pegar o código do evento, acrescentar uma unidade, procurar pelo primeiro registro da próxima sequência com uma chave parcial e SoftSeek, e depois fazer um Skip(-1) — coisa que é muito rápida e comum de ser feita no DBF — podemos simplesmente fazer uma query com “select max(datadoevento) as ultdata from tabela where codigodoevento = ‘xxxxxx’ and D_E_L_E_T != ‘*'”

E as inserções e Updates, são rápidas ?

Ao comparamos inserções via instrução direta “INSERT” e a inserção via DBAppend() — modo ISAM — , ambas são muito rápidas. A inserção tradicional de registros pelo AdvPL, usando DBAppend() e replace(s), possui um mecanismo de otimização que prioriza o envio das informações de inserção em apenas um evento de I/O. Porém, dependendo do que é executado durante a atribuição dos campos, enquanto o registro atual está em estado de inserção, o Protheus Server faz um “Flush” da inserção parcial, com os dados disponíveis até aquele momento, podendo executar mais I/Os de update para o mesmo registro enquanto a inserção não for finalizada. Este assunto será mais detalhado em um tópico específico de dicas de desempenho (Acelerando o AdvPL)

O que é o erro -35 ?

O código -35 é um código de erro genérico retornado pelo DBAccess quando a conexão com o SDGB não foi bem sucedida, OU quando a conexão não atendeu aos requisitos de operação com o SGDB. Para saber o que realmente causou o -35, o monitor do DBAccess deve ser acessado, e o registro de eventos de erro do DBAccess deve ser verificado. Pode ser desde o SGDB estar fora do ar, ou usuário e senha não configurados corretamente no DBAccess, falha de criação ou verificação de alguma tabela interna de controle do DBAccess, etc.

O que é o erro -2 ?

O erro -2 indica uma interrupção inesperada da conexão entre o Protheus Server e o DBAccess. Da mesma forma que o erro -35, o -2 pode ter diversas causas, desde um problema de rede entre o Protheus Server e o DBAccesse, ou entre o DBAccess e o SGDB, até um término anormal do processo que estava atendendo esta conexão no DBAccess (Assert Exception, Out Of Memory, Access Violation). Deve ser verificado o LOG do DBAccess para ver os detalhes do que aconteceu.

E os demais erros ?

Cada um possui um significado geral, para um tipo de operação que não foi bem sucedida. O código do erro apenas informa que uma operação falhou, porém somente descobriremos a causa efetiva da falha olhando o log de registro de erros do DBAccess. A lista de códigos de erro do DBAccess está na TDN, no link (http://tdn.totvs.com/pages/viewpage.action?pageId=6064500).

O DBAccess faz “leitura suja” de registros ?

Sim, para a grande maioria dos bancos. Devido a natureza das transações do SGDB, e da necessidade do comportamento esperado do AdvPL em ter acesso de leitura a qualquer registro sem espera ou bloqueio, mesmo que o dado esteja sendo alterado dentro de uma transação por outro processo, foi necessário definir explicitamente o nível de isolamento “READ UNCOMMITED”. O Banco Oracle não tem a possibilidade de leitura “suja”, ele é endereçado com “Read Commited”, mas ele permite que as demais conexões acessem a última versão committed, mesmo que exista uma transação aberta atualizando aquele dado.

O DBAccess “prende” conexões ?

Uma conexão feita pelo Protheus ao DBAccess faz o DBAccess abrir uma conexão no SGDB. Uma vez aberta, boa parte do tempo a conexão no DBAccess fica aberta, esperando o Protheus pedir alguma coisa ao DBAccess, como por exemplo: abrir uma tabela, uma query, posicionar em um registro, inserir um registro, executar uma Stored Procedure, etc. Ao receber uma requisição de abertura de Query, por exemplo, o DBAccess solicita ao SGDB a abertura da Query, e aguarda do SGDB um retorno ( sucesso ou erro). Enquanto isso, o Protheus fica esperando o DBAccess retornar. A Aplicação AdvPL estabelece a conexão com o DBAccess, e pode encerrar a conexão durante a execução da aplicação, e mesmo que o programa não desconecte, quando a Thread da aplicação AdvPL terminar, qualquer conexão com o DBAccess que tenha sido deixada aberta é encerrada. Existem algumas condições que o SGDB pode demorar a responder, como por exemplo um DeadLock no SGDB, a abertura de uma query muito complexa sobre um número muito grande de tabelas, a execução de uma Stored Procedure que fará muito processamento, etc.

Existe uma condição onde uma conexão pode ficar aberta no DBAccess, dando a impressão de estar “presa”: Caso a thread que está executando uma aplicação AdvPL, que consequentemente conectou no DBAccess, seja interrompida por uma ocorrência crítica de erro, como um Access Violation ou Segment Fault (Invasão de memória), e ocorra falha no destrutor da thread. O Processo em si não está mais no ar, mas a conexão permanecera aberta até que o serviço do Protheus seja finalizado.

E, existe também uma última condição, que pode fazer uma conexão no DBAccess permanecer aberta indefinidamente — ou até que o DBAccess seja finalizado, ou a conexão seja encerrada pelo DBAccess Monitor: Caso exista algum problema de rede entre a aplicação Protheus e o DBAccess, e ocorra uma queda na conexão TCP, quando o DBAccess está “IDLE”, esperando pelo Protheus pedir alguma coisa. Se a informação da perda da conexão não chegar ao Socket do DBAccess, ele vai ficar esperando indefinidamente, mantendo a conexão aberta e os recursos pertinentes alocados.

Existem estudos em andamento para criar novas funcionalidades na ferramenta, como permitir derrubar uma conexão do SGDB através do DBAccess, cancelando uma Query ou Stored Procedure, entre outros que eu não posso comentar agora …rs… Aguardem as próximas versões … 😀

Como o DBAccess encerra uma conexão pelo DBAccess Monitor ?

Um processo de conexão entre o DBAccess e um SGDB é mantido no ar enquanto existe a conexão entre o Protheus e o DBAccess. Logo, o DBAccess fica em um laço de espera por requisições. A cada intervalo de alguns segundos que o Protheus não envia nenhuma requisição, e a cada requisição recebida, ele verifica um flag de controle de processo, que indica se o DBAccess Monitor pediu para aquela conexão ser encerrada. Quando você pede ao DBAccess Monitor para encerrar uma conexão, ele apenas seta este flag no processo, que somente será considerado e avaliado quando o DBAccess está esperando por uma requisição do Protheus. Se  DBAccess pediu pro banco a execução de uma Stored PRocedure, que pode demorar de segundos a minutos, e neste meio tempo você pedir para a conexão ser encerrada através do DBAccess Monitor, ele somente vai encerrar a conexão com o SGDB quando a procedure terminar, e ele voltar ao loop de requisições. Se por um acaso você quer que a procedure ou query que está demorando seja interrompida, somente um DBA com acesso ao SGDB consegue derrubar o processo usando algum mecanismo de administração do SGDB.

E se eu derrubar o processo no SGDB ?

Se o DBAccess está executando algo no SGDB, e aguardando por um retorno, e o processo for finalizado no SGDB, o DBAccess receberá um retorno de erro, e repassa ao Protheus. Se o DBAcccess não estava fazendo nada no SGDB, e você derruba a conexão no SGDB …. O DBaccess somente vai “perceber” que a conexão foi pro espaço quando o PRotheus pedir alguma coisa, e o DBAccess for tentar pedir algo para o SGDB através da conexão que não existe mais …

Por que o DBAccess não traz campos MEMO em Query ?

Um campo MEMO pode conter muito mais dados em um campo do que em muitos registros. Uma tabela DBF por padrão traz todos os campos do registro atualmente posicionado a cada cada DbSkip() ou DBSeek(). Para ganhar desempenho e não onerar os processos do sistema que fazem leituras sequenciais em processamentos, os campos MEMO são trafegados somente sob demanda, quando abrimos a tabela em modo ISAM emulado, e somente quando a aplicação tenta ler ou acessar um campo Memo do registro atualmente posicionado.

Como os campos MEMO foram implementados no DBAccess para garantir a compatibilidade com o campo Memo do DBF, a forma de armazenar esta informação em cada SGDB é escolhida diretamente pelo DBAccess, a critério dele. Por questões de economia e otimização de recursos (estamos falando da aplicação que nasceu em um tempo onde uma rede 100 MBits era um “luxo”), foi decidido não retornar campos MEMO em Queries, o DBAccess não faz o Bind dos dados de campos usados como “Memo”. Por isso, se hoje você precisa da informação de um ou mais campos memo de uma tabela, você pode selecionar os dados necessários por Query, porém você recupera também o número do registro (R_E_C_N_O_) na Query, usando um outro nome para este campo, e mantendo a tabela original aberta em modo ISAM emulado, você posiciona no registro desejado usando DbGoto(), e então faz um Fieldget() do campo MEMO.

Quem alimenta o R_E_C_N_O_ da tabela na inserção de registros ?

O DBAccess possui um cache das estruturas de colunas e índices das tabelas, alimentado sob demanda, e um controle de numeração e locks. Ele guarda o número do ultimo registro inserido em uma lista em memória, e cada nova inserção incrementa o último registro da tabela na lista. Este processo é muito rápido, porém torna algo nada prático você fazer inserções através de uma Query ou Stored Procedure. Foi criado um mecanismo para permitir o DBAccess criar uma tabela com numeração automática de R_E_C_N_O_ pelo SGDB, disponibilizado para o FrameWork AdvPl, onde alguns novos módulos desenvolvidos no ERP Microsiga já se utilizam desta funcionalidade. Porém, isto ainda não é extensível para as demais tabelas dos módulos do ERP por questões de impacto. Todas as rotinas hoje escritas que alimentam tabelas, por exemplo via Stored Procedure, precisariam ser refatoradas para contemplar esta funcionalidade, pois nenhuma delas poderia mais fornecer um número de R_E_C_N_O_ ao fazer inserção na tabela, e imediatamente após a inserção, algumas delas precisam obter do SGDB qual foi o número do registro inserido.

Não é algo simples de ser feito, ainda mais “de uma vez”. Para cada SGDB o DBAccess define uma forma de criar o campo com auto-incremento, algumas usando um tipo de campo de auto-incremento do próprio SGDB, outas através de gatilhos criados internamente pelo DBAccess no momento de criação da tabela com estas características.

Para que serve a tabela TOP_FIELD, criada pelo DBAccess ?

Como a aplicação foi feita para emular ISAM, no momento da criação da tabela pelo Protheus, a estrutura da tabela é informada ISAM/DBF, onde especificamos campos do tipo “C” Caractere, “N” numérico (com precisão inteira ou decimal), “D” Data, “L” Lógico e “M” Memo. Porém, como o DBAccess escolhe cada tipo de campo que se adéqua melhor a necessidade, ele precisa guardar algumas definições que o Protheus forneceu na criação da tabela. E ele faz isso na tabenla TOP_FIELD.  Se você copia uma tabela diretamente de um Ddatabase para outro, no mesmo banco, mas não copia as definições da TOP_FIELD, todos os campos “D” data serão mostrados como “C” Caractere de 8 bytes, cmapos “L” lógicos serão “C” caractere de 1 byte, contendo “T” ou “F”, e todos os campos numéricos vão vir com uma precisão de 15 dígitos com 8 decimais.

Por quê as tabelas do DBAccess usam constraints DEFAULT ?

Não existe o valor “NULL” nas bases ISAM usadas pelo ERP Microsiga. Logo, mesmo que o campo esteja vazio, ele precisa ter o seu conteúdo default ( caracteres em branco, números com 0, data com string em branco, booleano com “F”). A aplicação conta com este comportamento, e as queries e joins foram construídas baseadas nesta premissa.

Como o DBAccess faz alteração estrutural na tabela ?

Através da função TC_Alter(), o DBAccess recebe a estrutura atual da tabela e a nova estrutura desejada, e determina para cada SGDB a sequência de operações necessárias para ajustar a tabela para ela ficar com a definição da nova estrutura, através do cruzamento das estruturas. Campos existentes na estrutura antiga e não existentes na nova são removidos, campos existentes na nova e não existentes na antiga são criados, e campos existentes nas duas podem ter suas características alteradas. Apenas as trocas de tipo de “C” Caractere para “N” numérico e vice-versa suportam manter os dados nos campos.

Como funciona a transação no DBAccess ?

Cada SGSB homologado possui transacionamento atômico por instrução. Isto significa que, caso seja disparado uma execução SQL de um Update que afete várias linhas, se uma não pode ser alterada, nenhuma será. Quando precisamos garantir que várias operações em um bloco sejam completas em conjunto, usamos as instruções BEGIN TRANSACTION e END TRANSACTION do Advpl, onde todas as instruções executadas dentro deste bloco não vão fazer COMMIT das informações no SGDB, isto será feito apenas no END TRANSACTION. Se ocorrer algum erro durante o processo, entre o Begin e o End transaction, todas as operações feitas a partir do BEGIN TRANSACTION serão descartadas. Por baixo destas instruções existe uma implementação que depende do ambiente ERP Microsiga, isto é, o processo em execução precisa ser um programa do ERP chamado a partir do Menu, ou um Job que faça a inicialização do ambiente ERP usando por exemplo o comando PREPARE ENVIRONMENT ou a função RcpSetEnv(). Estes tratamentos também estão atrelados às funções RecLock() e MsUnlock() do Framework AdvPL do ERP Microsiga.

O DBAccess pode conectar com outros SGDBs ?

Sim, ele pode. Porém, esta conexão é feita via uma conexão ODBC genérica, que não permite a emulação ISAM. Praticamente qualquer ODBC que você possa registrar como fonte de dados de ODBC no Windows pode ser acessada. Usando a build mais atual do DBAccess, existe uma aba de configuração de ODBC genérica. Você pode estabelecer a conexão usando a função TClink(), informando o banco “ODBC/” mais o alias da fonte de dados cadastrada no Gerenciador de Fontes ODBC do sistema operacional. Com esta conexão, voce pode abrir Queries, que devem ser montadas de acordo com a capacidade e regras da ODBC utilzada, onde os dados retornados podem ser char/varchar ou numéricos, usando DbUseArea() com TcGenWry(), e pode executar instruções diretamente no SGDB através da função AdvPL TcSqlExec(). Isto pode ser muito útil para realizar integrações com outras fontes de dados.

Conclusão

Eu acho que com estes parágrafos, dá pra matar um pouco a curiosidade sobre o DBAccess e seu papel no acesso a dados do ERP Microsiga. Caso algúem tenha mais alguma pergunta a acrescentar sobre este assunto, insira a sua pergunta como um comentário deste post 😀 PAra dúvidas e sugestões de outros assuntos, me envie um e-mail com o assunto “BLOG” para siga0984@gmail.com 😀

Novamente, agradeço a audiência, e desejo a todos TERABYTES de sucesso 😉

Até o próximo post, pessoal 😀

Escalabilidade e Performance – Stored Procedures

Introdução

Em um tópico anterior sobre “Escalabilidade e performance – Técnicas”, um dos tópicos falava sobre Stored Procedures, inclusive sugerindo que seu uso deveria ser minimizado. Vamos entrar neste tema com um pouco mais de profundidade neste tópico. Vamos começar com o clone do tópico abordado, e esmiuçar ele dentro do contexto do AdvPL e Protheus.

Minimize o uso de Stored Procedures

Este é um ponto aberto a discussão, depende muito de cada caso. Não é uma regra geral, existem pontos em um sistema onde uma stored procedure realmente faz diferença, mas seu uso excessivo ou como regra geral para tudo impõe outros custos e consequências. O Princípio 1 diria: “use apenas stored procedures”. No entanto, esta decisão pode causar grandes problemas para o Princípio 2 devido à escalabilidade. As Stored procedures têm a vantagem de ser pré-compiladas, e não há nada mais perto de um dado no Banco de Dados.

Porém Bancos de Dados transacionais são especializados em quatro funções: Sort, Merge, gerência de Locks e Log. A gerência de lock é uma das tarefas mais críticas para a implementação de algoritmos distribuídos, e este é o real motivo de existirem poucos Bancos de Dados que possam implementar a escalabilidade horizontal. Se as máquinas de Banco de Dados têm dificuldade de escalar horizontalmente, ela é um recurso escasso e precioso. Temos então que otimizar seu uso para não consumir excessivamente seus recursos a ponto de onerar os demais processos do ambiente. Isto acaba adiando a necessidade de escalar horizontalmente o SGBD.

Abordando a questão de desempenho

Se o algoritmo para processamento de um grande grupo de informações pode ser escrito dentro de uma Stored Procedure no próprio Banco de Dados, esta alternativa tende fortemente a ser a mais performática. Num cenário onde o algoritmo é escrito usando um programa sendo executado dentro do servidor de aplicação da linguagem, cada processamento que dependa da leitura de grupos de dados e tenha como resultado a geração de novos dados vai ser onerado pelo tempo de rede de tráfego destes dados, na ida e na volta. Logo, com uma base de dados modelada adequadamente, e uma stored procedure bem construída, ela naturalmente será mais rápida do que um processamento que precisa trafegar os dados pra fora do SGDB e depois receba novos dados de fora.

Porém, este recurso não deve ser usado como solução mágica para tudo. Afinal, o SGDB vai processar uma Stored Procedure mais rápido, pois ele não vai esperar um processamento ser realizado “fora dele”, porém o SGDB vai arcar com o custo de ler, processar e gravar a nova informação gerada. Se isto for feito sem critério, você pode mais facilmente esgotar os recursos computacionais do Banco de Dados, ao ponto da execução concorrente de Stored Procedures afetar o desempenho das demais requisições da aplicação.

Outras técnicas pra não esgotar o SGDB

Existem alternativas de adiar um upgrade no SGDB, inclusive em alguns casos as alternativas são a solução para você não precisar comprar um computador da “Nasa” …risos… Normalmente estas alternativas envolvem algum tipo de alteração na aplicação que consome o SGDB.

Réplicas de leitura

Alguns SGDBs permitem criar nativamente réplicas da base de dados acessadas apenas para consulta, onde as cópias de leitura são sincronizadas em requisições assíncronas. Existem muitas partes da aplicação que podem fazer uma leitura “suja”. Neste caso, a aplicação pode ler os dados de uma base sincronizada para leitura, e os processos que precisam de leitura limpa são executados apenas na instância principal. Para isso a aplicação precisaria saber qual e o banco “quente” e qual é o espelho, para fazer as coisas nos lugares certos.

Caches

Outra alternativa é a utilização de caches especialistas, implementados na própria aplicação. Utilizando por exemplo uma instância de um “MemCacheDB” em cada servidor, cada aplicação que pode reaproveitar a leitura de um dado com baixo índice de volatilidade (dados pouco atualizados ou atualizados em momentos específicos), poderiam primeiro consultar o cache, e somente se o cache não têm a informação desejada, a aplicação acessa o banco e popula o cache, definindo uma data de validade. Neste caso, o mais legal a fazer é definir um tempo de validade do cache (Expiration Time). E, paralelo a isso, para informações de baixa volatilidade, a rotina que fizer update desta informação pode eliminar ela do cache no momento que um update for realizado, ou melhor ainda, pode ver se ela se encontra no cache, e em caso afirmativo, ela mesma poderia atualizar o cache 😉

Sequenciamento de operações

Operações de inserção ou atualização de dados que não precisam ser refletidas em real-time no SGDB podem ser enfileiradas em pilhas de requisições, e processadas por um processo dedicado. O enfileiramento de requisições não essenciais em tempo real limita o consumo de recursos para uma determinada atividade. Caso a pilha se torne muito grande, ou um determinado processo dependa do esvaziamento total da pilha, podem ser colocados mais processos para ajudar a desempilhar, consumindo mais recursos apenas quando estritamente necessário.

Escalabilidade Vertical

Devido a esta questão de dificuldade de escalabilidade de bancos relacionais horizontalmente, normalmente recorremos a escalabilidade vertical. Escalamos horizontalmente as máquinas de processamento, colocando mais máquinas menores no cluster e balanceando carga e conexões, e quando a máquina de banco começa a “sentar”, coloca-se uma máquina maior só para o SGDB, com vários processadores, discos, memória e placas de rede. Mas tudo tem um limite, e quando ele for atingido, a sua máquina de Banco de Dados pode ficar mais cara que o seu parque de servidores de processamento.

Dificuldade de Implementação

Usar caches e réplicas e pilhas não é uma tarefa simples, fatores como a própria modelagem da base de dados podem interferir negativamente em algumas destas abordagens. Não se pode colocar tudo em cache, senão não vai ter memória que aguente. O cache é aconselhável para blocos de informações repetidas constantemente requisitadas, e de baixa volatilidade. Também não é necessário criar pilhas para tudo que é requisição, apenas aquelas que não são essenciais em tempo real, e que podem ter um delay em sua efetivação.

Stored Procedures no AdvPL

O ERP Microsiga disponibiliza um pacote de Stored Proecures, aplicadas no SGDB em uso por um programa do módulo “Configurador” (SIGACFG). As procedures foram desenvolvidas para funcionalidades específicas dentro de cada módulo, normalmente aquelas que lidam com grandes volumes de dados, e foi possível criar um algoritmo que realize o processamento dentro do SGDB, trafegando menos dados “pra fora” do Banco de Dados. Normalmente um pacote de procedures é “casado” com a versão dos fontes do Repositório, pois uma alteração na aplicação pode envolver uma alteração na procedure. Os ganhos de performance são nítidos em determinados processamentos, justamente por eliminar uma boa parte do tráfego de informações para fora do SGDB durante os processos.

Conclusão

Dado o SGDB como um recurso “caro e precioso”, como mencionado anteriormente, a utilização de recursos adicionais como réplicas e caches, ajuda a dar mais “fôlego” pro SGDB, você consegue aumentar o seu parque de máquinas e volume de dados processados sem ter que investir proporcionalmente na escalabilidade do SGDB. E em tempos de “cloudificação” , SaaS e IaaS, quando mais conseguimos aproveitar o poder computacional que temos em mãos, melhor !

Desejo novamente a todos TERABYTES de Sucesso 😀

Até o próximo post, pessoal 😉

Referências

“Escalabilidade e performance – Técnicas”