Abstração de Acesso a Dados e Orientação a Objetos – Parte 02

Introdução

No post anterior (Abstração de Acesso a Dados e Orientação a Objetos) vimos o conceito de abstração e uma ideia de implementar uma classe superior — ou superclasse — que eliminaria várias duplicidades entre as classes ZDBFFILE e ZMEMFILE. Bem, mãos a obra.

Classe ZISAMFILE

Tudo o que é comum e exatamente igual na implementação de ambas as classes de acesso a DBF em disco e em memória são parte de uma lógica de acesso e comportamento ISAM. Ao criar a classe ZISAMFILE, ela passa a ter as propriedades e métodos comuns a ambas implementações, que são removidas das respectivas implementações e colocadas nela.

A classe ZISAMFILE não tem construtor explícito, ela não têm um “New”. Mas não precisa, pois ela não foi feita para ser instanciada diretamente. Ela deve ser a classe superior a ser herdada pelas classes ZMEMFILE e ZDBFFILE, da seguinte forma:

// Ao invés de 
CLASS ZDBFFILE FROM LONGNAMECLASS
CLASS ZMEMFILE FROM LONGNAMECLASS

// Agora temos
CLASS ZISAMFILE FROM LONGNAMECLASS
CLASS ZDBFFILE FROM ZISAMFILE
CLASS ZMEMFILE FROM ZISAMFILE

Métodos reimplementados

Existem alguns métodos comuns implementados tanto na classe filha como na classe pai. Ao implementar na classe filha um método da classe pai, você pode ou não chamar o método da classe pai de dentro da classe filha, quando o objetivo do método não é substituir a implementação da classe pai, mas sim COMPLEMENTÁ-LA.

Por exemplo, cada uma das classes (ZDBFFILE e ZMEMFILE) possui propriedades específicas, declaradas em sua definição. E, a classe pai ( ZISAMFILE) também tem as suas propriedades, comuns a todas as heranças. Na implementação original, o método de uso interno da classe chamado _InitVars() foi feito para justamente inicializar estas propriedades, e ele agora também foi implementado na classe ZISAMFILE.

A forma correta e elegante de se fazer isso é: Cada método _InitVars() da sua classe inicializa as propriedades da sua classe. E, as classes que herdam a ZISAMFILE -- no caso ZMEMFILE e ZDBFFILE -- antes de mais nada chamam o método _InitVars() da classe superior (ZISAMFILE). Sendo assim, o método _InitVars da classe ZMEMFILE ficou assim:

METHOD _InitVars() CLASS ZMEMFILE 

// Inicialização das propriedades da classe pai
_Super:_InitVars()

// Inicializa demais propriedades da ZMEMFILE
::aFileData   := {}
::lOpened     := .F. 
::lExclusive  := .F. 
::lCanWrite   := .T. 
::dLastUpd    := ctod("")
::aGetRecord  := {}
::aPutRecord  := {}
::lUpdPend    := .F. 
::lSetDeleted := .F. 
::nRecno      := 0

Return

Como eu disse, ainda existem propriedades em duplicidade implementadas nas classes ZMEMFILE e ZDBFFILE, elas serão remanejadas em outro momento. Mas sabe o que é o mais lindo de tudo isso?

  • Os programas de teste que usavam as classes continuam funcionando perfeitamente, pois todos eles acessam as funcionalidades das classes através de métodos, até mesmo as propriedades são retornadas por métodos — recurso também chamado de “Getters and Setters” — torne as propriedades privadas da classe, e encapsule qualquer mudança de estado das propriedades em métodos Set<Propriedade>(), e as consultas por métodos Get<Propriedade>()
  • A classe ZISAMFILE ficou com 700 linhas. Isto significa que cada fonte das classes ZMEMFILE e ZDBFFILE agora tem cada um 700 linhas a menos, eliminando a duplicidade de código, e implementando as funcionalidades na classe pai. 
  • Até mesmo um método que era

Outras mudanças

Aproveitando o momento de refatoração, a classe de índices em memória deixou de se chamar ZDBFMEMINDEX e passou a ser ZMEMINDEX — afinal ela é usada pelos métodos e propriedades de controle da implementação da ZISAMFILE. Outra alteração interessante era o processamento de uma expressão AdvPL, onde era necessário trocar a ocorrência de campos na expressão pelo o:FieldGet() do campo. Isto era feito exatamente da mesma forma tanto na classe de índice quanto nas classes de ZDBFFILE e ZMEMFILE para aplicar filtros.

Agora, existe um método chamado _BuildFieldExpr(), que recebe uma string com uma expressão AdvPL qualquer que use campos da tabela — onde todos os campos na expressão devem ser colocados com letras maiúsculas — e retorna uma string com o texto do Codeblock com a expressão resultante. Agora, quem precisa desta funcionalidade chama o método  _BuildFieldExpr() da classe  ZISAMFILE, e com a expressão resultante, criar o Codeblock dinâmico com macro-execução e usar conforme a necessidade.

GITHUB

Conforme o projeto vai sendo alterado e os fontes refatorados, novos recursos arquivos vão sendo acrescentados no GITHUB, a versão mais atual de todos os fontes envolvidos está lá. Pode ser necessário remover alguns fontes do projeto e recompilar os programadas para dar tudo certo. Em breve os fontes das implementações de arquivo e implementações comuns vão fazer parte de um novo projeto — Chamado “ZLIB”.

Conclusão

Eu deixo a conclusão desse post e da implementação para vocês. Espero que este exemplo sirva não somente pela sua funcionalidade, mas como um modelo de boas práticas de desenvolvimento.

Desejo a todos novamente TERABYTES DE SUCESSO 😀

 

 

 

 

 

Arquivos em Memória – Classe ZMEMFILE

Introdução

Nos posts anteriores, acompanhamos a criação de uma classe de acesso a dados ISAM — chamada de ZDBFTABLE, renomeada para ZDBFFILE — , feita para leitura e manutenção de arquivos no formato DBF em AdvPL, sem dependência de nenhum Driver. Agora, tomando esta classe como base da implementação, nasceu a classe ZMEMFILE.

Classe ZMEMFILE

O lindo da orientação a objetos é o reaproveitamento de código. Como eu não comecei a implementação com uma classe abstrata, e não pretendia criar uma agora, a classe ZMEMFILE nasceu de um “Clone” da classe ZDBFFILE. A diferença é que, ao invés de eu endereçar um handler de arquivo em disco para ler e gravar dados, eu criei na classe uma propriedade chamada ::aFileData, que é um array multi-dimensional com as colunas da tabela, e uma coluna interna a mais — para indicar se o registro foi marcado para deleção ou não.

O meu “RECNO” passa a ser o próprio elemento do array. Cada inserção de novo registro é feita no final do array, e a deleção apenas habilita um flag na ultima coluna do array. Os demais mecanismos são os mesmos, o registro atual lido em memória é uma cópia do original em um array separado, a atualização de valores idem, e os dados da tabela estão associados ao objeto da tabela, visível apenas pelo processo atual, enquanto o objeto não foi destruído ou a tabela for fechada.

Todos os demais métodos da classe que não acessavam fisicamente o arquivo, simplesmente não foram alterados. Estes métodos são os candidatos para uma futura refatoração, criando uma classe superior com estes métodos, fazendo as classes ZDBFFILE e ZMEMFILE herdarem esta classe base, e removendo as duplicidades desnecessárias da implementação.

Aproveitamento da classe ZMEMINDEX

Como a classe que cria o índice em memória não acessa diretamente os dados de nenhum arquivo, mas faz as leituras, criação de índice e demais operações usando os métodos da classe ZDBFFILE, eu praticamente não precisei mexer em nenhuma linha da ZMEMINDEX para usá-la com a ZMEMFILE. 

Isso me deixou simplesmente radiante. A implementação de filtro foi clonada, a implementação de bisca indexada e manutenção de índices também clonada, uma vez que eu reimplementei os métodos que efetivamente acessavam o disco para acessar um array da própria classe, o fonte já funcionava.

Fonte de Testes

Vamos ver o fonte abaixo, chamado de CriaMem.PRW

#include "protheus.ch"

USER Function CriaMEM()
Local cFile := 'memfile.dbf'
Local oDbf
Local aStru := {}

SET DATE BRITISH
SET CENTURY ON 
SET EPOCH TO 1950

// Define a estrutura 
aadd(aStru,{"CPOC","C",10,0})
aadd(aStru,{"CPOM","M",10,0})

// Cria o objeto da tabela 
oDbf := ZMEMFILE():New(cFile)

// Cria a tabela em si 
oDbf:Create(aStru)

// Abre em modo de escrita 
If !oDbf:Open(.T.,.T.)
	UserException( oDBF:GetErrorStr() )
Endif

// Insere um registro
oDBF:Insert()
oDBF:Fieldput(1,'Laranja')
oDBF:Fieldput(2,'0000000001')
oDBF:Update()

// Insere mais um registro 
oDBF:Insert()
oDBF:Fieldput(1,'Banana')
oDBF:Fieldput(2,'0000000002')
oDBF:Update()

