CRUD em AdvPL – Parte 15

Introdução

No post anterior, foi feito um Controle de Acesso para a Agenda, certo? Porém, o controle ainda não funciona, pois não foi feita a rotina de manutenção do cadastro de usuários. Logo, vamos criar esta rotina, mas antes disso vamos ver um pouco sobre reaproveitamento de código. Assim, quando partirmos para a rotina de manutenção de usuários não vamos precisar de tantas linhas de código como foi necessário na Agenda.

Reaproveitamento de Código

Quando você programa de modo segmentado, deixando claro o papel de cada função, mesmo que você não use OO (Orientação a Objetos), é relativamente fácil reaproveitar uma função para uma mesma tarefa, feita por entidades do programa diferentes.

Vamos começar pelo acesso aos dados. No programa AGENDA.PRW, existem duas funções, chamadas de OpenDB() e CloseDB(). Ambas, como foram inicialmente projetadas para serem chamadas apenas de dentro do fonte AGENDA.PRW, foram declaradas como STATIC FUNCTION. De fato, o uso de uma STATIC FUNCTION inicialmente limita a chamada desta função para outras funções ou mesmo métodos de classes, desde que eles sejam escritos dentro do mesmo arquivo fonte.

Porém, quando utilizamos o Protheus como plataforma de desenvolvimento, sem uma autorização especial fornecida pela TOTVS, você não consegue declarar uma FUNCTION no código, que poderia ser chamada de outros arquivos fonte. Quando não temos nenhuma autorização diferenciada de compilação, o ambiente Protheus permite a compilação de CLASSES ADVPL,  STATIC FUNCTION(s) e USER FUNCTION(s).

Escopo e Chamada de Funções

Quando declaramos uma USER FUNCTION, por exemplo USER FUNCTION AGENDA(), na verdade a função compilada no repositório de objetos chama-se U_AGENDA — É inserido o prefixo “U_” no nome da função. Uma USER FUNCTION pode ser chamada entre fontes distintos, porém reduz ainda mais a parte útil do nome da função. Exceto quando programamos em TL++ (ainda vamos falar disso em outro post), o AdvPL padrão permite a declaração de funções com até 10 caracteres em seu nome. Logo, quando usamos uma USER FUNCTION, o prefixo “U_” já usa dois caracteres, sobrando apenas 8 caracteres para identificar a função.

Você pode inclusive declarar a função com mais de 10 caracteres, e chamar ela com mais de 10 caracteres, PORÉM somente os 10 primeiros caracteres serão considerados, tanto na declaração quanto na chamada. Por exemplo, declare uma função chamada USER FUNCTION MEUTESTEOK(), e agora, de dentro de outra função, chame U_MEUTESTEX() — a função U_MEUTESTEOK será chamada. A função real compilada no Repositório vai se chamar U_MEUTESTE — com apenas os 10 caracteres significativos — então, se você chamar a função como U_MEUTESTE1 ou U_MEUTESTEX, são considerados apenas os 10 caracteres iniciais do nome da função, inclusive os dois caracteres do prefixo “U_”.

Quando utilizado TL++, onde existe suporte para funções, variáveis, classes, proriedades e métodos com mais de 10 caracteres — acho que vai até 250 — um fonte escrito assim daria erro, pois a chamada da função deve conter o nome inteiro igual a declaração da mesma.

Escopo de Classes AdvPL

Como as classes no AdvPL têm um fator de dinamismo bem alto, e não é necessário possuir um arquivo auxiliar ou #include com a definição da classe para ser possível consumir a classe em AdvPL, basta saber o nome da classe, seus métodos e propriedades para chamar seus construtores, métodos e propriedades, de qualquer arquivo fonte compilado no repositório.

De volta ao reaproveitamento

Se eu quiser usar as funções OpenDB() e CloseDB(), hoje declaradas como STATIC FUNCTION, em mais de um código ou programa fonte do projeto, eu preciso redefinir esta função.

A primeira alternativa poderia ser alterar a declaração de STATIC FUNCTION OPENDB() para USER FUNCTION OPENDB(), e trocar no fonte atual todas as chamadas dela, que originalmente usavam apenas OpenDb(), para o novo nome da função, que passou a se chamar U_OPENDB()

