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

Introdução

No post anterior (Abstração de Acesso a Dados e Orientação a Objetos – Parte 04), foram implementadas algumas opções de exportação de tabelas, para os objetos ZDBFFILE e ZMEMFILE. Enquanto isso, foi implementado na classe ZISAMFILE a importação dos formatos SDF e CSV. Agora, para compatibilizar alguns recursos do AdvPL já existentes com estas classes, vamos partir para as novas classes ZTOPFILE e ZQUERYFILE.

Classe ZQUERYFILE

Quando queremos um result-set de uma Query em AdvPL, usamos a função TCGenQry() em conjunto com a função DBUseArea(), para abrir um cursor READ ONLY e FORWARD ONLY usando um ALIAS em AdvPL.

Embora todas as funções ISAM possam ser usadas em uma WorkArea aberta sob um ALIAS, para um cursor ou result-set de Query, aplicam-se apenas as seguintes funções:

  • DbSkip() — Avança um registro no cursor. Podemos especificar um número de registros positivo para avançar, mas nunca negativo. Lembre-se, cursor FORWARD ONLY.
  • DBGoTOP() — Se você ainda está no primeiro registro da Query, a função é ignorada. Se você já leu um ou mais registros, ou mesmo todos os registros da Query, como não tem como “voltar” registros, a função fecha e abre a query novamente no Banco de Dados, recuperando um novo result set. Logo, se no intervalo de tempo entre a primeira abertura do cursor e o DBGoTOP(), registros foram acrescentados ou tiveram algum campo alterado, a quantidade de registros retornada pela Query pode ser diferente.
  • TCSetField() — Se no banco de dados existem um campo “D” Data em AdvPL, ele é gravado e retornado pela Query como um campo “C” Caractere de 8 posições. Usando a função TCSetField() podemos informar ao Protheus que esta coluna deve ser retornada no campo do ALIAS j;a convertida para Data. O mesmo se aplica a precisão de campos numéricos, e a campos do tipo “L” lógico — que no Banco de Dados são gravados com um caractere “T” indicando verdadeiro ou “F” indicando falso.
  • DbCloseArea() — Fecha o alias da Query
  • FieldGet() — Recupera o valor de um campo da query pelo número do campo
  • DbSTruct() — Retorna o array com a definição dos campos retornados na Query.
  • FieldPos() — Retorna a posição de um campo na estrutura a partir do nome.

Quaisquer outras instruções são ignoradas, ou retornam algum tipo de erro. DBRLOCK() sempre retorna .F., pois um alias de Query é READ ONLY — Somente leitura. Nao é possível setar filtros, nem para registros deletados. O filtro de registros deletados deve ser uma condição escrita de forma literal na Query, usando o campo de controle D_E_L_E_T_ .

A ideia da classe ZQUERYFILE é encapsular o alias retornado da Query, e implementar as mesmas funcionalidades que um objeto de acesso a dados da ZLIB foram concebidos — como por exemplo o ZDBFFILE e o ZMEMFILE.

Desta forma, uma rotina escrita para trabalhar com um tipo de arquivo que herda desta classe ou que possui os métodos mínimos de navegação necessários possa utilizá-la também, onde colocamos um overhead mínimo apenas criando métodos para encapsular os acessos aos dados.

Classe ZTOPFILE

O DBAccess permite eu criar um arquivo em um banco relacional (SQL) homologado, e emular um acesso ISAM neste arquivo, como se ele fosse um arquivo DBF. Toda essa infra-estrutura já está pronta e operacional dentro do Protheus Server. O objetivo desta classe é fazer o mesmo encapsulamento de acesso feito pela ZQUERYFILE, porém implementando todas as funcionalidades disponíveis, como filtros e índices, além da navegação entre registros pela ordem de um índice.

Dentro da classe — que vai herdar a ZISAMFILE — implementamos os métodos para usar os recursos nativos, e alguns adicionais para permitir o uso de recursos implementados na ZLIB, como por exemplo o índice em memória do AdvPL — ZMEMINDEX.