// Insere um terceiro registro 
oDBF:Insert()
oDBF:Fieldput(1,'Abacate')
oDBF:Fieldput(2,'0000000003')
oDBF:Update()

conout("Mostrando 3 registros")
oDBF:GoTop()
While !oDBF:Eof()
	// Mostra o registro atual
	ShowRecord(oDBF)
	oDBF:Skip()
Enddo

// Agora cria um indice
conout("Criando indice por CPOC")
oDBF:CreateIndex("CPOC")

// Mostra os dados da tabela novamente, agora ordenados
// A criacao de um indice já o torna ativo, e reposiciona 
// a tabela no primeiro registro 
While !oDBF:Eof()
	// Mostra o registro atual
	ShowRecord(oDBF)
	oDBF:Skip()
Enddo

// Fecha a tabela
// -- Os dados sao eliminados da memoria 
oDBF:Close()

// Limpa / Libera o Objeto
FreeObj(oDBF)

Return

// Função feita para mostrar o conteudo do registro atual 
STATIC Function ShowRecord(oDBF)
Local nI
Local aStru := oDBF:GetStruct()
conout(replicate('-' ,79))
conout("RECNO() ...... " + cValToChar(oDBF:Recno()) +" | " + "DELETED() .... "+cValToChar(oDBF:Deleted()) )
conout("BOF() ........ " + cValToChar(oDBF:Bof())   +" | " + "EOF() ........ "+cValToChar(oDBF:Eof()) )
conout("Index ........ " + "("+cValToChar(oDBF:IndexOrd())+") "+oDBF:IndexKey())
conout("")
For nI := 1 to len(aStru)
	conout("Fld #"+padr(cValToChar(nI),3)+" | "+ ;
	  aStru[nI][1]+" ("+aStru[nI][2]+") => ["+cValToChar(oDBF:Fieldget(nI))+"]" )
Next
conout("")
Return

Em poucas palavras, o fonte de exemplo cria a tabela, insere três registros, mostra no console, cria um índice pelo campo “CPOC”, e mostra novamente os dados ordenados.

Sabe o que você precisa fazer para isso rodar com um DBF ? Apenas troque a seguinte linha:

// Troque a linha abaixo 
oDbf := ZMEMFILE():New(cFile)

// Para esta aqui:
oDbf := ZDBFFILE():New(cFile)

E está feito. Salve o fonte, compile novamente e teste. O arquivo chamado “memfile.dfb” será criado no disco. Recomendo baixar a pasta inteira do GirHub (Blog), e criar um projeto com os seguintes fontes:

zDBFFile.prw
zDbfMemIndex.prw
zDBTFile.prw
zFPTFile.prw
zLibDateTime.prw
zLibDec2Hex.prw
zLibNBin.prw
zMEMFile.prw

Na próxima refatoração, a classe ZDBFMEMINDEX será renomeada para ZMEMINDEX — afinal ela não serve apenas para DBF 😀

Por que não usar um Array “direto” ?

A-há !! A pergunta de meio milhão de dinheiros …risos… Sim, armazenar um resultado temporário de qualquer coisa em um array em AdvPL pode ser feito de forma direta, sem dúvida. Se o seu uso de dados em Array é simples, o array é pequeno e as buscas são ridículas, e a manutenção de elementos praticamente inexistente, então você não precisa usar uma classe para emular um arquivo em memória. Escreva seu fonte criando um array direto e seja feliz.

Agora, se você vai ter um número maior de elementos, precisa de uma busca ou navegação ordenada — HASH faz busca, mas não ordena — e já tem um código que usa o arquivo em disco, como um container temporário, mas ele não vai acabar com a memória do Protheus Server se ele for usado em memória … pronto, você tem uma implementação virtual de arquivo para pintar e bordar.

Evolução natural de funcionalidades

Conforme a implementação vai sendo utilizada, naturalmente sentimos a necessidade de algumas melhorias, alguns métodos ou mesmo novas classes facilitadoras. Por exemplo, copiar dados de um arquivo em disco para a memória. É simples montar, basta instanciar o arquivo em disco em um objeto, pegar a estrutura, criar o objeto em memória, fazer um loop lendo  o primeiro arquivo e inserindo os dados no segundo arquivo.

Porém, se cada vez que você precisar fazer isso, você replicar a implementação, dá-lhe código duplicado. Nada como implementar um método CreateFrom(). Você cria o objeto do arquivo no disco, onde estão os dados, cria o objeto do arquivo em memória, então chama o método CreateFrom() do arquivo em memória, passando como parâmetro o objeto do arquivo em disco. O método CreateFrom() ainda não existe, mas é o próximo da fila… risos…

Internamente este método já vai fazer o que tem que ser feito. Quer algo mais elegante que isso? Seu arquivo temporário em memória não precisa de todos os dados do arquivo em disco, porém apenas os registros que atendam uma determinada condição. Basta setar um filtro no arquivo de origem, o método CreateFrom() vai copiar apenas os registros logicamente visíveis, que atendam a condição de filtro.

Conclusão

Por hora, a conclusão óbvia é que, embora inicialmente pareça um pouco mais difícil mudar seu mind-set para pensar Orientado a Objetos, adotar este paradigma da forma consciente e adequada só têm benefícios.

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

Referências

PROGRAMAÇÃO ORIENTADA A OBJETOS. In: WIKIPÉDIA, a enciclopédia livre. Flórida: Wikimedia Foundation, 2018. Disponível em: <https://pt.wikipedia.org/w/index.php?title=Programa%C3%A7%C3%A3o_orientada_a_objetos&oldid=53496356>. Acesso em: 2 nov. 2018.

Abstração de Acesso a Dados e Orientação a Objetos

Introdução

Nos últimos posts, a ideia de criar um componente em AdvPL para leitura de arquivos DBF, acessando diretamente o arquivo no disco sem uso de Driver, acabou virando uma implementação completa de manutenção de DBF. Vamos ver agora conceitualmente o que isto representa em termos de desenvolvimento.

Abstração

“O uso da abstração na computação pode ser exemplificada da seguinte forma: Imagine que um determinado processamento é realizado em vários pontos de um sistema, da mesma forma ou de forma idêntica. Ao invés de repetirmos o trecho de código responsável por este processamento, o abstraímos na forma de um procedimento ou função, e apenas fazemos uma chamada à tal procedimento, onde quer que necessitemos e por quantas vezes se fizer necessário.” — Fonte: Wikipedia

Quando programamos com orientação a objetos, uma forma muito elegante de reaproveitamento de código, é usar uma herança de uma classe abstrata — ela não contém exatamente a implementação, mas serve de guia para uma ou mais classes herdarem a classe abstrata e implementarem as operações declaradas usando os métodos como “guia”.

“Uma classe abstrata é desenvolvida para representar entidades e conceitos abstratos. A classe abstrata é sempre uma superclasse que não possui instâncias. Ela define um modelo (template) para uma funcionalidade e fornece uma implementação incompleta – a parte genérica dessa funcionalidade – que é compartilhada por um grupo de classes derivadas. Cada uma das classes derivadas completa a funcionalidade da classe abstrata adicionando um comportamento específico.” — Fonte: Wikipedia

Implementação em AdvPL

Usando AdvPL — mesmo sem usar a implementação do TL++ — podemos criar uma classe base, com métodos e propriedades, e implementar uma classe filha, com os métodos para o seu propósito. Por exemplo, a classe ZDBFTABLE — criada para permitir manutenção e leitura de dados em um arquivo DBF — ela têm os métodos definidos para abertura, fechamento, inserção , atualização, ordenação e afins, mas ela trabalha com um arquivo em formato DBF.

Eu começo a escrever códigos que consomem esta classe e seus métodos. Então, amanhã eu quero armazenar os dados em um outro formato de tabela, pode ser c-Tree, algum banco relacional, um banco em memória, etc. Devido ao dinamismo das classes em AdvPL, eu consigo implementar uma classe nova com os mesmos métodos, e reaproveitar o mesmo código.

Por exemplo, um programa que cria um arquivo e insere um registro, usando a classe ZDBFTABLE, poderia ser escrito assim:

User Function  NewDBF()
Local cFile := '\meuarquivo.dbf'
Local oDbf
Local aStru := {}

aadd(aStru,{"CPOC","C",10,0})
aadd(aStru,{"CPOD","D",8,0})

oDbf := ZDBFTABLE():New(cFile)

If !oDbf:Create(aStru)
   UserException( oDBF:GetErrorStr() )
Endif

If !oDbf:Open(.T.,.T.)
   UserException( oDBF:GetErrorStr() )
Endif

oDBF:Insert()
oDBF:Fieldput(1,'0000000001')
oDBF:Fieldput(2,date())
oDBF:Update()

oDBF:Close()
FreeObj(oDBF)
Return

Se amanhã eu criar uma nova classe para armazenar estes dados em outro formato de arquivo, ou mesmo em memória, ou em um servidor remoto, não importa. Basta eu criar uma classe com os mesmos métodos, e implementá-la. A única coisa que vai mudar inicialmente seria a criação do objeto. Você usaria o construtor  da nova classe, que deve ter publicadas os mesmos métodos disponíveis para uso, com os mesmos nomes e emulando o mesmo comportamento. Ao invés de usar o construtor da ZDBFTABLE, você poderia usar uma outra classe, para criar por exemplo o arquivo em memória, sem alterar praticamente nada do código.

