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

 

 

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