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

Introdução

Continuando a mesma linha dos posts anteriores, vamos ver agora como exportar um arquivo de dados — das classes ZMEMFILE e/ou ZDBFFILE — para os formatos SDF , CSV e JSON 😀

Formato SDF

O formato SDF é um arquivo texto com linhas de tamanho fixo (SDF = System Data Format, fixed length ASCII text). Cada linha do arquivo é composta pelo conteúdo de um registro da tabela, onde cada campo é gravado sem separador ou delimitador, na ordem de colunas da estrutura da tabela, onde o formato de gravação de cada campo depende do tipo.

SDF Text File Format Specifications
     ------------------------------------------------------------------------
     File Element        Format
     ------------------------------------------------------------------------
     Character fields    Padded with trailing blanks
     Date fields         yyyymmdd
     Logical fields      T or F
     Memo fields         Ignored
     Numeric fields      Padded with leading blanks for zeros
     Field separator     None
     Record separator    Carriage return/linefeed
     End of file marker  1A hex or CHR(26)
     ------------------------------------------------------------------------

Campos Caractere são gravados com espaços a direita para preencher o tamanho do campo da estrutura, campos do tipo “D” data são gravados no formato ANSI (AAAAMMDD), uma data vazia é gravada com 8 espaços em branco, campos “L” Lógicos são gravados como “T” ou “F”, campos numéricos usam o tamanho especificado na estrutura com espaços a esquerda, e “.” ponto como separador decimal. A quebra de linha é composta pela sequência de dois bytes CRLF — chr(13) + chr(10) —  e ainda têm um último byte indicando EOF (Final de Arquivo). Campos “M” memo não são exportados.

Este formato é muito rápido de ser importado, pois uma vez que eu trabalho com os tamanhos dos campos da estrutura, eu não preciso fazer um “parser” de cada linha para verificar onde começa e onde termina cada campo e informação. Porém, o formato SDF não leva junto a estrutura da tabela, portanto uma importação somente será efetuada com sucesso caso o arquivo de destino já exista, e tenha a mesma estrutura do arquivo de origem — estrutura idêntica, tipos e tamanhos de campos, na mesma ordem.

Formato CSV

Este formato é muito comum em integrações entre sistemas, definido pela RFC-4180 (CSV = Comma Separated Values). Os campos caractere são gravados desconsiderando espaços à direita, e delimitados por aspas duplas. Caso exista uma aspa dupla dentro do conteúdo do campo a aspas é duplicada. As colunas são separadas por vírgula, valores numéricos são gravados sem espaços e usando o separador decimal “.” (ponto), valores booleanos são gravados como “true” ou “false”, campos do tipo “D” Data são gravados entre aspas no formato AAAAMMDD, data vazia é uma string vazia. Campos “M” memo também não são exportados. A primeira linha do CSV possui a lista de campos da tabela — apenas o nome do campo.

Um arquivo CSV tende a ser menor que um SDF, quando os campos do tipo Caractere possuem espaços a direita — que são desconsiderados ao gerar o CSV. Porém, com campos de tamanho variável, é necessário tratar linha a linha para verificar onde começa e termina cada informação a ser recuperada.

FORMATO JSON

Existem várias formas de se especificar uma tabela neste formato ( JSON = JavaScript Object Notation). A forma mais econômica e segura de exportar uma tabela de dados neste formato é representá-la como um objeto de duas propriedades: “header”, composta de um array multidimensional de N linhas e 4 colunas, representando a estrutura da tabela (Campo, Tipo, Tamanho e Decimais), e a propriedade “data”, composta de um array multi-dimensional de X linhas por N colunas, onde cada linha representa um registro da tabela, na forma de um array com os dados das colunas, na ordem de campos especificada no header. O formato de representação dos dados é praticamente o mesmo do CSV, exceto o campo “C” Caractere, que caso tenha aspas duplas em seu conteúdo, esta aspa é representada usando a sequencia de escape  \” (barra inversa e aspa dupla). Por hora, campos “M” Memo não são exportados. A diferença de tamanho dos mesmos dados exportados para CSV ou JSON é mínima.

Então, como eu faço isso?

Até agora tudo é lindo, então vamos ver como fazer a exportação, usando o método — ainda em desenvolvimento — chamado Export(). Primeiramente, partimos de um exemplo onde eu abri uma Query e criei um arquivo em memória com os registros retomados da Query, usando a classe ZMEMFILE. Então, eu chamo o método EXPORT() deste objeto, informando o formato a ser gerado e o arquivo em disco com o resultado a ser criado.

