O que é CODEPAGE e ENCODING – Parte 02

Introdução

No post anterior (O que é CODEPAGE e ENCODING – Parte 01) vimos que por baixo do capô que tudo são bytes, e a interpretação deles depende da aplicação que o manipula e como o texto está codificado (ASCII, CP-nnnn ,UTF-8, etc) . Agora, vamos ver o impacto disso em interface, processamento e armazenamento de dados, relacionado ao ADVPL !!!

CP-1252 e ANSI

O Windows em Inglês, Português, e demais idiomas que usam o alfabeto Romano/Latino utiliza o codepage CP-1252 — também conhecido por WIN-1252. Veja a tabela completa nas referências, no final do post. Nas primeiras versões do Windows, a expressão “ANSI Code Page” foi utilizada para designar os codepages novos, não usados no DOS. Oficialmente o CodePage 1252 não virou um “ANSI Standard” (padrão oficial documentado pela A.N.S.I — American National Standard Institute),  por isso muitos programas se referem ao codepage do Windows como “ANSI”.

Microsiga, AdvPL e Protheus

Os códigos em AdvPL antes do Protheus foram escritos usando codepage CP-437 , e posteriormente o codepage CP-850 — que permitia caracteres acentuados em português — usados no DOS na época. Como existiam duas versões do ERP Siga Advanced — um executável para DOS e outro para Windows, e os componentes de interface do Windows usam o codepage CP-1252, existem duas funções para realizar a conversão de-para o CP-850 e o CP-1252. Chamam-se OemtoAnsi() e AnsiToOem() — e existem até hoje no Protheus. Embora o nome destas funções seja um tanto genérico, entenda-se por “OEM” o codepage do DOS CP-850, e por “ANSI” o codepage CP-1252 do Windows.

Quando o ERP Siga Advanced era configurado para permitir caracteres acentuados, estes eram gravados nas tabelas de dados do ERP — DBF na época — com o codepage em uso (CP-850). Porém, para mostrar este nome em um componente visual na versão Windows, o dado precisava ser convertido usando OemToAnsi(). A mesma conversão também era necessária ser feita com as mensagens em string presentes no código-fonte para serem visualizadas corretamente na interface.

Protheus, DBAccess e CP-1252

Com o nascimento do Protheus como plataforma de desenvolvimento client-server, com porte para Windows e Linux, foi adotado oficialmente o codepage CP-1252 para strings, fontes, includes e dados. Por isso, quando usamos o TDS (Totvs Development Studio) e/ou o VSCode para AdvPL, devemos configurar que os arquivos fonte devem ser tratados como CP-1252, e os bancos de dados relacionais (SQL) homologados para o Protheus devem também usar especificamente este codepage ou um equivalente — como o MSSQL por exemplo, o charset (definição de caratere) usado chama-se “Latin1”, o que corresponde diretamente ao CP-1252. No Banco de Dados Oracle, por exemplo, o charset chama-se “WE8MSWIN1252”

Pela mesma razão, as ODBCs utilizadas para conexão do DBAccess com os bancos de dados homologados, quando existem duas versões (ANSI ou UNICODE), deve ser usada a versão ANSI.

Protheus e UTF-8

Quando criamos páginas WEB (AdvPL-ASP) ou vamos integrar sistemas usando XML / Web Services, podemos receber conteúdos e dados codificados em UTF-8 (Unicode), mas para mostrar estes dados na interface, manipulá-los e gravá-los na base de dados, precisamos converter entre UTF-8 e CP-1252. Para isso, existem os seguintes recursos:

  • Funções AdvPL EncodeUTF8() e DecodeUTF8().

A primeira recebe uma string em CP-1252 e converte para UTF-8, enquanto a segunda faz a conversão de UTF-8 para CP1252.

  • Parsers XML ( XmlParser e tXMLManager)