Implementação xBase / ISAM (Clipper / FoxPRO / Harbour / AdvPL)

A implementação de acesso a dados usando xBASE / ISAM usadas nas linguagens acima foram feitas em cima de funções, sem orientação a objeto, porém com o mesmo propósito de reaproveitamento de código. No momento de CRIAR ou ABRIR uma tabela, você informa qual é o DRIVER (ou RDD) que deve ser usado. A partir de então, todas as operações de consulta e manutenção dos dados é feita sobre um ALIAS aberto daquela tabela.

A primeira versão de arquivo de índice usada no Clipper 5.x usava o formato de índice NTX — baseado em uma árvore B+Tree modificada, permitia apenas uma expressão de índice por arquivo. Este driver era chamado DBFNTX, basta especificar na criação e abertura da tabela — e fazer o link com a library correspondente ao gerar o executável. Posteriormente foi criado um novo driver, que usava índices no formato CDX (Compound Index File), que permitia mais de uma expressão de índice no mesmo arquivo, nomeadas internamente como “TAGs”, e internamente usava um algoritmo de compactação.

Para usar o novo driver, era necessário apenas alterar no programa o nome do driver informado na criação e abertura de tabelas, especificar a library DBFCDX no linker, e alguns pequenos ajustes no código devido a mudanças de características intrínsecas ao novo formato — você não criava um novo arquivo para cada índice, mas sum uma nova TAG com a expressão de índice dentro de um arquivo indexador.

Foram implementados vários Drivers no Protheus, seguindo ao máximo a mesma premissa de manter o comportamento esperado de suas funções, de modo que você possa usar um novo Driver de armazenamento e recuperação de dados com o menor impacto possível no seu código.

Abstraindo

O conceito de abstração pode ser aplicado em diversos componentes do sistema. Neste momento só me vêm a cabeça exemplos de uso em componentes de tecnologia. Por exemplo, uma classe de comunicação qualquer ( TCP, HTTP, IMAP, POP, SMTP … ) pode usar uma conexão TCP/IP ou uma conexão segura (SSL/TLS). Porém, ambas conexões possuem os métodos de criar uma nova conexão, encerrar uma conexão, enviar dados, receber dados, verificar status da conexão, espera com time-out. Pronto, crie uma classe abstrata de conexão com estes métodos, crie quantas heranças forem necessárias, e implemente.

O conceito de persistência de qualquer informação parte da premissa que eu possa salvá-la e recuperá-la posteriormente. Eu posso salvar o estado de uma classe em um formato que a própria classe seja capaz de fazer um Save() e um Load(). Uma vez feito isso, eu posso criar uma abstração de persistência de contexto, e implementar o Save e o Load em um arquivo no disco, uma tabela de um banco de dados, ou mesmo um e-mail. E posso ainda ir mais longe, implementar uma abstração de formato de conteúdo. Uma vez que meu armazenamento seja baseado por exemplo em chave e valor, eu posso salvar e ler este conteúdo por exemplo em JSON, XML, Binary ASCII, etc.

Conclusão

Algumas das premissas da orientação envolvem a criação de código reutilizável. Recursos de herança, polimorfismo e associação permitem separar a lógica da sua implementação. Implementar estes conceitos onde cabe e como manda o figurino fatalmente vai fazer parte do diferencial da solução proposta!

Desejo a todos TERABYTES DE SUCESSO !!!! 😀 

Referências

Classe ZDBFTABLE – Índice em Memória

Introdução

Nos posts anteriores, começamos a ver a implementação da classe ZDBFTABLE, uma forma de leitura de arquivos no formato DBF, sem o uso de Driver ou RDD, lendo o arquivo direto no disco usando as funções de baixo nível de arquivo do AdvPL. Agora, vamos ver como criar um índice eficiente em memória

Classe ZDBFMEMINDEX

Criar um índice para um arquivo DBF é uma forma não apenas de permitir uma navegação ordenada nos registros, mas também uma forma de realizar uma busca muito rápida pelo índice. Como a ideia original da classe ZDBFTABLE é realizar leitura de dados de um DBF, seria deveras interessante ter uma forma de criar um ou mais índices para a tabela.

Devido a natureza das tabelas DBF, um índice com chave simples ou composta (um ou mais campos) pode usar qualquer expressão ou função do AdvPL, onde o resultado da expressão é gravado em um índice no disco. A implementação da classe de índice em memória implementa esta funcionalidade, mas sem persistir o índice em disco. Ele é criado e mantido na memória por um array ordenado.

CLASS ZDBFMEMINDEX

   DATA oDBF		// Objeto ZDBFTABLE relacionado ao índice 
   DATA cIndexExpr      // Expressão AdvPL original do índice
   DATA bIndexBlock     // CodeBlock para montar uma linha de dados do índice
   DATA aIndexData      // Array com os dados do índice ordenado pela chave 
   DATA aRecnoData      // Array com os dados do índice ordenado pelo RECNO 
   DATA nCurrentRow     // Numero da linha atual do índice 
   DATA lSetResync      // Flag de resincronismo pendente da posição do índice
   DATA lVerbose

   METHOD NEW(oDBF)     // Cria o objeto do índice
   METHOD CREATEINDEX(cIndexExpr) // Cria o índice baseado na chave fornecida 
   METHOD CLOSE()       // Fecha o índice e limpa os dados da memória 

   METHOD GetFirstRec() // Retorna o RECNO do primeiro registro do índice
   METHOD GetPrevRec()  // Retorna o RECNO do Registro anterior do índice
   METHOD GetNextRec()  // Retorna o RECNO do próximo registro do índice
   METHOD GetLastRec()  // Retorna o RECNO do último registro do índice 
   
   METHOD GetIndexExpr()  // Rertorna a expressão de indexação 
   METHOD GetIndexValue() // Retorna o valor da chave de indice do registro atual 
   METHOD GetIndexRecno() // REtorna o numero do RECNO da posição do índice atual 
   METHOD IndexSeek()     // Realiza uma busca ordenada por um valor informado 
   METHOD RecordSeek()    // REaliza uma busca no indice pelo RECNO 
   METHOD UpdateKey()     // Atualiza uma chave de indice ( em implementação ) 
   
   METHOD _BuildIndexBlock(cIndexExpr) // Cria o codeblock da chave de indice 
   METHOD _CheckSync()    // Verifica a necessidade de sincronizar o indice 
   METHOD SetResync()     // Seta flag de resincronismo pendente
   METHOD SetVerbose()    // Seta modo verbose com echo em console ( em implementação 
   
ENDCLASS

Trocando em miúdos

Quando navegamos pela ordem física de registros de uma tabela DBF — ou seja, pelo número do RECNO — basicamente somamos uma unidade no marcador de registro para acessar o próximo, e subtraímos uma unidade para acessar o registro anterior, o primeiro registro da tabela é o número um, e o último registro é o maior registro da tabela (LastRec).

Quando optamos por realizar a navegação por um índice, basicamente criamos uma lista ordenada do tipo chave-valor, onde cada registro gera uma chave baseado na expressão de indexação, e o índice é responsável por guardar a chave e o número do RECNO correspondente desta chave. Uma vez que a lista seja ordenada, cada chave aponta para um registro físico (RECNO).

Logo, ao navegar usando um índice, eu preciso ter um marcador de controle (::nCurrentRow) para informar em qual linha do índice eu estou posicionado, e quando a tabela receber uma instrução de SKIP(), para ir para o próximo registro, se eu estou usando um índice, eu pergunto para o índice qual é o número do próximo registro que eu preciso posicionar.

Método CreateIndex()

METHOD CREATEINDEX( cIndexExpr ) CLASS ZDBFMEMINDEX

// Guarda a expressão original do indice
::cIndexExpr := cIndexExpr

// Monta o CodeBlock para a montagem da linha de dados
// com a chave de indice
::_BuildIndexBlock( cIndexExpr )

// Agora varre a tabela montando o o set de dados para criar o índice
::aIndexData := {}
::aRecnoData := {}

// Coloca a tabela em ordem de regisrtros para a criação do indice
::oDBF:DbSetORder(0)
::oDBF:DBClearFilter()
::oDBF:DbGoTop()

While !::oDBF:Eof()
	// Array de dados 
	// [1] Chave do indice
	// [2] RECNO
	// [3] Numero do elemento do array aIndexData que contém este RECNO
	aadd( ::aIndexData , { Eval( ::bIndexBlock , ::oDBF ) , ::oDBF:Recno() , NIL } )
	::oDBF:DbSkip()
Enddo

// Sorteia pela chave de indice, usando o RECNO como criterio de desempate
// Duas chaves iguais, prevalesce a ordem fisica ( o menor recno vem primeiro )
aSort( ::aIndexData ,,, { |x,y| ( x[1] < y[1] ) .OR. ( x[1] == y[1] .AND. x[2] < y[2] ) } )