User Function TstExport()
Local cQuery
Local nH

nH := tclink("MSSQL/DBLIGHT","localhost",7890)
IF nH < 0
	MsgStop("TCLINK ERROR - "+cValToChar(nH))
	Return
Endif

// Abre uma Query no AdvPL 
cQuery := "SELECT CPF , NOME, VALOR from DOADORES WHERE VALOR > 2000 order by 3 DESC"
USE (tcGenQry(,,cQuery)) ALIAS QRY SHARED NEW VIA "TOPCONN"
TCSetField("QRY","VALOR","N",12,2)

// Cria um arquivo em memoria com a Query usando o ALIAS / WORKAREA
oMemory := ZMEMFILE():New('qrymemory')
oMemory:CreateFrom("QRY",.T.)

// Fecha a Query 
USE

// Exporta os registros para SDF 
oMemory:Export("SDF",'\temp\tstexport.sdf' )

// Exporta os registros para CSV
oMemory:Export("CSV",'\temp\tstexport.csv' )

// Exporta para JSON
oMemory:Export("JSON",'\temp\tstexport.json' )

// Fecha a tabela temporária em memória 
oMemory:Close()
FreeObj(oMemory)

Return

Método EXPORT()

E, para apreciação, vamos ver por dentro o método de exportação de dados. Ele ainda está em desenvolvimento, estou estudando como parametrizar algumas características específicas dos formatos suportados. Por hora, ele está assim:

METHOD Export( cFormat, cFileOut ) CLASS ZISAMFILE
Local nHOut
Local nPos
Local cBuffer := ''
Local lFirst := .T. 

// Primeiro, a tabela tem qye estar aberta
IF !::lOpened
	UserException("ZISAMFILE:EXPORT() Failed - Table not opened")
	Return .F.
Endif

cFormat := alltrim(Upper(cFormat))

If cFormat == "SDF" 
	
	// Formato SDF
	// Texto sem delimitador , Campos colocados na ordem da estrutura
	// CRLF como separador de linhas 
	// Campo MEMO não é exportado
	
	nHOut := fCreate(cFileOut)
	If nHOut == -1
		::_SetError(-12,"Output SDF File Create Error - FERROR "+cValToChar(Ferror()))
		Return .F.
	Endif
	
	::GoTop()
	
	While !::Eof()
		
		// Monta uma linha de dados
		cRow := ""
		
		For nPos := 1 TO ::nFldCount
			cTipo := ::aStruct[nPos][2]
			nTam  := ::aStruct[nPos][3]
			nDec  := ::aStruct[nPos][4]
			If cTipo = 'C'
				cRow += ::FieldGet(nPos)
			ElseIf cTipo = 'N'
				cRow += Str(::FieldGet(nPos),nTam,nDec)
			ElseIf cTipo = 'D'
				cRow += DTOS(::FieldGet(nPos))
			ElseIf cTipo = 'L'
				cRow += IIF(::FieldGet(nPos),'T','F')
			Endif
		Next
		
		cRow += CRLF
		cBuffer += cRow
		
		If len(cBuffer) > 32000
			// A cada 32 mil bytes grava em disco
			fWrite(nHOut,cBuffer)
			cBuffer := ''
		Endif
		
		::Skip()
		
	Enddo

	// Grava flag de EOF
	cBuffer += Chr(26)

	// Grava resto do buffer que falta 
	fWrite(nHOut,cBuffer)
	cBuffer := ''
	
	fClose(nHOut)
	
