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 😀

 

Persistência de dados – ISAM

Introdução

Uma parte fundamental de qualquer sistema informatizado é a persistência dos dados. Hoje temos bancos de dados ISAM, relacionais ( SQL ), Bancos No-SQL — para dados não-tabulados, XML, entre outros. Cada um deles é fundamentado em um paradigma para o tipo, quantidade, formato e necessidade de armazenamento e recuperação de dados. Como a maioria dos recursos da tecnologia da informação, cada um destes bancos possui um diferencial ligado a casos de uso. Muitas aplicações comerciais podem utilizar mais de um tipo de banco de dados, de acordo com a necessidade de cada ponto da aplicação.

Neste tópico será abordado um pouco do conceito, características e utilização de mecanismos que usam a implementação ISAM para persistência e consulta de dados.

Conceito (um pouco de teoria é inevitável)

ISAM é um acrónimo de Indexed Sequential Access Method, ou método de acesso sequencial indexado. Trata-se de um método de indexação de dados para acesso rápido. Este mecanismo foi desenvolvido pela IBM para mainframes, antes da década de 70. O termo ISAM aplica-se atualmente a mecanismos e APIs de acesso a dados onde o desenvolvedor explicitamente faz a busca em um índice, para então recuperar a informação que o índice aponta (em contraste com um banco relacional, que possui um optimizador de queries que escolhe internamente o melhor índice a ser usado), ou ainda a um mecanismo de indexação que permita simultaneamente buscas sequenciais e por chave. Normalmente a API ISAM é composta por um grupo de funções, usadas para realizar operações individuais com tabelas, índices e registros.

Características e utilização

Os dados são organizados em registo de tamanho fixo, e salvos sequencialmente em um arquivo, onde a posição ordinal do dado no arquivo representa o seu número de registro. Um ou mais índices podem ser criados para este arquivo, com um ou mais campos nas chaves, e as operações básicas de recuperação de dados consiste basicamente em buscar uma informação diretamente em um índice a partir de uma chave de busca montada com os campos usados no índice, onde após o registro ser encontrado no índice, a informação na tabela de dados é posicionada (ponteiro de registro), e a partir deste instante você pode navegar para os próximos registros de dados ou aos registros anteriores, usando a ordem do índice atualmente aberto, ou usando a ordem natural de inserção (física) da tabela. Algumas APIs permitem campos de conteúdo variável, que normalmente são gravados em um arquivo auxiliar, onde um registro do arquivo ISAM de dados aponta para um offset do arquivo auxiliar para recuperar o(s) campo(s) de tamanho variável.

Na página dedicada ao ISAM pela Wikipedia (em inglês, vide referências no final do post), têm uma lista de aplicações que implementam a arquitetura ISAM. Entre elas, o Clipper, FoxPro, Microsoft Access, Faircom c-TreeACE, Dataflex, Paradox, entre outras. Existe uma relação mais próxima entre o dados fisicamente gravados e o desenho do algoritmo. Normalmente um sistema que utiliza uma engine ISAM para dados obtém grande desempenho desenhando bem a distribuição dos dados dentro de tabelas e os índices necessários para acessar rapidamente as informações.

Destas ferramentas, algumas também oferecem uma camada superior de abstração para implementar acesso relacional, com queries e tudo. A raiz dos dados no disco continua sendo ISAM, mas existem as tabelas de catálogo do banco (systables, sysindexes, syscolumns, etc.) e os algoritmos para realizar o parser de queries, recuperação de dados, transacionamento, etc.

Na prática, é muito mais fácil e prático desenvolver usando um banco relacional. Você cria as tabelas, índices para acelerar as buscas mais utilizadas ou que precisam efetivamente de performance, e utiliza recursos do SGDB para monitorar, configurar e administrar o Banco de Dados, como as estatísticas de índices, segmentação de dados, recursos avançados de pesquisa de textos, além de existir uma ampla gama de aplicações de mercado construídas para conectar-se com estes bancos e permitir de forma assistida a recuperação de dados para análises, estatísticas e relatórios.

Como eu mencionei anteriormente, cada um dos tipos de banco de dados possui um diferencial interessante, muito eficaz quando aplicado no seu cenário ideal. Por exemplo, em uma etapa intermediária de um processamento, ou para armazenamento de dados de baixa volatilidade ( pouca manutenção de estrutura e conteúdo), uma API ISAM pode oferecer um diferencial de performance, devido justamente ao fato de não termos presentes alguns overheads que são implícitos em um Banco de Dados relacional. Porém, a decisão de como distribuir os dados e como criar e usar os índices paa acessar as informações da forma mais eficiente possível ficam na mão do desenvolvedor. E se isto não for feito como manda o figurino, o ganho que poderia ser obtido em determinada parte do processo usando o ISAM é engolido pela forma de implementação do algoritmo.

Uso no ERP Microsiga

O ERP Microsiga utiliza um gateway de conexão com bancos de dados relacionais (DBAccess), homologado para uma lista de Bancos de Dados relacionais, como Microsoft SQL, Oracle, DB2, Informix, e inclusive versões Open Source, como o PostgreSQL e o MySQL — com algumas restrições e usos em produtos específicos. E, ao mesmo tempo, utiliza também o c-TreeACE, da Faircom, para acesso a tabelas de meta-dados do ERP (também conhecidos como “SXS”), bem como armazenamento de dados em módulos do ERP concebidos para serem executados em modo Stand-Alone, e para criar tabelas temporárias compartilhadas entre servidores (a partir do RootPath do ambiente, compartilhado pela rede com as demais máquinas servidoras em ambiente balanceado) e exclusivas (tabelas temporárias no HD local da máquina onde o serviço do TOTVS | Application Server está sendo executado).

Conclusão

Na programação contemporânea, quem não conhece mais profundamente a camada ISAM normalmente referem-se a ela como algo ultrapassado. Realmente, usar uma base principal ISAM em um sistema com larga previsão de crescimento de base de dados, dada a amarração intrínseca com o meio físico, fatalmente você iria esbarrar com questões de performance e dependência direta de devices/hardware. Inclusive, por causa disso, a IBM criou um superset do ISAM, chamado VSAM (Virtual Storage Access Method) na década de 70. Porém, a engine ISAM é muito eficiente para determinados casos de uso, muito comuns em etapas de processos intermediárias, como a criação de arquivos de uso temporário.

A criação de arquivos temporários é um tema que merece atenção especial, e será abordado em profundidade em um outro post, diretamente relacionado a performance e escalabilidade.

Até o próximo post, pessoal 😉

Referências

Wikipedia contributors. ISAM. Wikipedia, The Free Encyclopedia. October 20, 2014, 21:33 UTC. Available at: http://en.wikipedia.org/w/index.php?title=ISAM&oldid=630429759. Accessed December 19, 2014.