Como os parsers XML podem receber dados em UTF-8 (Unicode), eles podem receber qualquer caractere de qualquer alfabeto. Porém, a linguagem AdvPL trabalha com o CP-1252. Logo, os parsers XML do AdvPL lêem os conteúdos do CML em UTF-8, e já convertem internamente para CP-1252, assim se um XML vir com uma string acentuada no alfabeto latino, a consulta ao valor da propriedade já vai retornar uma string no codepage da linguagem.

  • Web Services AdvPL ( Client e Server – SOAP ) 

Da mesma forma, uma string alimentada pelo AdvPL em uma propriedade de WebServices com o CP-1252, vai ser convertida internamente para UTF-8 na geração do XML SOAP para envio, e na recepção de dados.

Strings em AdvPL

Uma String — variável do tipo ‘c’ caractere em AdvPL — pode conter bytes de valor 0 a 255. Logo, você pode abrir um arquivo no disco — usando as funções de acesso direto a disco e arquivos FOpen() e FRead() — e ler qualquer coisa, desde texto em qualquer formato, imagens, etc. Porém, os componentes da interface visual do AdvPL — como tSay, tGet e afins — vão tratar este conteúdo como CP-1252.

Comparação de Strings – operadores > e <

O AdvPL compara strings pelo valor de seus bytes. Isso chama-se comparação BINÁRIA. Logo, ao comparar se uma string de mesmo tamanho é maior ou menor que outra, a sequência de bytes da string é percorrida desde o início e sendo comparada um a um, enquanto os valores forem iguais, até que o primeiro valor diferente entre as strings seja encontrado.

DBACCESS e SGDBs — O que é  “collation” ?

Até agora falamos de codificações (encoding) e formatos de representação de strings. Um banco de dados relacional possui tipos de dados criados para trabalhar com um codepage, e tipos de dados para trabalhar com Unicode. Normalmente os tipos CHAR e VARCHAR trabalham com um codepage, e os tipos NCHAR e NVARCHAR trabalhar com Unicode. Essa nomenclatura de tipos é praticamente universal.

Porém, o Banco de Dados precisa de mais informações para definir COMO ele deve tratar estes dados, em um caso de comparação e ordenação. As informações de como este dado será codificado e tratado é definida por um configuração de COLLATION. Normalmente eu posso escolher um collation para os campos que trabalham com um codepage, que define regras como :

  • Letras maiúsculas e minúsculas devem ser consideradas iguais
  • Letras acentuadas e não-acentuadas também devem ser tratadas iguais
  • Ao solicitar uma Query com uma determinada ordem, a ordem pode ser linguística (ordem fonética do idioma) ou binária (comparação dos valores dos bytes).

O collation do banco de dados que é compatível com o tratamento de strings do AdvLP é chamado de “collation binário”, onde a ordenação de strings é feita pelo valor dos bytes de seu conteúdo, e letras maiúsculas, minúsculas ,acentuadas e não-acentuadas são diferentes.

Então é por isso que os bancos de dados para o ERP Microsiga devem ser criados com um Collation de ordenação binaria ? R : SIM ! Vamos ver mais abaixo o porque.

O Problema de JOSÉ e JOÃO

Vamos pegar o CP-1252 do Windows, e ver quais são os bytes usados para representar estes dois nomes, com os respectivos acentos, em letras maiúsculas:

J = 74
O = 79
S = 83
à = 195
É = 201

JOÃO = 74,79,195,79
JOSÉ = 74,79,83,201

Logo, se eu colocar estes dois nomes em String no AdvPL, e perguntar se “JOÃO” é MENOR QUE “JOSÉ”, a resposta será .F. (falso). Pois embora alfabeticamente JOÃO vêm ANTES de JOSÉ, como o AdvPL trabalha com comparação binária de strings, a partir do 3o byte, a letra “S” de JOSÉ tem um valor menor que a letra “Ô de JOÃO.

Então, o que acontece se eu  usar um collation linguístico ?