// Guardando a posicao do array ordenado pelos dados na terceira coluna do array 
aEval( ::aIndexData , {| x,y| x[3] := y })

// Agora, eu preciso tambem de um indice ordenado por RECNO 
// Porem fazendo referencia a todos os elementos do array, mudandi apenas a ordenação 

// Para fazer esta magica, cria um novo array, referenciando 
// todos os elementos do array principal , então ordena
// este array pelo RECNO
::aRecnoData := Array(len(::aIndexData))
aEval(::aIndexData , {|x,y| ::aRecnoData[y] := x })

// Ordena o array aRecnoData em ordem crescente de RECNO
aSort( ::aRecnoData ,,, { |x,y| x[2] < y[2] } )

Return .T.

Aqui é que a mágica acontece. Neste fonte eu uso e abuso de Arrays, aEval e ASort. Primeiro, eu preciso criar um array que tenha pelo menos o valor da expressão de ordenação de cada registro, e o número do registro correspondente. Estes dados serão armazenados no array ::aIndexData. Por razões que eu vou explicar mais abaixo, este array possui uma coluna adicional, que contém qual é o número deste elemento no próprio array — parece não fazer sentido, mas vai fazer.

Primeiro, a expressão AdvPL recebida para criar o índice pode retornar uma String, um número ou uma data. Podemos criar uma chave composta usando dois campos por exemplo, mas como a expressão final do valor chave a ser ordenado é única, a expressão de índice normalmente concatena estes campos — sendo eles do tipo  “C” Caractere, por exemplo. Se existem campos de tipos diferentes a serem usados para gerar um índice, normalmente convertemos tudo para string: Campos numéricos são convertidos para Caractere usando a função STR(), passando como parâmetro o valor do campo, o tamanho do campo e a quantidade de decimais. Para campos data, convertemos para string usando DTOS() — que converte a data no formado AAAAMMDD (ou YYYYMMDD), própria para ordenação em string.

::_BuildIndexBlock( cIndexExpr )

Uma vez montado o CodeBlock para gerar um valor de chave de ordenação por registro, usamos o objeto  oDBF para limpar qualquer filtro da tabela, usar a ordem física dos registros, posicionar no primeiro, e criar o array por enquanto apenas com o valor de cada chave, e o RECNO correspondente — primeira e segunda colunas do Array — varrendo a tabela do primeiro ao último registro.

While !::oDBF:Eof()
	// Array de dados 
	// [1] Chave do indice
	// [2] RECNO
	// [3] Numero do elemento do array aIndexData que contém este RECNO
	aadd( ::aIndexData , { Eval( ::bIndexBlock , ::oDBF ) , ::oDBF:Recno() , NIL } )
	::oDBF:DbSkip()
Enddo

Feito isso, a ordenação deste array deve levar em conta que, caso hajam expressões de índice de mesmo valor, o campo RECNO passa a ser um fator de ordenação. Chaves repetidas do índice são ordenadas pelo valor do RECNO, no menor para o maior.

// Sorteia pela chave de indice, usando o RECNO como criterio de desempate
// Duas chaves iguais, prevalesce a ordem fisica ( o menor recno vem primeiro )
aSort( ::aIndexData ,,, { |x,y| ( x[1] < y[1] ) .OR. ( x[1] == y[1] .AND. x[2] < y[2] ) } )

Agora que o array ::aIndexData está ordenado, vamos preencher a terceira coluna com a posição de cada elemento no array ordenado. Para isso,  preenchemos a terceira coluna deste Array usando:

// Guardando a posicao do array ordenado pelos dados na terceira coluna do array 
aEval( ::aIndexData , {| x,y| x[3] := y })

A função AEVAL() passa dois parâmetros para o Codeblock, o primeiro é o elemento do Array em processamento, e o segundo parâmetro é o número da posição deste elemento no Array. Deste modo, eu uso esta AEval() para atribuir o número da própria posição neste array.

O pulo do gato vêm agora. Embora eu possa estar usando um índice para navegar na tabela,  uma vez que faça um “Goto” em um determinado registro, eu preciso posicionar nele. E ao fazer isso, eu preciso pesquisar no array de índice qual é a posição atual do índice para o registro que eu posicionei. Afinal, uma vez posicionado em um registro pelo seu número, ao perguntar qual é o próximo registro desta ordem para o índice, como ele vai saber onde ele está ?!

::aRecnoData := Array(len(::aIndexData))
aEval( ::aIndexData , {|x,y| ::aRecnoData[y] := x })
aSort( ::aRecnoData ,,, { |x,y| x[2] < y[2] } )

Para isso, o array chamado de ::aRecnoData, foi criado. Ele serve para referenciar todos os elementos do primeiro array, porém ordenando os elementos pelo RECNO. Deste modo eu não duplico os dados na memória, apenas faço referência aos elementos do primeiro array, em outra ordenação.

Assim, quando eu faço um posicionamento direto na tabela usando Goto() — sem usar o índice — eu ligo um flag de re-sincronismo pendente, e quando eu voltar a navegar pelo índice, caso exista um re-sincronismo pendente, eu uso o array ordenado pelo RECNO para realizar uma busca binária, para localizar qual é a linha do array ordenado pela chave de índice (::aIndexData) que corresponde ao RECNO atualmente posicionado na tabela — e justamente esta informação está na terceira coluna dos elementos de ambos os arrays.  Ao recuperar a posição do índice atual, eu consigo ir para o anterior ou próximo elementos na ordem do índice.

Método interno _BuildIndexBlock

De modo similar — praticamente uma cópia — ao método de filtro de tabela, o método que cria o Codeblock para a geração do valor chave de ordenação a partir da expressão AdvPL informada, espera que qualquer campo da tabela referenciado no índice esteja em letras maiúsculas. Desse modo ele cria um Codeblock de ordenação que recebe o objeto da tabela em si, e retorna o valor da chave de ordenação baseado na expressão informada usando o conteúdo dos campos do registro atualmente posicionado.

METHOD _BuildIndexBlock(cIndexExpr) CLASS ZDBFMEMINDEX
Local aCampos := {}
Local cTemp
Local nI, nPos

// Cria lista de campos
aEval( ::oDBF:aStruct , {|x| aadd(aCampos , x[1]) } )

// Ordena pelos maiores campos primeiro
aSort( aCampos ,,, {|x,y| alltrim(len(x)) > alltrim(len(y)) } )

// Copia a expressao de índice
cTemp := cIndexExpr

// Troca os campos por o:Fieldget(nCpo)
// Exemplo : CAMPO1 + CAMPO2 será trocado para o:FieldGet(1) + o:FieldGet(2)

For nI := 1 to len(aCampos)
	cCampo := alltrim(aCampos[nI])
	nPos   := ::oDBF:Fieldpos(cCampo)
	cTemp  := StrTran( cTemp , cCampo,"o:FieldGet(" +cValToChar(nPos)+ ")")
Next

// Monta a string com o codeblock para indice
cTemp := "{|o| "+cTemp+"}"

// Monta efetivamente o codeblock de indice
::bIndexBlock := &(cTemp)

Return

Busca Binária – Métodos RecordSeek() e IndexSeek()

Uma busca binária somente pode ser realizada em cima de um array ordenado. A ideia e a implementação são simples: Você define um limite superior e inferior de busca, começando no primeiro e terminando no último elemento da lista, tira a média deste valor — calcula o “meio” entre estes dois pontos — e verifica se o valor procurado é igual, menor ou maior.

Se for igual, achou. Se for menor, redefine que o limite inferior para a busca é a posição do meio menos um , recalcula o meio novamente considerando os novos valores de topo e fim, e repete a busca. Caso o valor buscador for maior que o valor do meio da lista, redefine que o limite superior é o valor atual de meio mais um, recalcula o meio novamente considerando os novos valores de topo e fim, e repete a busca.

O desempenho desse tupo de busca é espantoso, pois a cada operação de comparação, metade da lista é desconsiderada. Deste modo, num pior cenário, em uma lista com 4 bilhões de registros, foram necessárias — no máximo — 32 comparações para encontrar um valor ou saber que ele não está lá. Vamos ver por exemplo o método para a busca no array ordenado pelo RECNO:

METHOD RecordSeek(nRecno) CLASS ZDBFMEMINDEX
Local lFound := .F. 
Local nTop := 1 
Local nBottom := Len(::aRecnoData)
Local nMiddle 

If nBottom > 0

	If nRecno < ::aRecnoData[nTop][2]
		// Chave de busca é menor que a primeira chave do indice
		Return 0
	Endif

	If nRecno > ::aRecnoData[nBottom][2]
		// Chave de busca é maior que a última chave
		Return 0
	Endif

	While nBottom >= nTop

		// Procura o meio dos dados ordenados
		nMiddle := Int( ( nTop + nBottom ) / 2 )

		If ::aIndexData[nMiddle][2] == nRecno
			// Achou 
			lFound := .T. 
			EXIT
		ElseIf nRecno < ::aRecnoData[nMiddle][2]
			// RECNO menor, desconsidera daqui pra baixo 
			nBottom := nMiddle-1
		ElseIf nRecno > ::aRecnoData[nMiddle][2]
			// RECNO maior, desconsidera daqui pra cima
			nTop := nMiddle+1
		Endif
	
	Enddo

	If lFound
		// Retorna a posição do array de dados 
		// ordenados (aIndexData) que contem este RECNO 
		Return ::aRecnoData[nMiddle][3]
	Endif
	