Outra alternativa — mais elegante — seria criar uma classe para agrupar estas funcionalidades — conexão com um Banco de Dados relacional através do DBAccess — e declarar estas funções como métodos da classe, para então criar uma instância da classe no início da execução do código, e fazer a chamada dos seus métodos no lugar das funções.

Embora ambas atendam a nossa necessidade, vamos optar pela mais elegante, vamos criar uma classe, em um novo fonte AdvPL, que deverá ser compilado no Projeto, para então utilizá-lo onde for necessário, em qualquer fonte do projeto.

Funções OpenDB() e CloseDB()

Vamos ver como eram estas funções:

STATIC Function OpenDB()
// Conecta com o DBAccess configurado no ambiente
nH := TCLink()
If nH < 0
  MsgStop("DBAccess - Erro de conexao "+cValToChar(nH))
  QUIT
Endif
// Liga o filtro para ignorar registros deletados na navegação ISAM 
SET DELETED ON
Return

STATIC Function CloseDB()
DBCloseAll()   // Fecha todas as tabelas
Tcunlink()     // Desconecta do DBAccess
Return

Agora, vamos ver como elas ficariam sendo escritas como métodos de uma classe — que vamos chamar de DBDriver,  criada no novo fonte DBDRIVER.PRW:

CLASS DBDriver
   DATA nHnd as INTEGER 
   METHOD New() CONSTRUCTOR 
   METHOD OpenDB()
   METHOD CloseDB()
ENDCLASS

METHOD New() CLASS DBDriver
::nHnd := -1
return self

METHOD OpenDB() CLASS DBDriver
If ::nHnd > 0 
  // Já estou conectado, torna esta conexão ativa 
  TCSetConn(::nHnd) 
  Return .T. 
Endif
// Conecta com o DBAccess configurado no ambiente
::nHnd := TCLink()
If ::nHnd < 0
  MsgStop("DBAccess - Erro de conexao "+cValToChar(::nHnd))
  Return .F.
Endif
// Liga o filtro para ignorar registros deletados na navegação ISAM 
SET DELETED ON
Return .T.

METHOD CloseDB() CLASS DBDriver
If ::nHnd > 0 
   DBCloseAll() // Fecha todas as tabelas e alias 
   Tcunlink(::nHnd) // Encerra a conexao atual com o DBAccess
   ::nHnd := -1 
Endif
Return .T.

Reparem no que houve, Eu criei um novo código fonte, chamado DBDRIVER.PRW, e dentro dele transformei as funções OpenDB() e CloseDB() em dos métodos. Depois, nos pontos do código onde as antigas funções eram chamadas, eu passsei a criar uma instância do Driver em uma variável local, chamada oDBSrv, chamando o construtor do Driver — DBDriver():New() — e nas chamadas das funções OpenDB() e CloseDB() , eu passei a chamar elas como métodos da classe, usando oDBSrv:OpenDB() e oDBSrv:CloseDB(). Dessa forma, eu estou isolando estas funcionalidades em um fonte separado, para ser chamado por outros fontes que precisem realizar as mesmas tarefas, como o fonte da Agenda e o fonte do Cadastro de Usuários.

Reaproveitando mais …

Ao implementar o cadastro de usuários na Agenda, a função OpenUsers() foi praticamente uma cópia da OpenAgenda(), trocando o nome da tabela e dos campos. Que tal fazermos dela apenas um método na nova classe DBDRiver?

METHOD OpenTable(cFile,aStru,cIdxUnq,aIndex) CLASS DBDriver
Local cLockId , aDbStru
Local nI , nIndex, cIndex

// Ajusta nome da tabela e cria 
// identificador de Bloqueio de operação
cFile := Upper(cFile)
cLockId := cFile+"_DB"

If empty(aIndex)
   aIndex := {}
Endif
// Quantos indices permanentes tem esta tabela 
nIndex := len(aIndex)

While !GlbNmLock(cLockId)
  If !MsgYesNo("Existe outro processo abrindo a tabela "+cFile+". "+;
               "Deseja tentar novamente ?")
    MsgStop("Abertura da tabela "+cLockId+" em uso -- "+;
           "tente novamente mais tarde.")
    Return .F. 
  Endif
Enddo

If !TCCanOpen(cFile)
  // Se o arquivo nao existe no banco, cria
  DBCreate(cFile,aStru,"TOPCONN")
Endif