ElseIf cFormat == "CSV" 
	
	// Formato CSV
	// Strings entre aspas duplas, campos colocados na ordem da estrutura
	// Virgula como separador de campos, CRLF separador de linhas 
	// Gera o CSV com Header
	// Campo MEMO não é exportado
	
	nHOut := fCreate(cFileOut)
	If nHOut == -1
		::_SetError(-12,"Output CSV File Create Error - FERROR "+cValToChar(Ferror()))
		Return .F.
	Endif
	
	// Primeira linha é o "header" com o nome dos campos 
	For nPos := 1 TO ::nFldCount
		If nPos > 1 
			cBuffer += ','
		Endif
		cBuffer += '"'+Alltrim(::aStruct[nPos][1])+'"'
	Next
	cBuffer += CRLF

	::GoTop()
	
	While !::Eof()
		
		// Monta uma linha de dados
		cRow := ""
		
		For nPos := 1 TO ::nFldCount
			cTipo := ::aStruct[nPos][2]
			nTam  := ::aStruct[nPos][3]
			nDec  := ::aStruct[nPos][4]
			If nPos > 1
				cRow += ","
			Endif
			If cTipo = 'C'
				// Dobra aspas duplas caso exista dentro do conteudo 
				cRow += '"' + StrTran(rTrim(::FieldGet(nPos)),'"','""') + '"'
			ElseIf cTipo = 'N'
				// Numero trimado 
				cRow += cValToChar(::FieldGet(nPos))
			ElseIf cTipo = 'D'
				// Data em formato AAAAMMDD entre aspas 
				cRow += '"'+Alltrim(DTOS(::FieldGet(nPos)))+'"'
			ElseIf cTipo = 'L'
				// Boooleano true ou false
				cRow += IIF(::FieldGet(nPos),'true','false')
			Endif
		Next
		
		cRow += CRLF
		cBuffer += cRow
		
		If len(cBuffer) > 32000
			// A cada 32 mil bytes grava em disco
			fWrite(nHOut,cBuffer)
			cBuffer := ''
		Endif
		
		::Skip()
		
	Enddo

	// Grava resto do buffer que falta 
	If len(cBuffer) > 0 
		fWrite(nHOut,cBuffer)
		cBuffer := ''
	Endif
	
	fClose(nHOut)
	
ElseIf cFormat == "JSON" 

	// Formato JSON - Exporta estrutura e dados   
	// Objeto com 2 propriedades 
	// header : Array de Arrays, 4 colunas, estrutura da tabela
	// data : Array de Arrays, cada linha é um registro da tabela, 
	// campos na ordem da estrutura
	// -- Campo Memo não é exportado 
	
	/*
	{
	"header": [
		["cCampo", "cTipo", nTam, nDec], ...
	],
	"data": [
	    ["José", 14, true], ...
	]
	}
	*/

	nHOut := fCreate(cFileOut)
	If nHOut == -1
		::_SetError(-12,"Output JSON File Create Error - FERROR "+cValToChar(Ferror()))
		Return .F.
	Endif


	cBuffer += '{' + CRLF
	cBuffer += '"header": [' + CRLF

	For nPos := 1 to len(::aStruct)
		If nPos = 1 
			cBuffer += "["
		Else
			cBuffer += '],'+CRLF+'['
		Endif
		cBuffer += '"'+Alltrim(::aStruct[nPos][1])+'","'+;
			::aStruct[nPos][2]+'",'+;
			cValToChar(::aStruct[nPos][3])+','+;
			cValToChar(::aStruct[nPos][4])
	Next

	cBuffer += ']'+CRLF
	cBuffer += ']' + CRLF
	cBuffer += ',' + CRLF
	cBuffer += '"data": [' + CRLF
		
	::GoTop()
	
	While !::Eof()
		
		// Monta uma linha de dados
		if lFirst
			cRow := "["
			lFirst := .F. 
		Else
			cRow := "],"+CRLF+"["
		Endif
				
		For nPos := 1 TO ::nFldCount
			cTipo := ::aStruct[nPos][2]
			nTam  := ::aStruct[nPos][3]
			nDec  := ::aStruct[nPos][4]
			If nPos > 1
				cRow += ","
			Endif
			If cTipo = 'C'
				// Usa Escape sequence de conteudo 
				// para astas duplas. -- 
				cRow += '"' + StrTran(rTrim(::FieldGet(nPos)),'"','\"') + '"'
			ElseIf cTipo = 'N'
				// Numero trimado 
				cRow += cValToChar(::FieldGet(nPos))
			ElseIf cTipo = 'D'
				// Data em formato AAAAMMDD como string
				cRow += '"'+Alltrim(DTOS(::FieldGet(nPos)))+'"'
			ElseIf cTipo = 'L'
				// Boooleano = true ou false
				cRow += IIF(::FieldGet(nPos),'true','false')
			Endif
		Next
		
		cBuffer += cRow
		
		If len(cBuffer) > 32000
			// A cada 32 mil bytes grava em disco
			fWrite(nHOut,cBuffer)
			cBuffer := ''
		Endif
		
		::Skip()
		
	Enddo

	// Termina o JSON
	cBuffer += ']' + CRLF
	cBuffer += ']' + CRLF
	cBuffer += '}' + CRLF

	// Grava o final do buffer
	fWrite(nHOut,cBuffer)
	cBuffer := ''
	
	// Fecha o Arquivo 
	fClose(nHOut)
	

Else

	UserException("Formato ["+cFormat+"] não suportado. ")

Endif

Return

Otimizações