Endif

Return 0

O método IndexSeek() também usa o mesmo princípio, porém é feito em cima do array ordenado pela chave de índice (::aIndexData, coluna 1). Normalmente as buscas demoram menos de dois milésimos de segundo, em testes realizados com uma lista de 109 mil elementos. Também pudera, no máximo em 16 comparações este algoritmo localiza a informação. Se a informação procurada estiver fora do range de dados, em apenas uma ou duas comparações o algoritmo já determina que a informação com aquela chave não existe e retorna .F. para a operação de busca.

Observações

A classe de índice em memória não é usada diretamente, na verdade ela é usada internamente pelos métodos de criação de índice, ordenação e busca ordenada publicados na classe ZDBFTABLE. Através dela criamos, usamos e fechamos um ou mais índices.

Conclusão

Era uma prova de conceito de leitura, está virando quase um Driver completo. Já estou trabalhando na criação de tabelas e inclusão/alteração de dados, mas isso exige outros truques com os índices já criados, para permitir manutenção dos índices nestas operações. Assim que tiver mais novidades, sai mais um post.

Novamente agradeço a todos pela audiência, e lhes desejo TERABYTES de SUCESSO 😀

 

 

 

Classe ZDBFTABLE – Implementação de Filtro AdvPL

Introdução

Já que a classe ZDBFTABLE permite a navegação em uma tabela DBF, vamos ver como seria implementar um filtro ? E ver como ele funciona por dentro.

Filtros de dados em xBASE / Clipper

Quando se trabalha diretamente com o arquivo DBF diretamente, sem ter um SGDB ou um programa intermediário de gerenciamento, a implementação de um filtro é uma forma de criar uma expressão usando campos da tabela e operadores lógicos, que retorne .T. (Verdadeiro) caso o registro deva ser considerado nas operações de navegação da tabela e posicionamento de registros. Para isso são usados os comandos SET FILTER ou a função DBSetFilter().

A expressão de filtro em AdvPL informada é traduzida para um Codeblock, que passa a ser executado quando você por exemplo faz um DBSkip(). Ao ler o próximo registro a ser posicionado, caso exista uma condição de filtro especificada, internamente o driver executa o Codeblock, e caso o mesmo retorne .F., o driver avança mais um registro e repete a operação até encontrar o primeiro registro que atenda a condição de filtro ou a tabela chegue ao final (EOF).

Método DBSetFilter

METHOD DBSetFilter( cFilter ) CLASS ZDBFTABLE
Local aCampos := {}
Local cTemp
Local nI, nPos

// Cria lista de campos 
aEval( ::aStruct , {|x| aadd(aCampos , x[1]) } )

// Ordena pelos maiores campos primeiro 
aSort( aCampos ,,, {|x,y| alltrim(len(x)) > alltrim(len(y)) } )

// Copia a expressao filtro
cTemp := cFilter

// Troca os campos por o:Fieldget(nCpo)
// Exemplo : CAMPO > 123 será trocado para o:FieldGet(1) > 123

For nI := 1 to len(aCampos)
	cCampo := alltrim(aCampos[nI])
	nPos := ::Fieldpos(cCampo)
	cTemp := Strtran( cTemp , cCampo,"o:FieldGet(" +cValToChar(nPos)+ ")")
Next

// Monta a string com o codeblock para filtro 
cTemp := "{|o| "+cTemp+"}"

// Monta efetivamente o codeblock 
::bFilter := &(cTemp)

Return

A implementação foi feita de forma similar ao comportamento original do DBF com Clipper ou mesmo com AdvPL. Porém, como eu não tenho um parser léxico para destrinchar a expressão de filtro, eu fiz da forma mais simples: Primeiro, qualquer campo da tabela atual usado na expressão de filtro deve ser escrito em letras maiúsculas.

Então, eu crio a lista de campos da tabela baseado na estrutura, ordeno a lista pelos campos com o maior nome, para depois trocar cada ocorrência de campo por uma chamada do método Fieldget() do campo, já passando o número do campo da estrutura como parâmetro. Os campos tem que ser ordenados pelo maior nome primeiro, pois eu posso ter por exemplo dois campos com o mesmo começo no nome : XIS e XIS2. Eu devo começar a troca sempre pelos campos de maior nome.

E, finalmente, eu crio um Codeblock com a expressão resultante, recebendo em “o” a instância do objeto da tabela, e nos métodos de navegação — DbSkip(), DBGoTop() e DBGoBottom() — eu passo a verificar se o registro deve ser considerado ou não, fazendo um Eval() do filtro informando o “self” — minha própria instância — como parâmetro.

Por exemplo, imagine que eu sete a condição de filtro “!Empty(X3_CBOX)”, vamos ver como ficaria o Codeblock resultante:

cFilter --> "!Empty(X3_CBOX)"
cTemp   --> "{|o| !Empty(o:FieldGet(28))}"

Método DBSkip()

METHOD DbSkip( nQtd ) CLASS ZDBFTABLE 
Local lForward := .T. 

If nQtd  == NIL
	nQtd := 1
ElseIF nQtd < 0  	lForward := .F.  Endif // Quantidade de registros para mover o ponteiro // Se for negativa, remove o sinal  nQtd := abs(nQtd) While nQtd > 0 
	If lForward
		IF ::_SkipNext()
			nQtd--
		Else
			// Bateu EOF()
			::_ClearRecord()
			Return
		Endif
	Else
		IF ::_SkipPrev()
			nQtd--
		Else
			// Bateu BOF()
			Return
		Endif
	Endif
Enddo

// Traz o registro atual para a memória
::_ReadRecord()

Return

O método DBSkip em si ficou simples. Ele recebe como parâmetro o número de registros que devem ser movimentados. Porém, em caso de filtro, eu preciso contar que as movimentações foram feitas apenas com registros válidos. Desse modo, quem faz o controle de navegação e filtro são os métodos internos _SkipNext e _SkipPrev.

Método interno _SkipNext

METHOD _SkipNext() CLASS ZDBFTABLE
Local nNextRecno

While (!::lEOF)

	// Parte do registro atual , soma 1 
	nNextRecno := ::Recno() + 1 

	// Passou do final de arquivo, esquece
	If nNextRecno > ::nLastRec
		::lEOF := .T.
		::_ClearRecord()
		Return .F. 
	Endif

	// ----------------------------------------
	// Atualiza o numero do registro atual 
	::nRecno := nNextRecno

	// Traz o registro atual para a memória
	::_ReadRecord()

	// Passou na checagem de filtro ? Tudo certo 
	// Senao , continua lendo ate achar um registro valido 
	If ::_CheckFilter()
		Return .T. 
	Endif

Enddo

Return .F.

Como a navegação (por enquanto) não usa índice, o próximo registro sempre será o atual mais um. Então, a função incrementa o registro, verifica se não atingiu EOF, lê o registro para a memória, e então verifica se o registro não está filtrado, usando o método interno _CheckFilter(). — Calma que a gente já chega nele .. é o próximo.

Método interno _CheckFilter

METHOD _CheckFilter() CLASS ZDBFTABLE
Local lOk := .T. 
If ::bFilter != NIL 
	lOk := Eval(::bFilter , self )	
Endif
Return lOk

O filtro setado foi guardado na propriedade ::bFilter. Caso ela não seja NIL, existe um filtro setado. Ao fazer o Eval() da condição de filtro, se o registro não atende a condição, o método _SkipNext() continua lendo os próximos registros até encontrar um registro válido.

Desempenho de filtro

Como a verificação de registro válido no filtro somente é realizada após o registro ser lido, durante o posicionamento. quando mais simples a expressão de filtro, mais rápida será a validação da visibilidade do registro. E, no momento de navegar pela tabela, quanto maior for a tabela, e quanto menos registros atenderem a condição de filtro, mais registros serão lidos na navegação para encontrar um registro válido.

Por exemplo, imagine uma tabela com 100 mil registros, onde você vai realizar um determinado processamento filtrado. Você seta o filtro, faz um DBGoTop() e um While !Eof() — DBSkip(). Mesmo que na sua tabela existam por exemplo apenas mil registros que atentam a condição de filtro, os 100 mil registros serão lidos e verificados se eles fazem parte do filtro ou não. Logo, cuidado com filtros e tabelas grandes.

Comportamento do filtro ISAM

Ao setar um filtro, o registro atualmente posicionado não é alterado, mesmo que ele não faça parte do filtro. Logo, normalmente reposicionamos a tabela no primeiro registro válido contemplando o filtro usando a função DBGoTop() — ou no nosso caso da classe ZDBFTABLE, o método DBGoTop().