Bem, primeiramente um collation “linguístico” é qualquer collation não-binário. E, um collation linguístico vai trazer os dados ordenados alfabeticamente de acordo com o idioma utilizado. Vamos pegar um exemplo de collation do MSSQL: Latin1_CI_AI ( CP1252, Idioma latino, case-insensitive ( CI = minúsculas e maiúsculas são consideradas iguais para comparação), e accent-insensitive ( AI = letras acentuadas ou não são consideradas iguais para efeito de comparação).

Usando esse collation no banco de dados, o comportamento das Queries em comparação e ordenação de resultado vai mudar. Por exemplo:

Na TABELA existem quatro registros : JOAO, JOSE, JOÃO e JOSÉ 

-- Collation Latin1_CI_AI
SELECT NOME FROM TABELA WHERE NOME = 'JOSE'
-- Serão retornados "JOSE" e "JOSÉ"

-- Collation Latin1_CI_AS
-- Será retornado apenas "JOSE", sem acento

O banco de dados está correto, este é o comportamento para  o qual ele foi configurado. Agora, se em AdvpL eu receber estes valores, e compará-los com a string “JOSE” sem acento, elas serão diferentes.

Se eu tiver na base de dados os nomes JOSE e JOAO, sem e com acento, e pedir a lista de nomes ordenada por nome, usando um collation linguístico — não-binário — a ordem de retorno será:

JOAO
JOÃO
JOSE
JOSÉ

Agora, se em AdvPL , cada registro retornado for testado com a comparação abaixo, eis o que acontece:

IF cNome >= "JOAO" .and. cNome <= "JOSE"
   conout("Nome .... " + cNome )
Endif

-- Como a comparação é binária, apenas os 
-- nomes sem acento serão considerados

Na prática, um collation linguístico somente pode ser usado em um banco de dados se a aplicação estiver preparada para lidar com estas diferenças. Vou agora dar um exemplo um pouco pior. Se eu usar em AdvPL um Collation CI_AI ( maiúsculas, minúsculas, letras acentuadas ou não são consideradas iguais), o que pode acontecer ?

R: Se você possui algum tipo de código em alguma tabela, onde você pode usar letras maiúsculas e minúsculas, mas elas devem ser tratadas como diferentes, uma Query feita selecionando o código “a1” pode trazer os códigos “a1” e “A1”.

R: Se existe algum campo na base de dados que grava um conteúdo codificado (como o ERP faz com os campos USERLGI e USERLGA), uma busca com uma Query vai considerar caracteres acentuados iguais aos não acentuados, minúsculas iguais a maiúsculas, e retornar dados a mais.

R: Mesmo que a ordenação retornada pelo banco de dados esteja linguisticamente correta, se eu REVALIDAR a ordem comparando a String retornada pelo banco de dados usando a comparação (binária) de strings no AdvPL, como por exemplo nome >= “JOAO” ( sem acento) .AND. nome <= ‘JOSÉ’ ( com acento), somente serão impressos os nomes JOAO (sem acento), JOSE (sem acento) e JOSÉ (com acento). JOÃO com acento não será considerado, pois é maior que “JOSÉ” na comparação binária. Se houver qualquer um destes nomes com letras minúsculas na base de dados, nenhum passa nessa comparação.

E, só pra ficar mais divertido , exitem diferenças de ordenação do mesmo collation entre bancos de dados diferentes. Independentemente do collation ser case sensitive ou não, accent sensitive ou não, a ordem de retorno pode mudar entre bancos de dados. Alguns bancos agrupam o retorno com as letras maiúsculas primeiro e depois as minúsculas, outros fazem o contrário — minúsculas primeiro –, outros agrupam as palavras acentuadas depois das não acentuadas, outros trazem as acentuadas próximas das não acentuadas. 

Conclusão

Para o AdvPL e o DBAccess, tanto faz o Collation usado no banco. Porém, a aplicação escrita na linguagem AdvPL que conta com um comportamento de comparação e ordenação binária teria que ser adequada e refatorada, e algumas funcionalidades da emulação ISAM — como o DBSEEK com SOFTSEEK ( posicione no primeiro registro imediatamente maior do que a chave informada caso esta não seja encontrada ) podem ter comportamento diferentes do esperado e diferentes entre bancos de dados.

Espero que tenham gostado, e desejo a todos TERABYTES DE SUCESSO !! 

Referências

 

 

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