// O Arquivo já existe, vamos comparar as estruturas
USE (cFile) ALIAS (cFile) SHARED NEW VIA "TOPCONN"
IF NetErr()
  MsgSTop("Falha ao abrir a tabela "+cFile+" em modo compartilhado. "+;
          "Tente novamente mais tarde.")
  Return .F.
Endif
// Obtêm a estrutura do banco de dados 
aDbStru := DBStruct()
USE

If len(aDbStru) != len(aStru)
  // O tamanho das estruturas mudou ?
  // Vamos alterar a estrutura da tabela
  // Informamos a estrutura atual, e a estrutura esperada
  If !TCAlter(cFile,aDbStru,aStru)
    MsgSTop(tcsqlerror(),"Falha ao alterar a estrutura da tabela "+cFile)
    Return .F. 
  Endif
  MsgInfo("Estrutura do arquivo "+cFile+" atualizada.")
Endif

// Se esta tabela deve ter indice unico
// Cria caso nao exista 
If !empty(cIdxUnq)
  If !TCCanOpen(cFile,cFile+'_UNQ')
    // Se o Indice único da tabela nao existe, cria
    USE (cFile) ALIAS (cFile) EXCLUSIVE NEW VIA "TOPCONN"
    IF NetErr()
      MsgSTop("Falha ao abrir a tabela "+cFile+" em modo EXCLUSIVO. "+;
              "Tente novamente mais tarde.")
      Return .F.
    Endif
    nRet := TCUnique(cFile,cIdxUnq)
    If nRet < 0
      MsgSTop(tcsqlerror(),;
              "Falha ao criar índice único ["+cIdxUnq+"] "+;
              "para a tabela ["+cFile+"]")
      Return .F.
    Endif
    USE
  EndIf
Endif

// Cria os indices que nao existem para a tabela \
// a Partir do Array de indices informado
For nI := 1 to nIndex
  // Determina o Nome do Indice
  cIndex := cFile+cValToChar(nIndex)
  // Pega a expressão de indexação
  cIdxExpr := aIndex[nI]
  If !TCCanOpen(cFile,cIndex)
    // Se o Indice nao existe, cria 
    USE (cFile) ALIAS (cFile) EXCLUSIVE NEW VIA "TOPCONN"
    IF NetErr()
      MsgSTop("Falha ao abrir a tabela "+cFile+" em modo EXCLUSIVO. "+;
              "Tente novamente mais tarde.")
      Return .F.
    Endif
    INDEX ON &cIdxExpr TO (cIndex)
    USE
  Endif
EndIf

// Abra o arquivo em modo compartilhado
USE (cFile) ALIAS (cFile) SHARED NEW VIA "TOPCONN"

If NetErr()
  MsgSTop("Falha ao abrir a tabela "+cFile+" em modo compartilhado. "+;
          "Tente novamente mais tarde.")
  Return .F.
Endif

// Abre os indices, seleciona o primeiro
// e posiciona o arquivo no topo

For nI := 1 to nIndex
  cIndex := cFile+cValToChar(nIndex)
  DbSetIndex(cIndex)
Next

DbSetOrder(1)
DbGoTop()

// Solta o MUTEX
GlbNmUnlock(cLockId)

Return .T.

Reparem que tudo o que estava pré-definido em código (ou “CHUMBADO”) agora é recebido por parâmetro. cFile recebe o nome do arquivo, aStru recebe o array com a estrutura da tabela no formato AdvPL, cIdxUnq quando informado deve conter os campos a serem usados para a criação de índice único no SGDB separados por vírgula, e aIndex deve ser um Array de Strings, onde cada elemento corresponde a expressão de indexação para cada índice que deve ser criado para esta tabela.

Vamos ver agora como ficaria a nova função OpenAgenda(), usando o método OpenTable() da classe DBDriver?

STATIC Function OpenAgenda(oDbDrv)
Local cFile := "AGENDA"
Local aStru := {}
Local cIdxUnq 
Local aIndex := {}
Local lOk

// Define a estrutura do arquivo 
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})
aadd(aStru,{"FONE1" ,"C",20,0})
aadd(aStru,{"FONE2" ,"C",20,0})
aadd(aStru,{"EMAIL" ,"C",40,0})
aadd(aStru,{"IMAGE" ,"M",10,0})

// Índice único pelo campo ID 
cIdxUnq := "ID"

// Define a estrutura de índices
aadd(aIndex,"ID") // Indice 01 por ID 
aadd(aIndex,"NOME") // Indice 02 por NOME