Todas as funções de navegação ISAM ( DbGoTop(), DBGoBottom(), DBSkip() — e inclusive a DBSeek() — respeitam a condição de filtro. A única função que não respeita nada é a DBGoto(). Uma vez que eu posicione diretamente em um registro pelo seu número de RECNO, ele será lido e posicionado, mesmo que esteja deletado ou que não faça parte da seleção do filtro. Uma vez posicionado neste registro, um próximo DbSkip() vai posicionar no registro imediatamente posterior, considerando o filtro setado.

Para limpar o filtro, usamos o método DbClearFilter(), que também não mexe no posicionamento da tabela, apenas coloca NIL na propriedade onde têm o Clodeblock do filtro.

Conclusão

O fonte atualizado sempre está no GITHUB. Entre um post e outro sempre têm coisas novas. O uso é livre, fique a vontade !!!

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

 

Classe ZDBFTABLE – Parte 01 – Introdução

Introdução

No post anterior (Lendo DBF em AdvPL — Sem DRIVER ou RDD), foi apresentado um exemplo de uso da classe ZDBFTABLE, criada em AdvPL para ler arquivos DBF usando diretamente as funções de baixo nível de arquivo do AdvPL. Com isso, eu consigo ler um DBF em qualquer Build, 32 ou 64 Bits, Windows ou Linux, sem nenhum Driver ou RDD.

Agora, vamos ver por dentro como a classe lê o arquivo do disco, e como ela emula o comportamento ISAM de leitura de registros e navegação de dados através de todos os métodos da classe.

Comportamento atual

Neste post, a classe está na sua primeira revisão, ela acabou de ganhar um método para permitir setar um filtro. O comportamento atual de leitura de dados possui as seguintes características e restrições:

  • Não suporta nenhum índice em nenhum formato.
  • Não respeita o filtro SET DELETED da RDD padrão. Todos os registros são lidos, inclusive deletados.
  • Suporta campos memo em formato DBT e FPT
  • Passa a suportar filtro com qualquer expressão válida em AdvPL, desde que os campos estejam escritos em letras maiúsculas.
  • Suporta navegação para frente — ::DbSkip(n)  — ou para trás — ::DbSkip(-n)
  • Suporta posicionamento direto pelo RECNO — DBGoto(nRecno)

Declaração da Classe

A orientação a objetos do AdvPL utilizada ainda não é o novo TL++, a classe foi escrita usando a orientação básica a Objetos do AdvPL, para ser possível utilizá-la inclusive em builds bem mais antigas do Protheus Server. Qualquer build de Protheus Server lançada nos últimos 15 anos roda esse código. Para separar o que deve e o que não deve ser feito, foram aditadas as seguintes regras:

  1. Não acesse propriedades da classe. Elas são de uso interno e podem ser alteradas. Use sempre os métodos publicados.
  2. Assuma que qualquer método com o nome iniciado com “_” não deve ser chamado pelos fontes que consomem a classe. Eles são de uso interno da classe apenas.

Logo, vamos para a declaração ou prototipagem da classe ZDBFTALBE:

CLASS ZDBFTABLE FROM LONGNAMECLASS

  DATA cDataFile		// Nome do arquivo de dados
  DATA cMemoFile		// Nome do arquivo memo (DBT/FPT) 

  DATA cDBFType			// Identificador hexadecimal do tipo do DBF 
  DATA dLastUpd			// Data registrada dentro do arquivo como ultimo UPDATE 
  DATA nLastRec			// Ultimo registro do arquivo - Total de registros
  DATA nRecLength		// Tamanho de cada registro 
  DATA nDataPos 		// Offset de inicio dos dados 
  DATA lHasMemo			// Tabela possui campo MEMO ?
  DATA cMemoType		// Identificador (extensao) do tipo do campo MEMO
  DATA nFileSize 		// Tamanho total do arquivo em bytes 
  DATA nFldCount		// Quantidade de campos do arquivo 
  DATA aRecord			// Array com todas as colunas do registro atual 
  DATA lDeleted			// Indicador de registro corrente deletado (marcado para deleção ) 
  DATA nRecno			// Número do registro (RECNO) atualmnete posicionado 
  DATA bFilter                  // Codeblock de filtro 

  DATA lBOF			// Flag de inicio de arquivo 
  DATA lEOF			// Flag de final de arquivo 
  
  DATA nHData			// Handler do arquivo de dados
  DATA nHMemo			// Handler do arquivo de MEMO
  DATA aStruct		   	// Array com a estrutura do DBF 
    	
  DATA nLastError		// Ultimo erro ocorrido 
  DATA cLastError		// Descrição do último erro 

  // ========================= Metodos de uso público da classe

  METHOD NEW(cFile)		// Construtor 
  METHOD OPEN()			// Abertura da tabela 
  METHOD CLOSE()		// Fecha a tabela 

  METHOD GetDBType()		// REtorna identificador hexadecimal do tipo da tabela 
  METHOD GetDBTypeStr() 	// Retorna string identificando o tipo da tabela 

  METHOD Lastrec()		// Retorna o total de registros / numero do ultimo registro da tabela 
  METHOD RecCount()		// Retorna o total de registros / numero do ultimo registro da tabela 
  METHOD DbStruct()		// Retorna CLONE da estrutura de dados da tabela 
  METHOD DBGoTo(nRec)		// Posiciona em um registro informado. 
  METHOD DBGoTop()		// Posiciona no RECNO 1 da tabela 
  METHOD DBGoBottom()   	// Posiciona em LASTREC da tabela 
  METHOD DbSkip( nQtd )     // Navega para frente ou para tráz uma quantidade de registros 
  METHOD FieldGet( nPos )   // Recupera o conteudo da coluna informada do registro atual 
  METHOD FieldName( nPos )  // Recupera o nome da coluna informada 
  METHOD FieldPos( cField ) // Retorna a posicao de um campo na estrutura da tabela ( ID da Coluna )
  METHOD BOF()	            // Retorna .T. caso tenha se tentado navegar antes do primeiro registro 
  METHOD EOF()		    // Retorna .T, caso o final de arquivo tenha sido atingido 
  METHOD Recno()            // Retorna o numero do registro (RECNO) posicionado 
  METHOD Deleted()	    // REtorna .T. caso o registro atual esteja DELETADO ( Marcado para deleção ) 
  METHOD SetFilter()        // Permite setar um filtro para os dados 
  METHOD ClearFilter()      // Limpa o filtro 

  METHOD Header() 	    // Retorna tamanho em Bytes do Header da Tabela
  METHOD RecSize()	    // Retorna o tamanho de um registro da tabela 
  METHOD LUpdate()	    // Retorna a data interna do arquivo que registra o ultimo update 
 
  METHOD GetError() 	    // Retorna o Codigo e Descricao por referencia do ultimo erro 
  METHOD GetErrorCode()     // Retorna apenas oCodigo do ultimo erro ocorrido
  METHOD GetErrorStr()	    // Retorna apenas a descrição do último erro ocorrido

  // ========================= Metodos de uso interno da classe

  METHOD _ResetError()	    // Limpa a ultima ocorrencia de erro 
  METHOD _SetError()        // Seta uma nova ocorrencia de erro 
  METHOD _ResetVars() 	    // Inicializa propriedades do Objeto, no construtor e no CLOSE
  METHOD _ReadHeader()	    // Lê o Header do arquivo  de dados
  METHOD _ReadStruct()	    // Lê a estrutura do arquivo de dados 
  METHOD _ReadRecord()	    // Le um registro do arquivo de dados
  METHOD _ClearRecord()	    // Limpa o registro da memoria (EOF por exemplo) 
  METHOD _ReadMemo()        // Recupera um conteudo de campo memo por OFFSET
  METHOD _CheckFilter()     // Verifica se o registro atual está contemplado no filtro 
  METHOD _SkipNext()	    // Le o proximo registro da tabela considerando filtro
  METHOD _SkipPrev()        // Le o registro anterior da tabela considerando filtro 

ENDCLASS

Por dentro do DBF

Eu já sabia por dentro como era um DBF — conceitualmente, header, estrutura e dados — mas não tinha entrado no “detalhe” do byte-a-byte dentro do arquivo. Em linhas gerais, os primeiros 32 bytes são o header do arquivo, com detalhes sobre o tipo ou versão de DBF, tamanho  do registro, início dos dados. Logo depois do header vêm a estrutura de campos, cada campo ocupa 32 bytes com nome, tipo, tamanho, decimais e outras propriedades para outras versões.

Terminada a estrutura, vêm a área de dados. Cada registro no DBF ocupa um valor fixo de bytes, equivalente a soma dos tamanhos dos campos na estrutura, mais um byte — o primeiro byte de dados do campo é usado para a marca de registro deletado, que pode ser reaproveitado pela aplicação ou eliminado fisicamente em uma operação de PACK.

Inclusive, uma curiosidade. Um campo MEMO possui um tamanho de 10 bytes na estrutura, Estes 10 bytes são usados para gravar no DBF uma referência ao bloco inicial para a leitura do campo memo em um arquivo auxiliar (DBT ou FPT). No DBF somente são gravados campos de tamanho fixo. Uma String de 40 bytes, mesmo que você preencha ela apenas com 10, é gravada com espaços em branco a direita.

Para maiores detalhes do DBF por dentro, consulte os links de referência deste post, foram o meu “guia” para abrir e ler as tabelas.

Por dentro da classe ZDBFTABLE

Vamos destrinchar a classe método a método, com os porquês de cada propriedade. O princípio de armazenamento e leitura de um driver xBASE ou ISAM é: Em um arquivo aberto, você sempre está posicionado em um registro, ou está em EOF (End Of File — Final de Arquivo). Você pode reposicionar o registro para um registro anterior ou próximo. Sem usar um índice, você navega pela ordem física de inserção dos registros. Cada registro ou Recno() é a posição ordinal deste registro no arquivo, iniciada em 1.

As propriedades da classe são usadas para manter o contexto de abertura e posicionamento do arquivo, para ser possível a navegação e reposicionamento dos registros. Cada registro posicionado é lido no posicionamento. Quando ao campo MEMO, apenas é lida a referência para localizar o dado no arquivo de dados de memo (DBF ou FPT), a leitura real do conteúdo somente será feita caso a aplicação AdvPL fizer um Fieldget() do campo MEMO.

Método NEW() — Construtor

METHOD NEW(cFile) CLASS ZDBFTABLE
::_ResetVars() 
::cDataFile := lower(cFile)
Return self

Recebe como parâmetro o path mais nome do arquivo a ser utilizado. Não é realizada a abertura da tabela no construtor. O método interno _ResetVars() inicializa as propriedades do contexto do arquivo na classe, e foi feita separadamente para ser chamada também no fechamento da tabela pelo método Close().

Método OPEN

Responsável pela abertura efetiva da tabela, identificação de Header e Estrutura, e em caso de sucesso, já posiciona e lê o primeiro registro da tabela — caso a mesma tenha dados. As etapas intermediárias de abertura são feitas por classes internas, para não deixar o corpo da classe de abertura muito grande, e procurar seguir as premissas de boas práticas de programação — Cada classe tem uma finalidade, cada método também têm uma finalidade.

METHOD OPEN() CLASS ZDBFTABLE 

::_ResetError()

If ::nHData <> -1
	::_SetError(-1,"File Already Open")
	Return .F.
Endif

// Abre o arquivo de dados
::nHData := Fopen(::cDataFile)

If ::nHData == -1
	::_SetError(-2,"Open Error - File ["+::cDataFile+"] - FERROR "+cValToChar(Ferror()))
	Return .F.
Endif

// Pega o tamanho do arquivo 
::nFileSize := fSeek(::nHData,0,2)

// Lê o Header do arquivo 
If !::_ReadHEader()
	FClose(::nHData)
	::nHData := -1
	Return .F. 
Endif

If ::lHasMemo

	// Se o header informa que a tabela possui campo MEMO 
	// Determina o nome do arquivo MEMO 

	::cMemoFile := substr(::cDataFile,1,at(".dbf",::cDataFile)-1)
	::cMemoFile += ::cMemoType
	
	If !file(::cMemoFile)
		::_SetError(-3,"Memo file ["+::cMemoFile+"] NOT FOUND.")
		::Close()
		Return .F. 
	Endif

	// Abre o arquivo MEMO 
	::nHMemo := FOpen(::cMemoFile)
    
	If ::nHMemo == -1
		::_SetError(-4,"Open Error - File ["+::cMemoFile+"] - FERROR "+cValToChar(Ferror()))
		::Close()
		Return .F. 
	Endif
	
Endif

If !::_ReadStruct()
	// Em caso de falha na leitura da estrutura 
	FClose(::nHData)
	::nHData := -1
	IF ::nHMemo <> -1
		FClose(::nHMemo)
		::nHMemo := -1
	Endif
	Return .F.
Endif

// Cria o array de campos do registro atual 
::aRecord := Array(::nFldCount)

// Vai para o topo do arquivo 
// e Lê o primeiro registro físico 
::DBGoTop()

Return .T.

Cada registro lido é armazenado na memória na propriedade aRecord. O método Fieldget() busca dos dados de cada coluna do registro atual pelo numero da coluna usando como índice do array. Caso a tabela não tenha dados, um método interno chamado _ClearRecord() preenche o array com os campos vazios, baseado nos tipos de dados da estrutura. Um campo caractere será preenchido com espaços em branco, um campo data terá uma data vazia, um tipo numérico será zero, um booleano será .F. (Falso) e um campo memo será uma string vazia. Em caso de erro de abertura, o método OPEN retorna .F., e você pode recuperar informações adicionais sobre o erro usando o método GetErrorStr() por exemplo.

Leitura e reposicionamento de registros

Podemos usar o método DbSkip() para avançar para registros para trás ( em direção ao início do arquivo) ou para frente (em direção ao final do arquivo). Caso cada um destes limites seja atingido (BOF ou EOF), os métodos de mesmo nome vão retornar .T. . Em caso de BOF(), é mantido o posicionamento no primeiro registro da tabela. Caso você atingiu EOF(), o registro atual é limpo da memória. Você também pode ir para o primeiro ou para o último registro da tabela, usando os métodos DbGoTop() e DBGoBottom() — respecitvamente — e ainda pode posicionar diretamente eu um registro a partir do RECNO — Caso um registro quer não exista fisicamente tente ser endereçado, você vai para EOF().

Desempenho

A operação mais executada na navegação entre registros é a leitura do conteúdo do registro atual e a recuperação dos dados. Por isso escolhi usar uma propriedade do tipo Array para armazenar na memória apenas a linha atual posicionada na tabela (::aRecord), e a recuperação dos dados é feita pelo número da posição da coluna na estrutura da tabela. Logo, se você tem um processamento em LOOP e vai precisar recuperar várias vezes a mesma coluna em cada loop, é muito mais performático primeiro descobrir a posição dos campos desejados e armazenar em memória, para então acessar cada campo pelo número da coluna.

Uma leitura de registro, feita pelo método interno _ReadRecord() calcula o offset de arquivo do RECNO atual, reposiciona o ponteiro para aquele offset, e lê em uma string o conteúdo de uma linha ou um registro na memória. Então, em um loop posterior, partindo do array com a estrutura da tabela, realizamos as conversões necessárias para alimentar o array de campos (::aRecord).

Método interno _ReadRecord

METHOD _ReadRecord() CLASS ZDBFTABLE 
Local cTipo , nTam , cValue
Local nBuffPos := 2 , nI
Local cRecord := '' , nOffset

// ----------------------------------------
// Calcula o offset do registro atual baseado no RECNO

nOffset := ::nDataPos 
nOffset += (::nRecno * ::nRecLength)
nOffset -= ::nRecLength

// Posiciona o arquivo de dados no offset do registro 
FSeek(::nHData , nOffset )

// Lê o registro do offset atual 
FRead(::nHData , @cRecord , ::nRecLength )

// Primeiro byte = Flag de deletato
// Pode ser " " (espaço)    registro ativo 
//          "*" (asterisco) registro deletado 
   
::lDeleted := ( left(cRecord,1) = '*' )

// Agora lê os demais campos e coloca no ::aRecord

For nI := 1 to ::nFldCount

	cTipo := ::aStruct[nI][2]
	nTam  := ::aStruct[nI][3]
	cValue := substr(cRecord,nBuffPos,nTam)

	If cTipo == 'C'
		::aRecord[nI] := cValue
		nBuffPos += nTam
	ElseIf cTipo == 'N'
		::aRecord[nI] := val(cValue)
		nBuffPos += nTam
	ElseIf cTipo == 'D'
		// Por hora le como caractere
		::aRecord[nI] := cValue
		nBuffPos += nTam
	ElseIf cTipo == 'L'
		::aRecord[nI] := ( cValue=='T' )
		nBuffPos += nTam
	ElseIf cTipo == 'M'
		// Recupera o Offset do campo no DBT/FPT
		::aRecord[nI] := val(cValue)
		nBuffPos += nTam
	Endif
  
Next

// Reseta flags de BOF e EOF 
::lBOF := .F. 
::lEOF := .F. 

Return .T.

Como podemos ver no fonte, o tratamento das partes que compõe o buffer do registro atual em memória — armazenada em cRecord — é tratada extraindo o dado em sequência, desmontando o buffer baseado no tipo e tamanho de cada campo. O Flag de registro deletado é o primeiro byte do campo, e seu resultado é armazenado na propriedade ::lDeleted, que pode ser consultada pela aplicação através do método Deleted()

O que são estes montes de “::” ?

Para quem ainda não está familiarizado com orientação a objetos em AdvPL, a sequência de “::” é um #translate, ou “açúcar sintático” — ela é traduzida de “::” para “self:”, e apenas facilita a leitura do código. Dentro de um método da classe, você faz referência a uma variável que é propriedade da classe usando “self:variavel”. Para o código ficar mais limpo, você usa “::variavel”. Para quem programa em C++, o “self” do AdvPL é o equivalente ao “this“.

Conclusão

Por hora, conclusão mesmo vai ser quando essa classe conseguir usar um índice. Aí sim ela será promovida a classe Chuck Norris Certified !!! Por hora, estou preparando para o próximo post um suporte a filtro de registros — De forma similar a um DbSetFilter(), usando uma expressão AdvPL para determinar a visibilidade lógica na navegação de registros.

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

Referências

 

 

Lendo DBF em AdvPL — Sem DRIVER ou RDD

Introdução

Com o passar do tempo, a utilização de tabelas em formato ISAM, como o DBF e o c-TREE, estão sendo substituídas por uso de tabelas temporárias no Banco de Dados principal das aplicações AdvPL. E, por questões tecnológicas, a utilização do Protheus Server 64 Bits não suporta mais abertura e manutenção de arquivos no formato DBF. Neste post eu compartilho uma forma de ler conteúdo de arquivos DBF em AdvPL, independente do sistema operacional (Windows / Linux ) e independente da Build do Application Server (32 ou 64 bits).

Driver DBF em AdvPL

Em breve será lançada uma build nova do Protheus 12 (ERP Microsiga) chamada “Lobo Guará”, para Windows 64 Bits e Linux 64, não devendo mais haver Build 32 bits, e o novo servidor não terá mais suporte ao uso de arquivos DBF.

Muitas vezes, existem integrações com sistemas legados que ainda usam arquivos no formato DBF. Eu mesmo tenho várias modelagens de sistemas antigos — coisas de mais de 20 anos — todas criadas com o driver DBFNTX do Clipper 5.x, campos memo em formato DBT. Após estudar um pouco a estrutura interna do arquivo DBF, e dos campos MEMO (DBT e FPT), resolvi construir uma classe em AdvPL que seja capaz de abrir e ler os dados de um arquivo DBF usando as funções de baixo nível  de arquivo do AdvPL (FOpen, FRead,FSeek).

Classe ZDBFTABLE

A implementação da classe foi feita partindo da premissa que todas as operações de leitura da tabela sejam feitas através de métodos, que foram publicados com o mesmo nome das funções de baixo nível de arquivo de dados do AdvPL — por exemplo DbSkip(), DBGotop(), FieldGet().

A primeira versão operacional suporta leitura de arquivos nos formatos dBASE III/Foxpro/Foxbase+/Clipper, com campos MEMO em arquivo DBT e/ou FPT. Não há suporte a índices ou filtros, todos os registros são lidos — inclusive os registros deletados logicamente (ou seja, marcados para deleção). Os dados são lidos na ordem física de registro — ou Recno(). Os métodos de navegação ISAM e de leitura implementados são:

  • DBGoBottom() — Posiciona no último registro da tabela.
  • DBGoTop() — Posiciona no primeiro registro da tabela
  • DBSkip(nRecs)  — Avança ou volta um ou mas registros
  • DBGoto(nRecno) — Posiciona direto em um registro pelo número.

O construtor do objeto — método New() — recebe como parâmetro o nome do arquivo DBF no disco a ser aberto. O arquivo é aberto apenas para leitura usando o método Open(), que já posiciona a tabela no primeiro registro, e fechado usando o método Close().  A recuperação de dados e de status dos registros é feita usando os métodos:

  • FieldGet(nCol) — Recupera o valor de uma coluna da linha atual
  • FieldPos(cCol) — Recupera o número da coluna a partir do nome
  • EOF() — Retorna .T. caso o ponteiro de registros não esteja apontando para um registro. Isto normalmente acontece quando a tabela está vazia ou quando houve a movimentação de ponteiro para próximo registro, e você já estava no último registro da tabela.
  • BOF() — Retorna .T. caso a ultima movimentação de registro tentou ler antes do primeiro registro da tabela
  • Recno() — Retorna o número do registro atual posicionado da tabela. Caso ela esteja em EOF(), é retornado o número do último registro mais um.
  • Deleted() — Retorna .T. caso o registro atualmente posicionado esteja deletado.

Outros métodos para recuperar propriedades da tabela são:

  • LastRec() e/ou RecCount() — Retornam o número do último registro da tabela. Como os registros no DBF são numerados a partir do número 1, ele também representa o número total de registros da tabela.
  • DBStruct() — Retorna um Array contendo a estrutura do arquivo DBF, com 4 colunas: Nome do campo, Tipo do Campo, Tamanho do campo e quantidade de decimais.
  • Header() — Retorna o tamanho em bytes do cabeçalho do arquivo DBF
  • RecSize() — Retorna o tamanho em bytes ocupado no arquivo DBF para uma linha de dados — Equivale a soma do tamanho de todas as colunas mais um (Coluna interna reservada para marcar um registro como deletado.)
  • LUpdate() — Retorna a data da última atualização de registro do arquivo DBF, gravada no Cabeçalho do arquivo.

Fonte de Exemplo

Sem maiores delongas, segue abaixo o fonte “LeUmDbf.prw”, com o exemplo de uso da classe ZDBFTable(). Ela abre uma tabela DBF, mostra informações sobre a tabela e a estrutura de dados, e o primeiro registro ( RECNO 1 ) da tabela no log de console.

#include  'protheus.ch'

User Function LeUmDBF()
Local oDBF
Local nCampos, aStru
Local cFile := '\system\sx3990.dbf'
Local nI

// Cria o objeto para leitura do arquivo 
oDBF := ZDBFTABLE():New(cFile)

// Faz a abertura. Em caso de falha, 
// aborta a aplicação obtendo os detalhes do erro
// usando o método GetErrorStr()
If !oDBF:Open()
	UserException( oDBF:GetErrorStr() )
Endif

// Mostra no log de console alguns dados sobre o arquivo aberto 
conout(replicate('=' ,79))
conout("Database File Name : " + cFile )
conout("Database File Type : " + oDBF:GetDBType()+ " => "+ oDBF:GetDBTypeStr())
conout("Last Update .......: " + dtoc(oDBF:LUpdate()) )
conout("Record Count ......: " + cValToChar(oDBF:RecCount()))
conout("Header Size .......: " + cValToChar(oDBF:Header()))
conout("Record Length......: " + cValToChar(oDBF:RecSize()))

// Recupera a estrutura da tabela 
aStru := oDBF:DbStruct()

// Verifica quantos campos tem a tabela 
nCampos := len(aStru)

// Mostra a estrutura de campos no console
conout("")
conout("--- Table Structure --- ")

For nI := 1 to nCampos
	
	conout("Field ["+aStru[nI][1]+"]"+;          // Nome
          " Type ["+aStru[nI][2]+ "]"+;          // Tipo (CNDLM)
          " Size ["+Str(aStru[nI][3],3)+"]"+;    // Tamanho
          " Dec ["+Str(aStru[nI][4],2)+"]")     // Decimais
	
Next

// Mostra o primeiro registro da tabela, e detalhes do registro
// Caso a tabela esteja vazia, BOF() e EOF() retornam .T. 

conout(replicate('-' ,79))
conout("RECNO() ...... "+cValToChar(oDBF:Recno()) )
conout("BOF() ........ "+cValToChar(oDBF:Bof()) )
conout("EOF() ........ "+cValToChar(oDBF:Eof()) )
conout("DELETED() .... "+cValToChar(oDBF:Deleted()) )
conout("")
For nI := 1 to len(aStru)
	conout(oDBF:FieldName(nI)+" => " +;
               "["+cValToChar(oDBF:Fieldget(nI))+"]" )
Next
conout("")

// Fecha a tabela 
oDBF:Close()

// Limpa / Libera o Objeto 
FreeObj(oDBF)

Return

Desempenho

Não é uma API em C/C++, mas o desempenho não ficou nada mal. Usando uma tabela de 44 colunas e 1660 bytes por registro (ou linha), o programa leu sequencialmente em média 5 mil linhas por segundo. Este desempenho pode ficar ate dez vezes mais lento, quando o arquivo a ser lido estiver sendo acessado por um compartilhamento de rede por exemplo — não têm almoço grátis.

Quer usar esta classe ?

Para usar esta classe, pegue o código fonte atualizado no GITHUB — arquivo “zDBFTable.prw” – – e fique a vontade. Apenas mantenha o cabeçalho do fonte com as informações da autoria do código. Simples assim 😀

Como ela acessa o arquivo diretamente no disco, ela funciona para qualquer Build do Protheus, 32 e 64 Bits, Windows e/ou Linux, e como as funções utilizadas são arrox-com-feijão, esse fonte pode ser usado em versões anteriores do Protheus Server, lançadas a pelo menos nos últimos 10 anos.

Conclusão

Para uma primeira versão de leitura, eu acho que já dá pra fazer bastante coisa. Ao usar a classe, use sempre o acesso pelos métodos, e não diretamente as propriedades, e também não use os métodos de uso interno — prefixados com “_”. Desta forma é possível eu continuar evoluindo esta classe sem impacto nos fontes que a utilizam. O fonte da classe já está no GITHUB, no próximo post nós vamos entrar dentro dos detalhes da implementação do DBF Driver em AdvPL!

Desejo a todos um ótimo uso da classe ZDBFTABLE, e TERABYTES DE SUCESSO !!!

Referências