Uma otimização interessante é usar a variável de memória cBuffer para armazenar os dados que devem ser gravados no arquivo, e apenas fazer a gravação caso ela tenha atingido ou ultrapassado 32000 (32 mil) bytes. É muito mais rápido fazer a variável de memória aumentar de tamanho, do que o arquivo no disco. Logo, é mais eficiente gravar um bloco de 32000 bytes no arquivo, do que gravar 32 blocos de 1000, pois a cada bloco gravado o sistema operacional aloca mais espaço para o arquivo, e é mais rápido alocar um espaço maior de uma vez do que várias chamadas de alocações menores.

Porém, não é por isso que eu vou deixar a minha String em AdvPL chegar a 1 MB de tamanho para fazer a gravação, pois com strings muito grandes na memória, as operações de alocar mais espaço em memória vão ficar mais pesadas. Para mim, algo perto de 32 KB é um “número mágico” bem eficiente e sem desperdício.

Conclusão

Bem, por hora foi feita a exportação… agora, vou queimar mais alguns neurônios pra fazer a importação destes dados nestes formatos 😀

Desejo a todos, como de costume, MAIS TERABYTES DE SUCESSO !!!

Referências

 

 

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

Introdução

Nos posts anteriores (Abstração de Acesso a Dados e Orientação a Objetos – Parte 02,Abstração de Acesso a Dados e Orientação a Objetos), vimos a montagem de um encapsulamento de acesso a dados usando orientação a objetos com herança em AdvPL. Agora, vamos integrar esse mecanismo com um Alias / WorkArea do AdvPL.

Criando uma Tabela ( DBF ou MEMORY ) a partir de um ALIAS

Usando as classes de arquivo em memória (ZMEMFILE) ou arquivo DBF (ZDBFFILE), podemos criar uma tabela (em memória ou no disco) com a mesma estrutura de uma outra tabela de uma destas classes, informando como parâmetro o objeto. Que tal ela também receber um ALIAS como parâmetro ? Pode ser de uma tabela qualquer, não importa. E, melhor ainda, que tal este método receber um segundo parâmetro, que caso seja especificado .T. (verdadeiro), já abre a tabela criada em modo exclusivo e copia todos os dados do ALIAS informado como parâmetro? Veja o exemplo abaixo:

#include 'protheus.ch"

User Function QRY2MEM()
Local cQuery
Local nH

nH := tclink()
IF nH < 0
  MsgStop("TCLINK ERROR - "+cValToChar(nH))
  Return
Endif

// Cria um select statement
cQuery := "SELECT CPF , NOME, VALOR from DOADORES WHERE VALOR > 2000 order by 3 DESC"

// Abre a Query no alias QRY 
USE (tcGenQry(,,cQuery)) ALIAS QRY SHARED NEW VIA "TOPCONN"

// Ajusta um campo 
TCSetField("QRY","VALOR","N",12,2)

// Cria um objeto de arquivo em memoria 
oMemory := ZMEMFILE():New('QRYINMEMORY')

// Popula o objeto com a estrutura e dados da Query 
// Passando o ALIAS como parâmetro. 
oMemory:CreateFrom("QRY",.T.)

// Mostra o conteudo da tabela na memoria. 
// Nao precisa abrir o objeto, o CreateFrom() já fez isso
While !oMemory:Eof()
  conout(oMemory:Fieldget(1)+" "+oMemory:FieldGet(2)+" "+cValToChar(oMemory:FieldGet(3))) 
  oMemory:Skip()
Enddo

// Fecha e mata a tabela
oMemory:Close()
FreeObj(oMemory)

// fecha a query
USE

return

Agora sim a coisa ficou prática. E, usando esta abordagem, eu tenho algumas vantagens incríveis. Primeira, com a Query copiada para a memória, usando o arquivo em memória eu posso mexer nos dados, eu posso inserir novos registros, posso navegar para frente e para trás ( Skip -1 ), e tudo o mais que com um ALIAS a partir de uma Query, nada disso é possível de ser feito.

O método CreateFrom() trabalha em conjunto com o AppendFrom(), ambos do ZISAMFILE. Uma vez determinado que eles receberam uma string ao invés de um Objeto como parâmetro, eles assumem que a string contém um ALIAS de uma WorkArea aberta, e fazem a leitura de estrutura e dados do ALIAS informado.

Métodos CreateFrom e AppendFrom

METHOD CreateFrom( _oDBF , lAppend  ) CLASS ZISAMFILE
Local lFromAlias := .F. 
Local cAlias := ""
Local aStruct := {}

If lAppend = NIL ; lAppend := .F. ; Endif