// Pede para o Driver de Dados abrir a tabela 
lOk := oDbDrv:OpenTable(cFile,aStru,cIdxUnq,aIndex)

Return lOk

Reparem que eu passei tudo como parâmetro para o Driver. Sim, eu também passei a receber como parâmetro o Driver de Dados da aplicação como parâmetro. Mas meu código, com mais de 100 linhas, caiu para 32 linhas ou menos. Ao fazer a mesma coisa com a função OpenUsers(), economizamos mais um montão de linhas e reaproveitamos o código de criação e abertura de tabelas e índices.

STATIC Function OpenUsers(oDbDrv)
Local cFile := "USUARIOS"
Local aStru := {}
Local cIdxUnq 
Local aIndex := {}
Local lOk

// Cria o array com os campos do arquivo 
aadd(aStru,{"IDUSR" ,"C",06,0})
aadd(aStru,{"LOGIN" ,"C",50,0})
aadd(aStru,{"SENHA" ,"C",32,0})

// Índice único pelo campo LOGIN
cIdxUnq := "LOGIN"

// Definição de índices
aadd(aIndex,"IDUSR") // Indice 01 por ID de Usuario 
aadd(aIndex,"LOGIN") // Indice 02 por Login

// Pede para o Driver de Dados abrir a tabela 
lOk := oDbDrv:OpenTable(cFile,aStru,cIdxUnq,aIndex)

Return lOk

Não ficou bem mais simples? O código apenas preencheu as definições necessárias, e a responsabilidade de fazer a mágica é do Driver. De qualquer modo, a simplicidade dos argumentos informados parte de algumas premissas e comportamentos da versão atual deste fonte:

  1. O Método OpenTable() vai criar os índices usando o nome da tabela informado como parâmetro, seguido por um número, iniciando em “1”, para os índices informados, na ordem em que foram informados.
  2. O índice único segue a nomenclatura padrão do DBACCESS — Nome da tabela mais o sufixo “_UNQ”.
  3. A análise realizada na abertura da tabela comparando a diferença entre a estrutura da tabela definida pelo código e a estrutura atual do banco de dados — na versão atual — apenas verifica se o tamanho delas está diferente, não levando em conta a possibilidade de haver alteração de propriedades de campos existentes.
  4. A verificação dos índices limita-se a criar índices inexistentes. Caso seja necessária uma alteração de alguma expressão de indexação, ainda não há verificação para isso na aplicação, devendo o índice ser removido do SGDB para ser recriado pela aplicação.

E, por mais bonito que tenha ficado o fonte desta classe, ele ainda têm uma característica indesejável: Ele ainda faz comunicação direta com a interface, através de funções como MsgInfo(), MsgStop() e afins. Mas não criemos pânico, logo logo vamos refatorar este código também…

Conclusão

Acho que o reaproveitamento do código atual vai precisar de mais uma etapa, antes de partirmos para a inclusão de um novo usuário. Porém, podemos fazer um AJUSTE PONTUAL na rotina, criando um usuário “Inicial”, caso você deseje usar e testar o mecanismo de LOGIN. Para isso, altere a função ChkUser() para ela fazer a inserção do ADMIN com senha em branco caso a tabela esteja fazia, e a partir de então sempre solicitar o login, veja a parte do fonte alterado abaixo:

// Vai para o topo do arquivo 
DbSelectarea("USUARIOS")
DBGoTOP()

If EOF()
   // Se nao tem usuarios , cria um "ADMIN", com senha em branco 
   DBAppend()   
   USUARIOS->IDUSR := '000000'
   USUARIOS->LOGIN := "ADMIN"
   USUARIOS->SENHA := MD5("",HEX_DIGEST)
   DBRUnlock()
Endif

// Faz a validação do usuário 
lOk := DoLogin(oDlg)

No próximo post, vamos continuar refatorando alguns pontos da Agenda para reaproveitar mais código, assim ao invés de COPIAR E COLAR — e replicar código DESNECESSARIAMENTE — usamos a refatoração para reaproveitar uma parte das funcionalidades já escritas, que serão comuns para novos componentes e tabelas da aplicação.

Desejo a todos um bom aproveitamento desse material, e TERABYTES DE SUCESSO 😀

 

Anúncios

2 comentários sobre “CRUD em AdvPL – Parte 15

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s