Uso após a implementação

Alguns métodos das classes ZISAMFILE já foram concebidos para trabalhar nativamente com um objeto de dados, ou o ALIAS de uma WorkArea. A ideia de utilização do modelo de abstração é permitir flexibilizar a manutenção e persistência de dados — permanente ou temporária — usando instruções de alto nível, dentro de fronteiras e comportamentos pré-definidos para viabilizar escalabilidade, desempenho e resiliência para a aplicação.

Idéias e mais idéias

Quer ver uma coisa deveras interessante que pode ser feita com, esta infraestrutura ? Então, imagine um operador de sistema realizando por exemplo uma inclusão de dados com muitas informações, em um terminal com conexão síncrona, como o SmartClient. Em determinado momento, algo horrível aconteceu com a rede entre a estação (SmartClient) e o Servidor (Protheus Server) que ela estava conectada. Vamos ao pior cenário, houve um problema de Hardware (no servidor, na rede, na própria estação — acabou a luz. e o terminal em uso não têm No-Break. Adeus operação, dados já digitados, foi pro beleléu.

Bem, usando um cache — por exemplo o ZMEMCACHED — a aplicação poderia gravar no cache, em intervalos de tempo pré-definidos ou mesmo a cada informação significativa digitada, um registro com os dados já preenchidos até aquele momento, atrelado ao usuário que está realizando a operação ou a operação em si. No término da operação, esta informação é removida do cache. Caso o processo seja interrompido, no momento que o usuário fizer o login, ou quando ele entrar na rotina novamente na rotina, o programa verifica se tem alguma informação no cache daquele usuário, e em caso afirmativo, permita a informação ser recuperada, e o usuário conseguiria continuar a operação do ponto onde foi feito o último salvamento.

Para isso ser feito de forma simples, a operação deve ser capaz de serializar — representar em um formato armazenável e recuperável — o seu estado no momento, para tornar fácil a operação de salvamento e recuperação de estado. Para isso, poderemos usar — assim que estiver pronta — a classe ZSTREAM.

Com ela, a ideia é ser possível inclusive salvar não apenas valores, mas um objeto. Isso mesmo, pegar as propriedades de um objeto automaticamente e salvar o estado do objeto em um cache, no disco ou onde você quiser. Porém, para restaurar o estado do objeto, você deverá recria-lo, e fazer os métodos Save e Load nele, para ser possível ajustar as propriedades necessárias, lembrando que CodeBlock não dá para ser salvo e restaurado, o bloco de código pode depender de variáveis do ambiente e referências de onde ele foi criado.

Neste cenário, o mundo ideal seria criar uma classe apenas para ser um agrupador de propriedades e ser o container de contexto de dados de uma operação. Neste caso, poderíamos construir um método NEW() na estrutura, retornando self e populando as propriedades com seus valores default. e os métodos SAVE e LOAD para salvar ou recuperar o estado atual a partir de uma Binary String — ou Stream.

Conclusão

Quando começamos a encapsular recursos, realmente conseguimos reaproveitar muito código. Os exemplos dos posts anteriores de CRUD em AdvPL — usando SmartClient e AdvPL ASP — foram úteis para mostrar o que pode ser feito usando instruções básicas da linguagem AdvPL. Agora, com o agrupamento de algumas funcionalidades em um Framework (zLib), podemos reaproveitar código e implementar funcionalidades similares com menos esforço. As classes mencionadas acima ainda estão em desenvolvimento, e serão acompanhadas de novos posts assim que estiverem “prontas” para uso !!!

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

 

 

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

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 😀

 

 

 

Acelerando o AdvPL – Importação de tabelas

Introdução

Existem muitas situações onde existe a necessidade de alimentar ou importar tabelas para uso do ERP Microsiga / Protheus. Quando esta necessidade envolve um grande número de registros, e um curto espaço de tempo, precisamos fazer esta operação ser o mais rápida possível. Nesse post vamos abordar algumas técnicas para realizar este tipo de operação.

  • Crie os índices da tabela apenas após a importação dos dados. 

Se a tabela está vazia, e os indices de trabalho desta tabela já existem, cada novo registro acrescentado faz o banco de dados atualizar todos os índices durante a inserção dos dados. Sem os índices, o banco apenas insere os dados. A inserção vai ser muito mais rápida, e os índices também são criados mais rápido no final do processo de importação, quando você já inseriu todos os registros na tabela. Se você precisa ter um índice na tabela, no processo de importação, por exemplo para fazer alguma validação ou query na tabela enquanto ela está sendo importada, crie apenas este índice.

  • Trabalhe com blocos de inserções transacionadas

Os bancos de dados relacionais garantem a integridade dos dados através do registro de um “LOG Transacional”. Cada operação de inserção primeiro é gravada no LOG do SGDB, e depois é “efetivada” — o Banco de Dados faz o COMMIT da informação. Quando não estamos usando uma transação explícita, cada instrução executada no Banco de Dados faz COMMIT automaticamente no final da instrução. É mais eficiente abrir uma transação — BEGIN TRANSACTION — pois enquanto a transação está aberta, cada instrução gera apenas o LOG Transacional, e depois de fazer por exemplo 1000 inserções na base de dados, você encerra a transação — END TRANSACTION no AdvPL — e neste momento o Banco de dados faz o COMMIT de todas as linhas do LOG de uma vez.

  • A nova tabela que vai receber os dados deve ser criada pelo DBAccess 

Mesmo que você saiba quais são os tipos dos campos da estrutura da tabela final, onde os dados serão gravados, quem sabe criar a tabela nos padrões e moldes necessários para o correto funcionamento da aplicação é o Protheus, que passa para o DBAccess a estrutura da tabela a ser criada. Internamente, o DBAccess cria constraints e outros elementos, além de alimentar algumas tabelas de controle interno. Deixe o DBAccess fazer a criação da tabela. Você pode até apagar os índices de dados para fazer a importação da tabela após eles terem sido criados, porém não apague o índice da primary key (R_E_C_N_O_), e se a tabela possui índice único (sufixo _UNQ), mantenha ele também, caso você queira que o próprio banco de dados aborte a operação no caso de haver uma chave única duplicada nos dados sendo importados.

  • Usando as funções de baixo nível de tabelas do AdvPL 

Caso a importação dos dados seja realizada por uma aplicação AdvPL, que foi criada para esta finalidade, que não vai concorrer com os processos do ERP, podemos usar diretamente as funções de baixo nível de tabelas do AdvPL. As funções de mais alto nível — como RECLOCK e MSUNLOCK devem ser usadas dentro dos programas do ERP, no ambiente do ERP, pois elas têm tratamentos adicionais ligados ao FrameWork do ERP. Se, ao invés disso, usarmos diretamente as funções diretas de manutenção de dados, em um cenário controlado, podemos obter mais ganhos de desempenho — DBAppend() para acrescentar um novo registro, DBRUnlock() — sem nenhum parâmetro — para soltar o bloqueio (lock) de todos os registros obtidos durante aquele processo (lembrando que eu não posso soltar o lock dentro de transação), abrir e fechar transação sem usar BEGIN e END TRANSACTION — Usando diretamente a função TCCommit().

IMPORTANTE

O Uso das funções básicas do AdvPL para a manutenção de tabelas NÃO DEVE SER MISTURADO com o uso das funções do Framework. Dentro de um processo preparado para o contexto de execução do ERP, BEGIN TRANSACTION trabalha junto com END TRANSACTION, Reclock() e MSUnlock() trabalham juntas, ambas possuem tratamentos diferenciados e automáticos quando você programa dentro do ambiente do ERP e/ou dentro de um bloco transacionado, e todas elas internamente fazem uso das funções de baixo nível do AdvPL. Se você vai fazer uma aplicação que não vai rodar dentro do contexto do ERP, como por exemplo as aplicações que eu publico no BLOG — CRUD em AdvPL por exemplo — você deve usar apenas as funções de baixo nível. Misturar chamadas diretas das funções de Framework com chamadas das funções de baixo nível NO MESMO PROCESSO pode causar efeitos imprevisíveis.

  • Abra a tabela de destino em modo exclusivo

Se a sua aplicação vai usar o ambiente do ERP — Prepare Environment, Reclock e afins, abra a tabela de destino em modo exclusivo — veja como na documentação da função ChkFile() do Framework AdvPL. Caso você vá utilizar as funções de baixo nível de acesso a tabelas, veja como abrir uma tabela em modo exclusivo na documentação da função DBUseArea() da TDN ou do comando USE da TDN. Caso a tabela esteja aberta em modo compartilhado, cada inserção de um novo registro faz o DBAccess registrar um bloqueio desse registro na inserção, e cada desbloqueio gera uma requisição a mais ao DBAccess para soltar o bloqueio. Quando usamos a tabela em modo EXCLUSIVO, somente o meu processo está usando a tabela, e não é feito nenhum lock no DBAccess.

  • Use um BULK INSERT ou ferramenta de apoio do próprio Banco de Dados

Não há forma mais rápida de inserir dados em uma tabela senão usando uma ferramenta do próprio Banco de Dados. Porém, nem sempre é possível fazer isso de forma automática, e alguns cuidados são necessários. Primeiro, a tabela já deve ter sido previamente criada pelo DBAccess. Depois, a gravação dos dados pelo Banco deve ser feita da mesma forma que o DBAccess faria — Qualquer campo do tipo caractere deve estar preenchido com espaços em branco até o final do tamanho do campo, nenhum campo pode ter valor NULL — exceto campo MEMO –, um campo “D” Data em AdvPL em uma tabela do DBAccess é gravado como “C” Caractere com 8 posições, no formato AAAAMMDD, uma data vazia são 8 espaços em branco, um campo “L” lógico do AdvPL é criado pelo DBAccess no SGDB como um campo de 1 Caractere, contento a letra “T” para valor verdadeiro e “F” para falso, um campo numérico têm o valor default = 0 (zero).

  • Diminua a distância entre as informações de origem e destino 

Normalmente uma ferramenta do próprio banco de dados que faz inserção deve ser executada na própria máquina onde está o SGDB. Isto a torna ainda mais rápida. Se você está fazendo uma importação de dados que estão sendo lidos pelo Protheus de alguma fonte de dados, e depois repassadas ao Banco de Dados através do DBAccess, e cada um destes componentes (Protheus, DBAccess e o SGDB) estão em máquinas distintas, onde existe uma interface de rede entre cada um destes componentes, fatalmente a rede vai influenciar no desempenho desta operação. Se for possível eliminar pelo menos uma camada de rede entre as aplicações, pelo menos na hora de fazer a importação, ela tende a ser mais rápida.

  • Se posssível, leia as informações da origem com Query

Se a origem dos dados for uma tabela da base de dados, ao invés de abrir a tabela no modo ISAM — com ChkFile() ou DBUseArea() — leia os dados da tabela com um SELECT — O DBAccess a partir de 2017 passou a trafegar blocos de linhas entre o DBAccess e o Protheus, o que é muito mais performático principalmente quando ambos estão em máquinas diferentes. No acesso ISAM emulado, cada DBSkip() na tabela traz apenas um registro. Quando usamos a Query, várias linhas são trafegadas em uma unica requisição, e cada DBSkip() na Query consome a próxima linha em cache sem usar a rede. Quando as linhas do cache terminarem, o DBAccess envia mais um bloco de linhas.

Inserção direta via TCSqlExec()

Da mesma forma que é possível usar uma aplicação externa para inserir dados direto no banco, é possível também realizar a inserção direta no banco em AdvPL usando TCSqlEXec() — MAS, POREM, TODAVIA, CONTUDO, ENTRETANTO, você precisa tomar os mesmos cuidados como se você estivesse alimentando estes dados por fora, E, você somente vai ter algum ganho de desempenho se você montar uma string de parâmetro com mais de uma inserção — por exemplo, inserindo múltiplas linhas na mesma instrução de insert — caso o banco de dados tenha suporte para isso — ou concatenando mais de uma instrução de inserção na mesma requisição, separando as instruções com “;” — ponto e vírgula.

cStmt := "INSERT INTO TABELA(CPO1,CPO2,R_E_C_N_O_) VALUES ('00001','Nome',1);"
cStmt += "INSERT INTO TABELA(CPO1,CPO2,R_E_C_N_O_) VALUES ('00002','Endereço',2);"
(...)
nRet := TCSqlExec(cStmt)

No exemplo acima, você pode por exemplo montar uma string de inserção com por exemplo 32 KB de dados, e enviar isso de uma vez para o banco de dados. Mas, inserir dados desta forma possui vantagens e desvantagens:

  • Uma requisição insere vários registros na mesma instrução. Usando DBAppend() / Reclock(), é inserido um registro por vez.
  • Uma requisição maior no lunar de várias requisições menores aproveita muito mais a banda de rede disponível.

Por outro lado…

  • Para montar a inserção dos valores de forma literal, você deve tratar manualmente a existência de caracteres especiais, aspas simples como conteúdo de campo, e para alguns bancos tratar “escape sequences” e caracteres especiais.
  • Campos MEMO (BLOB/CLOB/LONGVARBINARY) normalmente não são suportados com envio literal pela instrução de INSERT feita desta forma. Você acaba fazendo a inserção primeiro dos dados dos campos de tamanho fixo, e depois tem que usar o modo ISAM de acesso para alterar o registro em AdvPL, para então alimentar o conteúdo do campo.

IMPORTANTE

Nenhum dos métodos acima é recomendável de ser feito em processos concorrentes, em modo compartilhado, com outros programas inserindo informações na tabela em questão. As sugestões deste post mostram apenas jeitos diferentes e alternativas de importação de dados puros direto para tabelas de um banco de dados. Como nestes exemplos os dados são praticamente escritos “direto” nas tabelas, a aplicação que está importando os dados deve ser a responsável por criticar os dados de entrada, e garantir que a sua escrita esteja em conformidade com as regras do ERP. Mesmo uma operação de cadastro feita no ERP pode disparar integrações e processos auxiliares que alimentam outras tabelas. Mexer diretamente na base de dados sem respeitar as regras de negócio e/ou as validações do produto podem gerar comportamentos inesperados e indesejáveis no produto e nas rotinas que consomem estes dados. A forma mais segura de importar dados em tabelas do ERP é a utilização de rotinas automáticas, onde fatalmente vai existir um pênalti de desempenho para validar todos os valores de todos os campos informados, mas desta forma evita-se a alimentação incompleta ou inconsistente de dados que pode prejudicar o comportamento do produto.

Conclusão

Não existe “almoço grátis”, alguém está sempre pagando a conta. Uma inserção de dados é um processo deveras simples, você obtêm maior desempenho basicamente realizando requisições em blocos, e pode ganhar mais reduzindo as operações intermediárias intrínsecas ao processo — porém este ganho normalmente abre mão de validações que visam garantir a integridade das informações imputadas, ou acabam exigindo mais trabalho e etapas adicionais para a conclusão do processo.

Novamente agradeço pela audiência, curtidas e compartilhamentos, e desejo a todos TERABYTES DE SUCESSO !!! 😀

Referências

 

Identificando Problemas – Queries lentas – Parte 04

Introdução

Continuando o assunto de identificação de problemas, vamos ver agora o que e como lidar com queries que não apresentam um bom desempenho. Antes de chamar um DBA, existem alguns procedimentos investigativos e algumas ações que ajudam a resolver uma boa parte destas ocorrências.

Queries, de onde vêm?

Quando utilizamos um Banco de Dados relacional homologado com o ERP Microsiga / Protheus, todas as requisições de acesso a dados passam pela aplicação DBAccess. Como eu havia mencionado em um post anterior, o DBAccess serve de “gateway” de acesso ao Banco de Dados, e serve para emular as instruções ISAM do AdvPL em um banco relacional.

Logo, as queries submetidas pelo DBAccess ao Banco de Dados em uso podem ser de de dois tipos:

  • Queries emitidas (geradas) pelo DBAccess para atender a uma requisição ISAM — DBGoTop(), DBGoBottom(), DbSkip(), DBSeek().
  • Queries abertas pela aplicação AdvPL para recuperar dados diretamente do Banco de Dados.

Quando utilizamos o DBAccess Monitor, para realizar um Trace de uma conexão do Protheus com o DBAccess, podemos visualizar as operações solicitadas do Protheus ao DBAccess, e as respectivas queries submetidas ao Banco de Dados.

Como identificar uma query “lenta”?

Normalmente encontramos uma ou mais queries com baixo desempenho quando estamos procurando a causa da demora de um determinado processo. A descoberta acaba sendo realizada durante a análise de um Log Profiler obtido durante a execução da rotina, ou também através de um trace da conexão do DBAccess usando o DBMonitor.

Agora, podemos também usar o próprio DBAccess para gerar um log das queries que demoraram mais para serem abertas pelo Banco de Dados. Basta utilizar a configuração MaxOperationTimer, onde podemos especificar um número de segundos limite, a partir do qual um log de advertência deve ser gerado pelo DBAccess, caso o retorno da abertura de uma Query ultrapasse o tempo definido.

O que é uma query lenta?

Definir lentidão normalmente não está ligado apenas a medida absoluta de tempo, mas sim é relativa a urgência ou necessidade de obter a informação rapidamente, versus a quantidade de informações a serem avaliadas e retornadas.

Por exemplo, quando a aplicação AdvPL executa uma instrução como DBGoto(N), onde N é o número de um registro da base de dados, o DBAccess vai montar e submeter uma Query contra o banco de dados, para selecionar todas as colunas da tabela em questão, onde o campo R_E_C_N_O_ é igual ao número N informado.

SELECT CPO1,CPO2,CPO3,...N FROM TABELA WHERE R_E_C_N_O_ = N

Meu chapa, essa Query deve rodar normalmente em menos de 1 milissegundo no banco de dados, por duas razões: A primeira é que a coluna R_E_C_N_O_ é a chave primária (Primary Key) de todas as tabelas criadas pelo DBAccess, então naturalmente existe um índice no Banco de Dados usando internamente para achar em qual posição do Banco estão gravadas as colunas correspondentes a esta linha. E a segunda é que, apenas uma linha será retornada.

Se não existisse um índice para a coluna R_E_C_N_O_, o Banco de Dados teria que sair lento a tabela inteira, sequencialmente, até encontrar a linha da tabela que atendesse esta condição de busca. Esta busca na tabela de dados inteira sem uso de índice é conhecida pelo termo “FULL TABLE SCAN”.

Agora, imagine um SELECT com UNION de mais quatro selects, finalizado com um UNION ALL, onde cada Query faz SUB-SELECTS e JOINS no Banco de Dados, e o resultado disso não vai ser pequeno … Mesmo em condições ideais de configuração do Banco de Dados, não é plausível exigir que uma operação deste tamanho seja apenas alguns segundos.

Causas mais comuns de degradação de desempenho em Queries

Entre as mais comuns, podemos mencionar:

  1. Ausência de um ou mais índices — simples ou compostos — no Banco de Dados, que favoreçam um plano de execução otimizado do Banco de Dados para recuperar as informações desejadas.
  2. Estatísticas do Banco de Dados desatualizadas.
  3. Picos de CPU , Disco ou Rede, na máquina onde está o DBAccess e/ou na máquina onde está o Banco de Dados.
  4. Problemas de hardware na máquina do Banco de Dados ou em algum dos componentes da infra-estrutura.
  5. Problemas de configuração ou de comportamento do próprio Banco de Dados sob determinadas condições.
  6. Excesso de operações ou etapas do plano de execução, relacionadas a complexidade da Query ou da forma que a Query foi escrita para chegar ao resultado esperado.

Recomendo adicionalmente uma pesquisa sobre “Full Table Scan” e outras causas possíveis de baixo desempenho em Queries. Quanto mais infirmação, melhor. E, a propósito, mesmo que a tabela não tenha um índice adequado para a busca, se ela for  realmente pequena (poucas linhas) , o Banco de Dados internamente acaba fazendo CACHE da tabela inteira em, memória, então um Full Table Scan acaba sendo muito rápido, quando a tabela é pequena. Esta é mais uma razão por que muitas ocorrências de desempenho relacionados a este evento somente são descobertas após a massa de dados crescer representativamente no ambiente.

Troubleshooting e boas práticas

Normalmente as melhores ferramentas que podem dar pistas sobre as causas do baixo desempenho de uma Query são ferramentas nativas ou ligadas diretamente ao Banco de Dados, onde a ferramenta é capaz de desenhar e retornar — algumas inclusive em modo gráfico — o PLANO DE EXECUÇÃO da Query. Neste plano normalmente as ferramentas de diagnóstico informam quando está havendo FULL TABLE SCAN, e quais são as partes da Query que consomem mais recursos no  plano de execução. Algumas destas ferramentas inclusive são capazes de sugerir a criação de um ou mais índices para optimizar a busca dos dados desejados.

Mesmo sem ter uma ferramenta destas nas mãos, normalmente necessária para analisar queries grandes e mais complexas, podemos avaliar alguns pontos em queries menores “apenas olhando”, por exemplo:

  1. Ao estabelecer os critérios de busca — condições e comparações usadas na cláusula WHERE — procure usar diretamente os nomes dos campos, comparando com um conteúdo fixo,  evitando o uso de funções. É clado, vão existir exceções, mas via de regra procure estar atento neste ponto.
  2. Evite concatenações de campos nas expressões condicionais de busca. Imagine que você tem uma tabela com dois campos, e você tem nas mãos, para fazer a busca, uma string correspondendo a concatenação destes dois valores. Muto prático você fazer SELECT X FROM TABELA WHERE CPO1 || CPO2 = ‘0000010100’, certo ? Sim, mas mesmo que você tenha um índice com os campos CPO1 e CPO2, o Banco de Dados não vai conseguir usar o índice para ajudar nesta Query — e corre o risco de fazer FULLSCAN. Agora, se ao inves disso, você quebrar a informação para as duas colunas, e escrever SELECT X FROM TABELA WHERE CPO1 = ‘000001’ AND CPO2  = ‘01000’ , o Banco de Dados vai descobrir durante a montagem do plano de execução que ele pode usar um índice para estas buscas, e vai selecionar as linhas que atendem esta condição rapidinho.
  3. O Banco de Dados vai analisar a sua Query, e tentar criar um plano de acesso (ou plano de execução) para recuperar as informações desejadas o mais rápido possível. Se todas as condições usadas na cláusula WHERE forem atendidas por um mesmo índice, você ajuda o Banco de Dados a tomar a decisão mais rapidamente de qual índice internamente usar, se você fizer as comparações com os campos na ordem de declaração do índice. Por exemplo, para o índice CPO1, CPO2, CPO3, eu sugiro  uma Query com SELECT XYZ from TABELA WHERE CPO1 = ‘X’ AND CPO2 = ‘Y’ AND CPOC3 >=  ‘Z’

Queries emitidas pelo DBAccess

As queries emitidas pelo DBAccess no Banco de Dados para emular o comportamento de navegação ISAM são por natureza optimizadas. Para emular a navegação de dados em uma determinada ordem de índice, o DBAccess emite queries para preencher um cache de registros — não de dados — usando os dados dos campos como condições de busca, na mesma sequência da criação do índice. E, para recuperar o conteúdo (colunas) de um registro (linha), ele usa um statement preparado, onde o Banco faz o parser da requisição apenas na primeira chamada, e as demais usam o mesmo plano de execução.

Porém, isto não impede de uma ou mais queries emitidas pelo DBAccess acabem ficando lentas. Normalmente isso acontece quando é realizada uma condição de filtro na tabela em AdvPL, onde nem todos — ou nenhum —  os campos utilizados não possuem um índice que favoreça uma busca indexada, fazendo com que o Banco acabe varrendo a tabela inteira — FULL SCAN — para recuperar os dados desejados.

Este tipo de ocorrência também é solúvel, uma vez determinado qual seria a chave de índice que tornaria a navegação com esta condição de filtro optimizada, é possível de duas formas criar este índice.

Criando um índice auxiliar

A forma recomendada de se criar um índice auxiliar é acrescentá-lo via Configurador no arquivo de índices do ERP (SIX), para que ele seja criado e mantido pelas rotinas padrão do sistema. Porém, para isso este índice não pode conter campos de controle do DBAccess no meio das colunas do índice, e para se adequar ao padrão do ERP, seu primeiro campo deveria sempre ser o campo XX_FILIAL da tabela.

Quando esta alternativa não for possível, existe a possibilidade de criar este índice diretamente no Banco de Dados. Porém, a existência desse índice não deve interferir na identificação de índices de emulação ISAM que o DBAccess faz quando qualquer tabela é aberta. Para isso, a escolha do NOME DO ÍNDICE é fundamental.

Um índice criado diretamente no Banco de Dados para estes casos deve ter um nome que seja alfabeticamente POSTERIOR aos índices declarados no dicionário de dados (SIX). Por exemplo, existe uma tabela ABC990, que possui 8 índices. O ERP Microsiga nomeia os índices da tabela no padrão usando o nome da tabela e mais um número ou letra, em ordem alfabética. Logo, os oito primeiros indices da tabela chamam-se, respectivamente, ABC9901, ABC9902 … ABC9908.

Nomeie seu primeiro índice customizado com o nome de ABC990Z1. Caso seja necessário mais um índice, ABC990Z2, e assim sucessivamente. Chegou no 9, precisa de mais um índice, coloque a letra “A” — ABC990ZA. Dessa forma, estes índices ficarão por último na identificação do DBAccess, e por eles provavelmente não se encaixarem no padrão do ERP, você não vai — e não deve — usá-los dentro de fontes customizados AdvPL — Estes índices vão existir apenas no Banco de Dados, para favorecer a execução de queries especificas ou filtros específicos de navegação.

Precauções

Uma vez que um ou mais índices sejam criados dentro do Banco de Dados, sem usar o dicionário ativo de dados (SIX), qualquer alteração estrutural na tabela feita por uma atualização de versão ou outro programa pode apagar estes índices a qualquer momento, caso seja necessário, e isso vai novamente impactar o desempenho da aplicação. Para evitar isto, é possível escrever uma rotina em AdvPL — customização — para verificar usando Queries no banco de dados se os índices auxiliares criados por fora ainda existem, e até recriá-los se for o caso. Pode ser usado para isso o Ponto de Entrada CHKFILE() , junto das funções TCCanOpen() — para testar a existência do índice — e TcSqlExec() — para criar o índice customizado usando uma instrução SQL diretamente no banco de dados.

Outras causas de degradação de desempenho

Existem ocorrências específicas, que podem estar relacionadas desde a configuração do Banco de Dados, até mesmo problemas no mecanismo de montagem ou cache dos planos de acesso criados pelo Banco para resolver as Queries. Outas ocorrências podem estar relacionadas a execução de rotinas automáticas ou agendadas no próprio servidor de Banco de Dados. Normalmente o efeito destas interferências são percebidos como uma lentidão momentânea porém generalizada, por um período determinado de tempo.

Conclusão

Use a tecnologia ao seu favor, e em caso de pânico, chame um DBA! Muitas vezes existe uma forma mais simples ou com menos etapas para trazer as informações desejadas. Outras vezes é mais elegante, rápido e prático escrever mais de uma query menor, do que uma Super-Query-Megazord.

Desejo a todos um ótimo final de semana, e muitos TERABYTES DE SUCESSO 😀

Referências