If valtype(_oDBF) == 'C'
	// Se a origem é caractere, só pode ser um ALIAS 
	lFromAlias := .T. 
	cAlias := alltrim(upper(_oDBF))
	If Select(cAlias) < 1 
		UserException("Alias does not exist - "+cAlias)
	Endif
	aStruct := (cAlias)->(DbStruct())
Else
	aStruct := _oDBF:GetStruct()
Endif

If !::Create(aStruct)
	Return .F.
Endif

IF lAppend
	// Dados serão apendados na criação 
	// Abre para escrita exclusiva 
	If !::Open(.T.,.T.)
		Return .F.
	Endif
	// Apenda os dados	
	IF !::AppendFrom(_oDBF)
		Return .F.
	Endif
	// E posiciona no primeiro registro 	
	::GoTop()
Endif
Return .T.
METHOD AppendFrom( _oDBF , lAll, lRest , cFor , cWhile ) CLASS ZISAMFILE
Local aFromTo := {}
Local aFrom := {}
Local nI, nPos, cField
Local lFromAlias := .F. 
Local cAlias := ""

DEFAULT lAll  := .T. 
DEFAULT lRest := .F.
DEFAULT cFor := ''
DEFAULT cWhile := ''
              
// Primeiro, a tabela tem qye estar aberta
IF !::lOpened
	UserException("AppendFrom Failed - Table not opened")
	Return .F.
Endif

IF !::lCanWrite
	UserException("AppendFrom Failed - Table opened for READ ONLY")
	Return .F.
Endif

If valtype(_oDBF) == 'C'
	// Se a origem é caractere, só pode ser um ALIAS 
	lFromAlias := .T. 
	cAlias := alltrim(upper(_oDBF))
	If Select(cAlias) < 1 
		UserException("Alias does not exist - "+cAlias)
	Endif
	aFrom := (cAlias)->(DbStruct())
Else
	aFrom := _oDBF:GetStruct()
Endif

// Determina match de campos da origem no destino 
For nI := 1 to len(aFrom)
	cField :=  aFrom[nI][1]
	nPos := ::FieldPos(cField)
	If nPos > 0 
		aadd( aFromTo , { nI , nPos })
	Endif
Next

IF lFromAlias
	// Dados de origem a partir de uma WorkArea
	If lAll 
		// Se é para importar tudo, pega desde o primeiro registro 
		(cAlias)->(DbGoTop())
	Endif
	While !(cAlias)->(EOF())
		// Insere um novo registro na tabela atual
		::Insert()
		// Preenche os campos com os valores da origem
		For nI := 1 to len(aFromTo)
			::FieldPut(  aFromTo[nI][2] , (cAlias)->(FieldGet(aFromTo[nI][1]))  )
		Next
		// Atualiza os valores
		::Update()
		// Vai para o próximo registro
		(cAlias)->(DbSkip())
	Enddo
	
Else
	If lAll 
		// Se é para importar tudo, pega desde o primeiro registro 
		_oDBF::GoTop()
	Endif
	While !_oDBF:EOF()
		// Insere um novo registro na tabela atual
		::Insert()
		// Preenche os campos com os valores da origem
		For nI := 1 to len(aFromTo)
			::FieldPut(  aFromTo[nI][2] , _oDBF:FieldGet(aFromTo[nI][1])  )
		Next
		// Atualiza os valores
		::Update()
		// Vai para o próximo registro
		_oDBF:Skip()
	Enddo
Endif
Return .T.

O CreateFrom() permite criar a tabela apenas com a estrutura, porém se parametrizado com .T. no segundo parâmetro, já abre a tabela atual e importa os dados do objeto ou ALIAS de origem especificado.

Próximos passos

Criando mais alguns encapsulamentos, será possível colocar de modo mais prático uma tabela em Cache. Eu posso criar uma tabela temporária em memória com o resultado de uma Query, e colocar esta tabela no cache, para ser recuperada conforme a necessidade. Lembrando que esta tabela não deve ser monstruosa, mas ter um número de registros que isole um contexto. Senão você armazena um caminhão de tijolos no Cache, mas quando você resgata  o cache para uso, você usa apenas alguns tijolos.

Conclusão

Nada a declarar. Fontes da zLib atualizados no GITHUB, agora é só bolar um encapsulamento neste mesmo padrão para uma tabela ISAM do DBAccess e para uma Query, depois fazer Export e Import para outros formatos (JSON, TXT, SFD, CSV , XML, “Socorro”).

Desejo a todos um bom proveito desta implementação, e TERABYTES DE SUCESSO !!!

 

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 😀