Protheus no Linux – Parte 03

Introdução

No post anterior, instalamos na VM Ubuntu um Protheus Server, instância única, usando um c-Tree Server DLL (ou BoundServer). Agora, vamos instalar um banco de dados MYSQL e a UnixODBC nesta mesma VM.

Instalando

Após iniciar a VM do Ubuntu Linux — montada no primeiro post –, executamos os comandos abaixo, para instalar o MYSQL e a UNIXODBC, respectivamente:

sudo apt-get install mysql-server mysql-client
sudo apt-get install libmyodbc unixodbc-bin unixodbc

Com estes comandos, nesta versão do Sistema Operacional, o MySQL 5.5 será instalado. Durante a instalação, será perguntada uma senha do usuário “root” do MySQL. Insira uma senha e guarde-a, ela será necessária nas etapas posteriores.

Criando a base no MySQL

Após a instalação, o banco MYSQL já deve estar no ar, mas sem nenhuma base de dados. Utilize o comando “mysql” para acessar o interpretador de comandos do MySql, com a sintaxe abaixo:

mysql -u root -p

Uma vez dentro do interpretador de comandos do MySql, execute os comandos abaixo para criar a sua base de dados. Troque o conteúdo de ‘usuariodebanco’ para um nome de usuário que você queira criar no banco de dados para ter acesso a essa base. Você pode dar o mesmo nome do usuário que você criou para o sistema operacional. E, no lugar de ‘senha’, coloque a senha que você quer atribuir a este usuário, para ele acessar o Banco de Dados.

create database envp11mysql;
grant all on envp11mysql.* to 'usuariodebanco' identified by 'senha';

Mysql Create DB

Configurando a UnixODBC

Para não acessarmos diretamente a .so ( shared object library ) do banco de dados, vamos configurar a UnixODBC no Linux para o Mysql. Primeiro, vamos entrar em “root” mode no Linux, com o comando abaixo:

sudo su

Agora, vamos ver exatamente onde foram instaladas as libs ODBC do MySQL, usando o comando abaixo:

find / -name 'lib*odbc*.so'

O resultado esperado deve ser bem próximo de:

/usr/lib/x86_64-linux-gnu/odbc/libodbcdrvcfg2S.so
/usr/lib/x86_64-linux-gnu/odbc/libodbcnnS.so
/usr/lib/x86_64-linux-gnu/odbc/libodbcdrvcfg1S.so
/usr/lib/x86_64-linux-gnu/odbc/libodbctxtS.so
usr/lib/x86_64-linux-gnu/odbc/liboraodbcS.so
/usr/lib/x86_64-linux-gnu/odbc/libodbcmyS.so
/usr/lib/x86_64-linux-gnu/odbc/libmyodbc.so
/usr/lib/x86_64-linux-gnu/odbc/libodbcpsqlS.so
/usr/lib/x86_64-linux-gnu/odbc/liboplodbcS.so
/usr/lib/x86_64-linux-gnu/odbc/libodbcminiS.so

Os arquivos que nos interessam são os dois em destaque:

libmyodbc.so = MySQL Driver API 
libodbcmyS.so = MySQL Driver Setup

Agora, vamos criar a configuração de instalação da UnixODBC. Usando o editor de arquivos texto no Linux, crie o arquivo odbcinst.ini na pasta /etc/

sudo vi /etc/odbcinst.ini

O conteúdo do arquivo deve ser o seguinte:

[odbc_mysql]
Description     = ODBC for MySQL
Driver          = /usr/lib/x86_64-linux-gnu/odbc/libmyodbc.so
Setup           = /usr/lib/x86_64-linux-gnu/odbc/libodbcmyS.so
UsageCount      = 1

Agora, vamos ver a onde está a configuração de Sockets do MySQL, usando o comando abaixo:

mysqladmin -u root -p version

O resultado deve ser parecido com este aqui:

mysqladmin  Ver 8.42 Distrib 5.5.49, for debian-linux-gnu on x86_64
Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Server version          5.5.49-0ubuntu0.14.04.1
Protocol version        10
Connection              Localhost via UNIX socket
UNIX socket             /var/run/mysqld/mysqld.sock
Uptime:                 23 min 17 sec


Threads: 1  Questions: 583  Slow queries: 0  Opens: 189  
Flush tables: 1  Open tables: 41  Queries per second avg: 0.417

O que nos interessa é a informação em negrito: O caminho do Unix Socket em uso pelo Banco de Dados. Agora, baseado no que já temos até agora, vamos criar a configuração de ODBC efetivamente, apontando para os drivers.

vi /etc/odbc.ini

Podemos partir do seguinte conteúdo:

[envp11mysql]
Description = DSN para Banco P11 no MySQL
Driver      = odbc_mysql
Server      = localhost
Port        = 3306
Socket      = /var/run/mysqld/mysqld.sock
Database    = envp11mysql
Option      = 3
ReadOnly    = No

Na prática, os nomes das seções nos arquivos de configuração somos nós que definimos. No arquivo odbcinst.ini, cada seção configura um driver de banco diferente. No arquivo odbc.ini, cada seção corresponde a uma entrada de DSN da Odbc, que usa um determinado driver.

Após criar e editar os arquivos, vamos efetivar o registro das informações na UnixODBC, inicialmente instalando o Driver que configuramos, usando o comando abaixo.

odbcinst -i -d -f /etc/odbcinst.ini

Agora, vamos instalar a nossa configuração de DSN como “System DSN”, usando o comando abaixo:

odbcinst -i -s -l -f /etc/odbc.ini

Agora, usando o comando abaixo, podemos consultar quais DSN de sistemas estão instaladas:

odbcinst -s -q

E, finalmente, podemos testar a conexão com o MySQL via UnixODBC, usando o comando abaixo, trocando “MYSQLUSER” pelo usuário que nós criamos para o banco envp11mysql , e “MYSQLUSERPASSWORD” trocando pela senha utilizada.

isql -v myodbc_mysql_dsn MYSQLUSER MYSQLUSERPASSWORD

O resultado esperado é :

 

+---------------------------------------+
| Connected!                            |
|                                       |
| sql-statement                         |
| help [tablename]                      |
| quit                                  |
|                                       |
+---------------------------------------+
SQL>

 

Para sair do interpretador de comandos SQL, use a instrução “quit”. Com isso, já temos o MySQL instalado, um banco criado, e a UnixODBC devidamente configurada.

Referências

“Kafee Talk – How to setup and configure MySQL with unixODBC under Ubuntu 14.04”. Disponível em <http://www.kaffeetalk.de/how-to-setup-and-configure-mysql-with-unixodbc-under-ubuntu-14-04/>. Acesso em 16 de Julho de 2016.

Conclusão

Sim, foi mais simples e mais rápido do que imaginávamos. E, com mais um ou dois passos, você configura o MySQL no Linux para aceitar conexões TCP remotas, e pode instalar uma ODBC no Windows, e usar o MYSQL no Linux — Basta editar o arquivo /etc/mysql/my.cnf , localizar a configuração bind-address, e trocar ela para 0.0.0.0 😉 Mas, o foco é usarmos o MySQL no Linux, com o DBAccess também no Linux. E esta etapa será abordada no próximo post dese assunto !!!

Agradeço a todos pela audiência, e lhes desejo TERABYTES DE SUCESSO 😀 

Acesso a dados – DBAccess

Introdução

Nos posts anteriores dos tópicos de acesso a dados, abordamos o mecanismo ISAM e o mecanismo relacional, e vimos alguma coisa do DBAccess da TOTVS, o Gateway de acesso a dados relacionais usado pelo Protheus Server. Hoje vamos aprofundar um pouco o conhecimento sobre esse Gateway, na forma de um FAQ, com muitas perguntas bem cabeludas …rs… seguidas das respectivas respostas.

O que é o DBAccess ?

O DBAccess é um gateway de acesso a dados para bancos de dados relacionais, utilizado pelo Protheus Server, para fornecer uma camada de abstração de acesso a arquivos usando a abordagem ISAM em um SGDB relacional, e uma camada de acesso de consulta relacional através de Queries.

Por que o DBAccess foi construído ?

As versões de ERP da Microsiga, antes do Protheus, originalmente foram concebidos para acessar os formatos de dados nativos do Clipper, que eram DBFNTX e DBFCDX, onde a aplicação acessava diretamente os dados. Com a utilização do driver do ADS Client no Clipper, foi possível utilizar o ADS Server, um SGDB ISAM com arquitetura client-server para o armazenamento dos dados do ERP.

Com o surgimento e popularização dos Bancos de Dados relacionais para baixa plataforma, onde os SGDBs relacionais permitiam mais recursos, consultas mais fáceis e mais rápidas, e novas funcionalidades, além de estender a capacidade do acesso a dados ISAM/xBase, houve a necessidade da Microsiga criar um Gateway de acesso a dados relacionais, que permitisse a execução do código legado (escrito em uma abordagem ISAM) em um SGDB relacional, e permitir acesso a novas funcionalidades disponíveis apenas no ambiente relacional.

Onde entra o TOPConnect nisso ?

O TopConnect foi a primeira geração de Gateways de acesso a dados, sua primeira versão foi concebida antes do Protheus, através de uma parceria comercial com uma empresa estrangeira de desenvolvimento de software. Ela permitia aplicações em Clipper acessar bancos de dados DB2/400 (IBM AS400), Sybase, Microsoft SQL 6.5, Sybase, PostgreSQL, Informix, DB2 UDB, Oracle 6, e MySQL. O TOPConect era escrito em C, e para cada SGDB existia uma versão específica do Gateway.

A Microsiga optou por desenvolver uma tecnologia própria de acesso, sendo assim desenvolvido o TOPConnect 4. Usando C++ com uma abstração de acesso a dados, o mesmo executável possuía as implementações para usar qualquer um dos SGDBs homologados. Como o Gateway é atrelado a versão de produtos do ERP Microsiga, a partir do Protheus 10 ele passou a chamar-se DBACcess, incorporando as implementações para as novas versões dos SGDBs. Com a fundação da TOTVS, o DBAccess passou a ser responsabilidade do Depto de Tecnologia da TOTVS.

O DBAccess consome muito recurso ?

Como um Gateway de acesso a dados, seu consumo de recursos de Memória é diretamente proporcional ao número de conexões, versus o número total de tabelas e queries abertas, versus o tamanho de registro da tabela. Seu consumo de CPU e Rede é diretamente proporcional à quantidade de requisições realizadas pela aplicação AdvPL ao gateway. O consumo é relativamente pequeno por tabela aberta, mas considerando um ambiente de 3000 conexões, onde cada pode abrir e manter abertas mais de 100 tabelas e queries, o consumo de memória pode atingir ou ultrapassar 4GB de RAM.

Como o DBAccess acessa o SGDB ?

O Acesso ao banco Oracle é feito via OCI (Oracle Client Interface), e todos os demais bancos são acessados via ODBC, usando a interface ODBC fornecida pelo fabricante do SGDB. É necessário que a ODBC ou OCI do SGDB em questão esteja instalada no equipamento onde o DBAccess será utilizado.

Por que o DBAccess não têm balanceamento de carga ?

Bem, como o acesso aos dados é feito apenas pelo SGDB, ele sempre será o destino final de todas as conexões. Então, não faz muito sentido “balancear” as conexões do DBAccess. Porém, em ambientes com mais de 2 mil conexões, pode ser muito interessante você usar mais de uma instância de DBAccess em máquinas distintas. Esta topologia é chamada de “DBAccess Distribuído”. Nesta topologia, um ou mais serviços de Protheus apontam para um DBAccess, e todos os DBACcess devem usar uma ODBC apontando para um único SGDB, além de cada DBAccess apontar para uma instância única, nomeada de “master”, que exerce o papel de servidor de locks de registro e locks virtuais.

O que acontece se eu colocar 2 DBAccess apontando para o mesmo Database ?

Se eles não estiverem configurados na topologia de “DBAccess distribuído”, cada um deles vai olhar para o seu próprio controle de emulação de Lock ISAM de registros. Neste caso, pode haver invasão de lock (duas conexões de instâncias distintas vão conseguir fazer RecLock() no mesmo registro), o que pode levar a quebra de integridade dos dados do registro, pois somente a última atualização irá prevalecer), e pode causar DeadLock em transações no SGDB.

Por que o DBAccess não usa o Lock do Banco de Dados ?

O mecanismo de bloqueio de registros do SGDB é de uso intrínseco do Banco de Dados, e não oferece a flexibilidade exigida para o mecanismo de acesso ISAM emulado. Mesmo se fosse construído um mecanismo de emulação direta no SGBD, a quantidade de IOs e instruções para emular isso no SGDB inivabilizariam seu uso por questões de desempenho. O mecanismo de Locks ISAM e Locks Virtuais é feito na memória do DBAccess, de forma muito rápida e eficiente.

Por quê existem os campos R_E_C_N_O_ e D_E_L_E_T_ ?

Como o TOPConnect surgiu devido a necessidade de executar um código escrito originalmente para engine ISAM, precisamos destes campos para emular o comportamento original do ISAM: Uma coluna interna para registrar a numeração sequencial de inserção (ordem física e identificador único de registro), e o campo “D_E_L_E_T” para marcar os registros que foram “marcados para deleção permanente” através da função DbDelete().

E por quê existe a coluna R_E_C_D_E_L_ em algumas tabelas ?

Quando foi implementado o conceito de criação de índice único nas tabelas do ERP, no Protheus 8 se eu não me engano, caso a tabela possua um índice de chave única definido pelo ERP, eu não posso ter um registro “ativo” na base com a mesma chave única. Porém, como as tabelas do DBAccess trabalham com o conceito de deleção “lógica” de registros, marcando os registros a serem eliminados fisicamente usando a função DbDelete(), eu posso ter um ou mais registros marcados para deleção, com a mesma chave única. Por exemplo, eu crio a tabela TESTE com a coluna CHAVE, e crio um índice de chave única com a coluna CHAVE. Se eu usar apenas esta coluna na minha chave única, se eu inserir um registro “000001”, depois deletá-lo ( campo D_E_L_E_T_ está com um “*”), e depois tentar inserir outro registro com “000001”, o SGDB não vai deixar …

Então, quando o ERP solicita ao DBAccess a criação de um índice de chave única, o DBAccess acrescenta na tabela a coluna de controle “R_E_C_D_E_L_”, coloca ela como último campo da chave única, e as colunas R_E_C_D_E_L_ de todos os registros marcados para deleção ( D_E_L_E_T_ = ‘*’) são alimentadas com o conteúdo do R_E_C_N_O_, e todos os registros não marcados para deleção ( ativos ) ficam com este campo ZERADO. Deste modo, eu posso ter um e apenas um registro não marcado para deleção com uma determinada chave única, mas eu posso ter um ou mais registros com o campo chave com este mesmo valor, caso eles estejam marcados para deleção.

Cada vez que o Protheus pede ao DBAccess para marcar um registro como “Deletado” através da função DbDelete(), o DBAcccess verifica se a tabela possui a coluna R_E_C_D_E_L_ , e atualiza ao mesmo tempo a coluna “D_E_L_E_T_” com ‘*’ e o R_E_C_D_E_L_ com o R_E_C_N_O_. Caso um registro seja recuperado, isto é, seja removida a marca de deleção, usando a função DbRecall(), a coluna R_E_C_D_E_L_ é atualizada para “0” zero. Deste modo, se você tentar inserir um registro ativo com uma chave duplicada, o SGDB não deixa fazer a inserção, e se você tentar desmarcar um registro deletado que tenha uma chave, e já existir um registro ativo (não marcado para deleção) com a mesma chave, o SGDB não permite a recuperação do registro, pois isto viola a chave única definida.

O que é mais rápido : Uma consulta ISAM ou por QUERY ?

Normalmente o acesso por Queries é mais rápido, sendo visivelmente mais rápido em leituras de registros sequencialmente. Um acesso de leitura no resultset de uma Query reflete os dados obtidos no momento que a Query foi executada. Já a leitura por acesso ISAM emulado retorna cada registro sob demanda, no momento que o registro é posicionado. A consulta por Query permite JOINS, para busca de dados de tabelas relacionadas. NO ISAM, você é obrigado a posicionar manualmente cada tabela relacionada e realizar a busca sob demanda.

Verdade que o DBSeek() é lento no DBAccess ?

Um DBSeek() no DBAccess procura posicionar no primeiro registro da ordem informada cujos campos que compõe a ordem do índice em uso que foram informados na instrução atendam a condição de busca. Se você faz um DBSeek() para posicionar no primeiro registro imediatamente superior a ordem desejada ( ou “SoftSeek” ) , DBSeek( cChave , .T. ) … o DBAccess pode submeter internamente várias Queries ao SGDB até achar o registro que satisfaça esta condição. Trocar um DBSeek() simples, que apenas verifica se um registro existe ou não, e trocar isso por uma Query, fatalmente vai ser mais lento, pois a abertua de uma Query para pegar apenas um registro vai usar 3 IOs, enquanto o DBSeek() faz apenas um. No caso da busca pelo último registro de uma determinada chave, uma Query pode ser mais rápida. Por exemplo, para saber qual foi a última data de um determinado evento, onde a tabela é indexada pelo código do evento mais a data, ao invés de pegar o código do evento, acrescentar uma unidade, procurar pelo primeiro registro da próxima sequência com uma chave parcial e SoftSeek, e depois fazer um Skip(-1) — coisa que é muito rápida e comum de ser feita no DBF — podemos simplesmente fazer uma query com “select max(datadoevento) as ultdata from tabela where codigodoevento = ‘xxxxxx’ and D_E_L_E_T != ‘*'”

E as inserções e Updates, são rápidas ?

Ao comparamos inserções via instrução direta “INSERT” e a inserção via DBAppend() — modo ISAM — , ambas são muito rápidas. A inserção tradicional de registros pelo AdvPL, usando DBAppend() e replace(s), possui um mecanismo de otimização que prioriza o envio das informações de inserção em apenas um evento de I/O. Porém, dependendo do que é executado durante a atribuição dos campos, enquanto o registro atual está em estado de inserção, o Protheus Server faz um “Flush” da inserção parcial, com os dados disponíveis até aquele momento, podendo executar mais I/Os de update para o mesmo registro enquanto a inserção não for finalizada. Este assunto será mais detalhado em um tópico específico de dicas de desempenho (Acelerando o AdvPL)

O que é o erro -35 ?

O código -35 é um código de erro genérico retornado pelo DBAccess quando a conexão com o SDGB não foi bem sucedida, OU quando a conexão não atendeu aos requisitos de operação com o SGDB. Para saber o que realmente causou o -35, o monitor do DBAccess deve ser acessado, e o registro de eventos de erro do DBAccess deve ser verificado. Pode ser desde o SGDB estar fora do ar, ou usuário e senha não configurados corretamente no DBAccess, falha de criação ou verificação de alguma tabela interna de controle do DBAccess, etc.

O que é o erro -2 ?

O erro -2 indica uma interrupção inesperada da conexão entre o Protheus Server e o DBAccess. Da mesma forma que o erro -35, o -2 pode ter diversas causas, desde um problema de rede entre o Protheus Server e o DBAccesse, ou entre o DBAccess e o SGDB, até um término anormal do processo que estava atendendo esta conexão no DBAccess (Assert Exception, Out Of Memory, Access Violation). Deve ser verificado o LOG do DBAccess para ver os detalhes do que aconteceu.

E os demais erros ?

Cada um possui um significado geral, para um tipo de operação que não foi bem sucedida. O código do erro apenas informa que uma operação falhou, porém somente descobriremos a causa efetiva da falha olhando o log de registro de erros do DBAccess. A lista de códigos de erro do DBAccess está na TDN, no link (http://tdn.totvs.com/pages/viewpage.action?pageId=6064500).

O DBAccess faz “leitura suja” de registros ?

Sim, para a grande maioria dos bancos. Devido a natureza das transações do SGDB, e da necessidade do comportamento esperado do AdvPL em ter acesso de leitura a qualquer registro sem espera ou bloqueio, mesmo que o dado esteja sendo alterado dentro de uma transação por outro processo, foi necessário definir explicitamente o nível de isolamento “READ UNCOMMITED”. O Banco Oracle não tem a possibilidade de leitura “suja”, ele é endereçado com “Read Commited”, mas ele permite que as demais conexões acessem a última versão committed, mesmo que exista uma transação aberta atualizando aquele dado.

O DBAccess “prende” conexões ?

Uma conexão feita pelo Protheus ao DBAccess faz o DBAccess abrir uma conexão no SGDB. Uma vez aberta, boa parte do tempo a conexão no DBAccess fica aberta, esperando o Protheus pedir alguma coisa ao DBAccess, como por exemplo: abrir uma tabela, uma query, posicionar em um registro, inserir um registro, executar uma Stored Procedure, etc. Ao receber uma requisição de abertura de Query, por exemplo, o DBAccess solicita ao SGDB a abertura da Query, e aguarda do SGDB um retorno ( sucesso ou erro). Enquanto isso, o Protheus fica esperando o DBAccess retornar. A Aplicação AdvPL estabelece a conexão com o DBAccess, e pode encerrar a conexão durante a execução da aplicação, e mesmo que o programa não desconecte, quando a Thread da aplicação AdvPL terminar, qualquer conexão com o DBAccess que tenha sido deixada aberta é encerrada. Existem algumas condições que o SGDB pode demorar a responder, como por exemplo um DeadLock no SGDB, a abertura de uma query muito complexa sobre um número muito grande de tabelas, a execução de uma Stored Procedure que fará muito processamento, etc.

Existe uma condição onde uma conexão pode ficar aberta no DBAccess, dando a impressão de estar “presa”: Caso a thread que está executando uma aplicação AdvPL, que consequentemente conectou no DBAccess, seja interrompida por uma ocorrência crítica de erro, como um Access Violation ou Segment Fault (Invasão de memória), e ocorra falha no destrutor da thread. O Processo em si não está mais no ar, mas a conexão permanecera aberta até que o serviço do Protheus seja finalizado.

E, existe também uma última condição, que pode fazer uma conexão no DBAccess permanecer aberta indefinidamente — ou até que o DBAccess seja finalizado, ou a conexão seja encerrada pelo DBAccess Monitor: Caso exista algum problema de rede entre a aplicação Protheus e o DBAccess, e ocorra uma queda na conexão TCP, quando o DBAccess está “IDLE”, esperando pelo Protheus pedir alguma coisa. Se a informação da perda da conexão não chegar ao Socket do DBAccess, ele vai ficar esperando indefinidamente, mantendo a conexão aberta e os recursos pertinentes alocados.

Existem estudos em andamento para criar novas funcionalidades na ferramenta, como permitir derrubar uma conexão do SGDB através do DBAccess, cancelando uma Query ou Stored Procedure, entre outros que eu não posso comentar agora …rs… Aguardem as próximas versões … 😀

Como o DBAccess encerra uma conexão pelo DBAccess Monitor ?

Um processo de conexão entre o DBAccess e um SGDB é mantido no ar enquanto existe a conexão entre o Protheus e o DBAccess. Logo, o DBAccess fica em um laço de espera por requisições. A cada intervalo de alguns segundos que o Protheus não envia nenhuma requisição, e a cada requisição recebida, ele verifica um flag de controle de processo, que indica se o DBAccess Monitor pediu para aquela conexão ser encerrada. Quando você pede ao DBAccess Monitor para encerrar uma conexão, ele apenas seta este flag no processo, que somente será considerado e avaliado quando o DBAccess está esperando por uma requisição do Protheus. Se  DBAccess pediu pro banco a execução de uma Stored PRocedure, que pode demorar de segundos a minutos, e neste meio tempo você pedir para a conexão ser encerrada através do DBAccess Monitor, ele somente vai encerrar a conexão com o SGDB quando a procedure terminar, e ele voltar ao loop de requisições. Se por um acaso você quer que a procedure ou query que está demorando seja interrompida, somente um DBA com acesso ao SGDB consegue derrubar o processo usando algum mecanismo de administração do SGDB.

E se eu derrubar o processo no SGDB ?

Se o DBAccess está executando algo no SGDB, e aguardando por um retorno, e o processo for finalizado no SGDB, o DBAccess receberá um retorno de erro, e repassa ao Protheus. Se o DBAcccess não estava fazendo nada no SGDB, e você derruba a conexão no SGDB …. O DBaccess somente vai “perceber” que a conexão foi pro espaço quando o PRotheus pedir alguma coisa, e o DBAccess for tentar pedir algo para o SGDB através da conexão que não existe mais …

Por que o DBAccess não traz campos MEMO em Query ?

Um campo MEMO pode conter muito mais dados em um campo do que em muitos registros. Uma tabela DBF por padrão traz todos os campos do registro atualmente posicionado a cada cada DbSkip() ou DBSeek(). Para ganhar desempenho e não onerar os processos do sistema que fazem leituras sequenciais em processamentos, os campos MEMO são trafegados somente sob demanda, quando abrimos a tabela em modo ISAM emulado, e somente quando a aplicação tenta ler ou acessar um campo Memo do registro atualmente posicionado.

Como os campos MEMO foram implementados no DBAccess para garantir a compatibilidade com o campo Memo do DBF, a forma de armazenar esta informação em cada SGDB é escolhida diretamente pelo DBAccess, a critério dele. Por questões de economia e otimização de recursos (estamos falando da aplicação que nasceu em um tempo onde uma rede 100 MBits era um “luxo”), foi decidido não retornar campos MEMO em Queries, o DBAccess não faz o Bind dos dados de campos usados como “Memo”. Por isso, se hoje você precisa da informação de um ou mais campos memo de uma tabela, você pode selecionar os dados necessários por Query, porém você recupera também o número do registro (R_E_C_N_O_) na Query, usando um outro nome para este campo, e mantendo a tabela original aberta em modo ISAM emulado, você posiciona no registro desejado usando DbGoto(), e então faz um Fieldget() do campo MEMO.

Quem alimenta o R_E_C_N_O_ da tabela na inserção de registros ?

O DBAccess possui um cache das estruturas de colunas e índices das tabelas, alimentado sob demanda, e um controle de numeração e locks. Ele guarda o número do ultimo registro inserido em uma lista em memória, e cada nova inserção incrementa o último registro da tabela na lista. Este processo é muito rápido, porém torna algo nada prático você fazer inserções através de uma Query ou Stored Procedure. Foi criado um mecanismo para permitir o DBAccess criar uma tabela com numeração automática de R_E_C_N_O_ pelo SGDB, disponibilizado para o FrameWork AdvPl, onde alguns novos módulos desenvolvidos no ERP Microsiga já se utilizam desta funcionalidade. Porém, isto ainda não é extensível para as demais tabelas dos módulos do ERP por questões de impacto. Todas as rotinas hoje escritas que alimentam tabelas, por exemplo via Stored Procedure, precisariam ser refatoradas para contemplar esta funcionalidade, pois nenhuma delas poderia mais fornecer um número de R_E_C_N_O_ ao fazer inserção na tabela, e imediatamente após a inserção, algumas delas precisam obter do SGDB qual foi o número do registro inserido.

Não é algo simples de ser feito, ainda mais “de uma vez”. Para cada SGDB o DBAccess define uma forma de criar o campo com auto-incremento, algumas usando um tipo de campo de auto-incremento do próprio SGDB, outas através de gatilhos criados internamente pelo DBAccess no momento de criação da tabela com estas características.

Para que serve a tabela TOP_FIELD, criada pelo DBAccess ?

Como a aplicação foi feita para emular ISAM, no momento da criação da tabela pelo Protheus, a estrutura da tabela é informada ISAM/DBF, onde especificamos campos do tipo “C” Caractere, “N” numérico (com precisão inteira ou decimal), “D” Data, “L” Lógico e “M” Memo. Porém, como o DBAccess escolhe cada tipo de campo que se adéqua melhor a necessidade, ele precisa guardar algumas definições que o Protheus forneceu na criação da tabela. E ele faz isso na tabenla TOP_FIELD.  Se você copia uma tabela diretamente de um Ddatabase para outro, no mesmo banco, mas não copia as definições da TOP_FIELD, todos os campos “D” data serão mostrados como “C” Caractere de 8 bytes, cmapos “L” lógicos serão “C” caractere de 1 byte, contendo “T” ou “F”, e todos os campos numéricos vão vir com uma precisão de 15 dígitos com 8 decimais.

Por quê as tabelas do DBAccess usam constraints DEFAULT ?

Não existe o valor “NULL” nas bases ISAM usadas pelo ERP Microsiga. Logo, mesmo que o campo esteja vazio, ele precisa ter o seu conteúdo default ( caracteres em branco, números com 0, data com string em branco, booleano com “F”). A aplicação conta com este comportamento, e as queries e joins foram construídas baseadas nesta premissa.

Como o DBAccess faz alteração estrutural na tabela ?

Através da função TC_Alter(), o DBAccess recebe a estrutura atual da tabela e a nova estrutura desejada, e determina para cada SGDB a sequência de operações necessárias para ajustar a tabela para ela ficar com a definição da nova estrutura, através do cruzamento das estruturas. Campos existentes na estrutura antiga e não existentes na nova são removidos, campos existentes na nova e não existentes na antiga são criados, e campos existentes nas duas podem ter suas características alteradas. Apenas as trocas de tipo de “C” Caractere para “N” numérico e vice-versa suportam manter os dados nos campos.

Como funciona a transação no DBAccess ?

Cada SGSB homologado possui transacionamento atômico por instrução. Isto significa que, caso seja disparado uma execução SQL de um Update que afete várias linhas, se uma não pode ser alterada, nenhuma será. Quando precisamos garantir que várias operações em um bloco sejam completas em conjunto, usamos as instruções BEGIN TRANSACTION e END TRANSACTION do Advpl, onde todas as instruções executadas dentro deste bloco não vão fazer COMMIT das informações no SGDB, isto será feito apenas no END TRANSACTION. Se ocorrer algum erro durante o processo, entre o Begin e o End transaction, todas as operações feitas a partir do BEGIN TRANSACTION serão descartadas. Por baixo destas instruções existe uma implementação que depende do ambiente ERP Microsiga, isto é, o processo em execução precisa ser um programa do ERP chamado a partir do Menu, ou um Job que faça a inicialização do ambiente ERP usando por exemplo o comando PREPARE ENVIRONMENT ou a função RcpSetEnv(). Estes tratamentos também estão atrelados às funções RecLock() e MsUnlock() do Framework AdvPL do ERP Microsiga.

O DBAccess pode conectar com outros SGDBs ?

Sim, ele pode. Porém, esta conexão é feita via uma conexão ODBC genérica, que não permite a emulação ISAM. Praticamente qualquer ODBC que você possa registrar como fonte de dados de ODBC no Windows pode ser acessada. Usando a build mais atual do DBAccess, existe uma aba de configuração de ODBC genérica. Você pode estabelecer a conexão usando a função TClink(), informando o banco “ODBC/” mais o alias da fonte de dados cadastrada no Gerenciador de Fontes ODBC do sistema operacional. Com esta conexão, voce pode abrir Queries, que devem ser montadas de acordo com a capacidade e regras da ODBC utilzada, onde os dados retornados podem ser char/varchar ou numéricos, usando DbUseArea() com TcGenWry(), e pode executar instruções diretamente no SGDB através da função AdvPL TcSqlExec(). Isto pode ser muito útil para realizar integrações com outras fontes de dados.

Conclusão

Eu acho que com estes parágrafos, dá pra matar um pouco a curiosidade sobre o DBAccess e seu papel no acesso a dados do ERP Microsiga. Caso algúem tenha mais alguma pergunta a acrescentar sobre este assunto, insira a sua pergunta como um comentário deste post 😀 PAra dúvidas e sugestões de outros assuntos, me envie um e-mail com o assunto “BLOG” para siga0984@gmail.com 😀

Novamente, agradeço a audiência, e desejo a todos TERABYTES de sucesso 😉

Até o próximo post, pessoal 😀

Escalabilidade e Performance – Stored Procedures

Introdução

Em um tópico anterior sobre “Escalabilidade e performance – Técnicas”, um dos tópicos falava sobre Stored Procedures, inclusive sugerindo que seu uso deveria ser minimizado. Vamos entrar neste tema com um pouco mais de profundidade neste tópico. Vamos começar com o clone do tópico abordado, e esmiuçar ele dentro do contexto do AdvPL e Protheus.

Minimize o uso de Stored Procedures

Este é um ponto aberto a discussão, depende muito de cada caso. Não é uma regra geral, existem pontos em um sistema onde uma stored procedure realmente faz diferença, mas seu uso excessivo ou como regra geral para tudo impõe outros custos e consequências. O Princípio 1 diria: “use apenas stored procedures”. No entanto, esta decisão pode causar grandes problemas para o Princípio 2 devido à escalabilidade. As Stored procedures têm a vantagem de ser pré-compiladas, e não há nada mais perto de um dado no Banco de Dados.

Porém Bancos de Dados transacionais são especializados em quatro funções: Sort, Merge, gerência de Locks e Log. A gerência de lock é uma das tarefas mais críticas para a implementação de algoritmos distribuídos, e este é o real motivo de existirem poucos Bancos de Dados que possam implementar a escalabilidade horizontal. Se as máquinas de Banco de Dados têm dificuldade de escalar horizontalmente, ela é um recurso escasso e precioso. Temos então que otimizar seu uso para não consumir excessivamente seus recursos a ponto de onerar os demais processos do ambiente. Isto acaba adiando a necessidade de escalar horizontalmente o SGBD.

Abordando a questão de desempenho

Se o algoritmo para processamento de um grande grupo de informações pode ser escrito dentro de uma Stored Procedure no próprio Banco de Dados, esta alternativa tende fortemente a ser a mais performática. Num cenário onde o algoritmo é escrito usando um programa sendo executado dentro do servidor de aplicação da linguagem, cada processamento que dependa da leitura de grupos de dados e tenha como resultado a geração de novos dados vai ser onerado pelo tempo de rede de tráfego destes dados, na ida e na volta. Logo, com uma base de dados modelada adequadamente, e uma stored procedure bem construída, ela naturalmente será mais rápida do que um processamento que precisa trafegar os dados pra fora do SGDB e depois receba novos dados de fora.

Porém, este recurso não deve ser usado como solução mágica para tudo. Afinal, o SGDB vai processar uma Stored Procedure mais rápido, pois ele não vai esperar um processamento ser realizado “fora dele”, porém o SGDB vai arcar com o custo de ler, processar e gravar a nova informação gerada. Se isto for feito sem critério, você pode mais facilmente esgotar os recursos computacionais do Banco de Dados, ao ponto da execução concorrente de Stored Procedures afetar o desempenho das demais requisições da aplicação.

Outras técnicas pra não esgotar o SGDB

Existem alternativas de adiar um upgrade no SGDB, inclusive em alguns casos as alternativas são a solução para você não precisar comprar um computador da “Nasa” …risos… Normalmente estas alternativas envolvem algum tipo de alteração na aplicação que consome o SGDB.

Réplicas de leitura

Alguns SGDBs permitem criar nativamente réplicas da base de dados acessadas apenas para consulta, onde as cópias de leitura são sincronizadas em requisições assíncronas. Existem muitas partes da aplicação que podem fazer uma leitura “suja”. Neste caso, a aplicação pode ler os dados de uma base sincronizada para leitura, e os processos que precisam de leitura limpa são executados apenas na instância principal. Para isso a aplicação precisaria saber qual e o banco “quente” e qual é o espelho, para fazer as coisas nos lugares certos.

Caches

Outra alternativa é a utilização de caches especialistas, implementados na própria aplicação. Utilizando por exemplo uma instância de um “MemCacheDB” em cada servidor, cada aplicação que pode reaproveitar a leitura de um dado com baixo índice de volatilidade (dados pouco atualizados ou atualizados em momentos específicos), poderiam primeiro consultar o cache, e somente se o cache não têm a informação desejada, a aplicação acessa o banco e popula o cache, definindo uma data de validade. Neste caso, o mais legal a fazer é definir um tempo de validade do cache (Expiration Time). E, paralelo a isso, para informações de baixa volatilidade, a rotina que fizer update desta informação pode eliminar ela do cache no momento que um update for realizado, ou melhor ainda, pode ver se ela se encontra no cache, e em caso afirmativo, ela mesma poderia atualizar o cache 😉

Sequenciamento de operações

Operações de inserção ou atualização de dados que não precisam ser refletidas em real-time no SGDB podem ser enfileiradas em pilhas de requisições, e processadas por um processo dedicado. O enfileiramento de requisições não essenciais em tempo real limita o consumo de recursos para uma determinada atividade. Caso a pilha se torne muito grande, ou um determinado processo dependa do esvaziamento total da pilha, podem ser colocados mais processos para ajudar a desempilhar, consumindo mais recursos apenas quando estritamente necessário.

Escalabilidade Vertical

Devido a esta questão de dificuldade de escalabilidade de bancos relacionais horizontalmente, normalmente recorremos a escalabilidade vertical. Escalamos horizontalmente as máquinas de processamento, colocando mais máquinas menores no cluster e balanceando carga e conexões, e quando a máquina de banco começa a “sentar”, coloca-se uma máquina maior só para o SGDB, com vários processadores, discos, memória e placas de rede. Mas tudo tem um limite, e quando ele for atingido, a sua máquina de Banco de Dados pode ficar mais cara que o seu parque de servidores de processamento.

Dificuldade de Implementação

Usar caches e réplicas e pilhas não é uma tarefa simples, fatores como a própria modelagem da base de dados podem interferir negativamente em algumas destas abordagens. Não se pode colocar tudo em cache, senão não vai ter memória que aguente. O cache é aconselhável para blocos de informações repetidas constantemente requisitadas, e de baixa volatilidade. Também não é necessário criar pilhas para tudo que é requisição, apenas aquelas que não são essenciais em tempo real, e que podem ter um delay em sua efetivação.

Stored Procedures no AdvPL

O ERP Microsiga disponibiliza um pacote de Stored Proecures, aplicadas no SGDB em uso por um programa do módulo “Configurador” (SIGACFG). As procedures foram desenvolvidas para funcionalidades específicas dentro de cada módulo, normalmente aquelas que lidam com grandes volumes de dados, e foi possível criar um algoritmo que realize o processamento dentro do SGDB, trafegando menos dados “pra fora” do Banco de Dados. Normalmente um pacote de procedures é “casado” com a versão dos fontes do Repositório, pois uma alteração na aplicação pode envolver uma alteração na procedure. Os ganhos de performance são nítidos em determinados processamentos, justamente por eliminar uma boa parte do tráfego de informações para fora do SGDB durante os processos.

Conclusão

Dado o SGDB como um recurso “caro e precioso”, como mencionado anteriormente, a utilização de recursos adicionais como réplicas e caches, ajuda a dar mais “fôlego” pro SGDB, você consegue aumentar o seu parque de máquinas e volume de dados processados sem ter que investir proporcionalmente na escalabilidade do SGDB. E em tempos de “cloudificação” , SaaS e IaaS, quando mais conseguimos aproveitar o poder computacional que temos em mãos, melhor !

Desejo novamente a todos TERABYTES de Sucesso 😀

Até o próximo post, pessoal 😉

Referências

“Escalabilidade e performance – Técnicas”

Imagens no SGDB via DBAccess

Introdução

Recebi um e-mail, ou mensagem, ou post (não lembro agora) com uma sugestão interessante, para este tema (Imagens no SGDB) fosse abordado aqui no Blog. E, é totalmente possível de ser feito, de forma relativamente simples.

Imagens, por dentro

Um arquivo em disco que contém uma imagem pode ser salvo em diversos formatos: BMP (Bitmap Image File), JPEG (Joint Photographic Experts Group), PNG (Portable Network Graphics), entre outros. Cada formato consiste em uma especificação para a representação binária de uma imagem. Trocando em miúdos, um arquivo de imagem contém um determinado número de bytes, usando códigos ASCII de 0 a 255 (conteúdo binário), que são interpretados por uma aplicação capaz de mostrar seu conteúdo em uma interface. Cada tipo de imagem possui algumas características, como resolução, compressão, mas por dentro são apenas uma sequência de bytes.

Imagens no SGDB

Existem bancos de dados que possuem um tipo de campo próprio para imagens, como o Microsoft SQL Server (campo image), mas a grosso modo praticamente todos os bancos de dados comerciais possuem um tipo de campo conhecido por “BLOB” (Binary Large OBject), capaz de suportar conteúdo binário.

Acesso pelo DBAccess

Como é de conhecimento de todos que trabalham com o ERP Microsiga, todo o accesso a Banco de Dados relacional no Protheus é feito através do DBAccess, um gateway de acesso para bancos relacionais, que também é capaz de emular o acesso ISAM, ainda usado por boa parte do código legado do ERP.

O DBAccess não permite acesso direto a campos IMAGE, BLOB ou CLOB, mas internamente ele se utiliza destes campos para emular o campo do tipo “M” memo do AdvPL. Logo, para nos utilizarmos destes tipos de campo, devemos criar uma tabela no SGDB usando o tipo de campo “M” (Memo) do AdvPL.

Atenção, no ERP existe o conceito de campo Memo virtual, criado no dicionário de dados do ERP (SX3), que na prática utiliza um arquivo auxiliar (SYP) na Base de Dados principal, com acesso através de uma API Advpl, ao qual esse exemplo não se aplica. O campo Memo que será criado é um Memo “real” no SGDB.

Características e Limites

O AdvPL possui um limite de 1MB de tamanho máximo de String, logo ainda não é possível armazenar no SGDB uma imagem maior que isso. E, como o acesso ao conteúdo do campo é feito pelo DBAccess, não é possível fazer uma Query que recupere diretamente o conteúdo de um campo BLOB, CLOB ou IMAGE.

Para acessar o conteúdo de um campo “M” Memo criado em uma tabela, devemos abrir a tabela no AdvPL usando DbUseArea() — ou ChkFile(), para uma tabela de dados do ERP –, posicionar no registro desejado e ler o valor do campo do registro atual através da expressão cVARIAVEL := ALIAS->CAMPOMEMO, e o DBAccess irá fazer uma requisição exclusiva para trazer o conteúdo deste campo e colocá-lo na variável de memória.

Adicionalmente, o campo “M” Memo originalmente no AdvPL foi projetado para suportar apenas 64 KB de dados, e somente conseguimos aumentar esse limite para 1MB habilitando a configuração TOPMEMOMEGA=1 na configuração do environment desejado no arquivo de configuração do TOTVS Application Server (appserver.ini) — Vide TDN, no link http://tdn.totvs.com/pages/viewpage.action?pageId=6065746 )

Como as imagens gravadas costumam ser bem maiores que os registros gravados em tabelas de dados do ERP, deve-se tomar cuidado quando a leitura destes campos for realizada por muitos processos simultaneamente, isto pode gerar um gargalo na camada de rede entre as aplicações TOTVS Application Server, DBACcess e o SGDB.

E, existem alguns bancos de dados homologados que por default não se utilizam de campos BLOB ou similares para armazenar os dados de campo “M” Memo. Para ter certeza que a implementação vai funcionar em todos os bancos homologados, podemos limitar o tamanho da imagem em 745 KB, e converter o buffer binário da imágem para BASE64, onde são usadas strings de texto normal para a representação dos dados, e fazer as conversões em memória para Ler e Gravar o buffer binário.

Mãos à obra

Basicamente, armazenar uma imagem no SGDB requer no mínimo 2 campos na tabela de imagens: Um campo caractere, identificador único da imagem, indexado, e um campo “M” Memo do AdvPL, para armazenar a imagem. Podemos encapsular isso em uma classe — vamos chamá-la ApDbImage() — e implementar os métodos de leitura e gravação, manutenção e status, além de dois métodos adicionais para ler imagens de um arquivo no disco para a memória, e gravar a imagem da memória para o disco.

A classe APDBIMAGE() foi implementada com este propósito, mas ela têm ainda alguns detalhes adicionais, ela guarda um HASH MD5 gerado a partir da imagem original, um campo separado para o tipo da imagem, e permite as operações básicas de inclusão, leitura, alteração e exclusão.

Exemplo de uso e fontes

O programa de exemplo funciona como um manager simples de imagens, permitindo abrir os formatos suportados de imagens do disco, para serem mostrados na tela, ou do próprio repositório, ou também da tabela de imagens do Banco de Dados. Uma vez visualizada uma imagem na interface, ela pode ser gravada em disco (ou exportada), no mesmo formato que foi aberto — o programa não realiza conversões — , e também pode ser inserida no DBimage com um nome identificador qualquer, ou usada para alterar uma imagem já existente na tabela de imagens.

O fonte da classe APDBImage() pode ser baixado no link https://github.com/siga0984/Blog/blob/master/ApDBImage.prw , e o fonte de exemplo que usa a classe está no link https://github.com/siga0984/Blog/blob/master/TSTDBIMG.prw , ambos no GitHub do Blog ( https://github.com/siga0984/blog ). Basta baixar os fontes, compilá-los com o IDE ou o TDS, e executar a função U_TSTDBIMG para acionar o programa de testes da classe ApDbImage().

E, para os curiosos e ávidos por código, segue o fonte da Classe APDBIMAGE logo abaixo:

#include "protheus.ch"
/* ---------------------------------------------------
Classe ApDBImage
Autor Júlio Wittwer
Data 27/02/2015 
Versão 1.150308
Descrição Classe para encapsular leitura e gravação de 
 imagens em tabela do SGDB através do DBACCESS
Observação
Como apenas o banco MSSQL aceita conteúdo binário ( ASCII 0 a 255 )
para campos MEMO, e os bancos ORACLE e DB2 ( quando usado BLOB ), 
para servir para todos os bancos, a imagem é gravada no banco 
usando Encode64 -- para converter conteúdo binário em Texto 
codificado em Base64, a maior imagem nao pode ter mais de 745000 bytes
Referências
http://tdn.totvs.com/display/tec/Acesso+ao+banco+de+dados+via+DBAccess
http://tdn.totvs.com/pages/viewpage.action?pageId=6063692
http://tdn.totvs.com/display/tec/Encode64
http://tdn.totvs.com/display/tec/Decode64
--------------------------------------------------- */
#define MAX_IMAGE_SIZE 745000
CLASS APDBIMAGE
// Propriedades
 DATA bOpened 
 DATA cError
// Métodos 
 METHOD New() 
 METHOD Open()
 METHOD Close() 
 METHOD ReadStr( cImgId , /* @ */ cImgType , /* @ */ cImgBuffer ) 
 METHOD Insert( cImgId , cImgType , /* @ */ cImgBuffer ) 
 METHOD Update( cImgId , cImgType , /* @ */ cImgBuffer ) 
 METHOD Delete( cImgId ) 
 METHOD Status()
// Metodos de acesso de imagens no disco
 METHOD LoadFrom( cFile, cImgBuffer )
 METHOD SaveTo( cFile, cImgBuffer )
 
ENDCLASS
/* ---------------------------------------------------------
Construtor da classe de Imagens no SGDB
Apenas inicializa propriedades
-------------------------------------------------------- */
METHOD New() CLASS APDBIMAGE
::bOpened := .F.
::cError := ''
Return self
/* ---------------------------------------------------------
Abre a tabela de imagens no SGDB
Conecta no DBAccess caso nao haja conexão
--------------------------------------------------------- */
METHOD Open( ) CLASS APDBIMAGE
Local nDBHnd := -1
Local aStru := {}
Local cOldAlias := Alias()
::cError := ''
IF ::bOpened 
 // Ja estava aberto, retorna direto
 Return .T.
Endif
If !TcIsConnected() 
 // Se não tem conexão com o DBAccess, cria uma agora
 // Utiliza as configurações default do appserver.ini
 nDBHnd := tcLink()
 If nDBHnd < 0
 ::cError := "TcLink() error "+cValToChar(nDbHnd)
 Return .F.
 Endif
Endif
If !TCCanOpen("ZDBIMAGE")
 
 // Cria array com a estrutura da tabela
 aAdd(aStru,{"ZDB_IMGID" ,"C",40,0})
 aAdd(aStru,{"ZDB_TYPE" ,"C",3,0}) // BMP JPG PNG 
 aAdd(aStru,{"ZDB_HASH" ,"C",32,0}) 
 aAdd(aStru,{"ZDB_SIZE" ,"N",8,0})
 aAdd(aStru,{"ZDB_MEMO" ,"M",10,0})
// Cria a tabela direto no SGDB
 DBCreate("ZDBIMAGE",aStru,"TOPCONN")
 
 // Abre em modo exclusivo para criar o índice de ID
 USE ("ZDBIMAGE") ALIAS ZDBIMAGE EXCLUSIVE NEW VIA "TOPCONN"
 
 If NetErr()
 ::cError := "Failed to open [ZDBIMAGE] on EXCLUSIVE Mode"
 Return
 Endif
 
 // Cria o índice por ID da imagem 
 INDEX ON ZDB_IMGID TO ("ZDBIMAGE1")
 
 // Fecha a tabela
 USE
 
Endif
 
// Abre em modo compartilhado
USE ("ZDBIMAGE") ALIAS ZDBIMAGE SHARED NEW VIA "TOPCONN"
If NetErr()
 ::cError := "Failed to open [ZDBIMAGE] on SHARED Mode"
 Return .F.
Endif
DbSetIndex("ZDBIMAGE1")
DbSetOrder(1)
::bOpened := .T.
If !Empty(cOldAlias) .and. Select(cOldAlias) > 0
 DbSelectArea(cOldAlias)
Endif
Return ::bOpened
/* ---------------------------------------------------------
Le uma imagem do banco para a memoria
recebe o nome da imgem, retorna por referencia o tipo
da imagem e seu conteudo 
-------------------------------------------------------- */
METHOD ReadStr( cImgId , /* @ */cImgType, /* @ */ cImgBuffer ) CLASS APDBIMAGE
::cError := ''
If !::bOpened
 ::cError := "APDBIMAGE:ReadStr() Error: Instance not opened."
 Return .F.
Endif
If empty(cImgId)
 ::cError := "APDBIMAGE:ReadStr() Error: ImageId not specified."
 Return .F. 
Endif
cImgId := Lower(cImgId)
If !ZDBIMAGE->(DbSeek(cImgId))
 ::cError := "APDBIMAGE:ReadStr() ImageId ["+cImgId+"] not found."
 Return .F.
Endif
// Caso a imagem com o ID informado seja encontrada
// Carrega o buffer da imagem para a variável de memória
cImgBuffer := Decode64(ZDBIMAGE->ZDB_MEMO)
cImgType := ZDBIMAGE->ZDB_TYPE
Return .T.
/* ---------------------------------------------------------
Insere uma imagem na tabela de imagens do SGDB
Recebe o ID da imagem, o tipo e o buffer 
-------------------------------------------------------- */
METHOD Insert( cImgId , cImgType, cImgBuffer ) CLASS APDBIMAGE
Local bOk := .F.
::cError := ''
If !::bOpened
 ::cError := "APDBIMAGE:Insert() Error: Instance not opened."
 Return .F. 
Endif
If empty(cImgId)
 ::cError := "APDBIMAGE:Insert() Error: ImageId not specified."
 Return .F. 
Endif
If empty(cImgType)
 ::cError := "APDBIMAGE:Insert() Error: ImageType not specified."
 Return .F. 
Endif
cImgId := Lower(cImgId)
cImgType := Lower(cImgType)
If !ZDBIMAGE->(DbSeek(cImgId))
 // Se a imagem não existe, insere
 ZDBIMAGE->(DBAppend(.T.))
 ZDBIMAGE->ZDB_IMGID := cImgId
 ZDBIMAGE->ZDB_TYPE := cImgType
 ZDBIMAGE->ZDB_SIZE := len(cImgBuffer)
 ZDBIMAGE->ZDB_HASH := Md5(cImgBuffer,2) // Hash String Hexadecimal
 ZDBIMAGE->ZDB_MEMO := Encode64(cImgBuffer)
 ZDBIMAGE->(DBRUnlock())
 bOk := .T.
else
 ::cError := 'Image Id ['+cImgId+'] already exists.'
Endif
Return bOk
/* ---------------------------------------------------------
Atualiza uma imagem ja existente no banco de imagens
Recebe ID, tipo e buffer
-------------------------------------------------------- */
METHOD Update( cImgId , cImgType, cImgBuffer ) CLASS APDBIMAGE
::cError := ''
If !::bOpened
 ::cError := "APDBIMAGE:Update() Error: Instance not opened."
 Return .F. 
Endif
If empty(cImgId)
 ::cError := "APDBIMAGE:Update() Error: ImageId not specified."
 Return .F. 
Endif
If empty(cImgType)
 ::cError := "APDBIMAGE:Update() Error: ImageType not specified."
 Return .F. 
Endif
cImgId := Lower(cImgId)
cImgType := Lower(cImgType)
 
If !ZDBIMAGE->(DbSeek(cImgId))
 ::cError := 'Image Id ['+cImgId+'] not found.'
 Return .F.
Endif
// Se a imagem existe, atualiza
IF !ZDBIMAGE->(DbrLock(recno()))
 ::cError := 'Image Id ['+cImgId+'] update lock failed.'
 Return .F.
Endif
ZDBIMAGE->ZDB_TYPE := cImgType
ZDBIMAGE->ZDB_SIZE := len(cImgBuffer)
ZDBIMAGE->ZDB_HASH := MD5(cImgBuffer,2) // Hash String Hexadecimal
ZDBIMAGE->ZDB_MEMO := Encode64(cImgBuffer)
ZDBIMAGE->(DBRUnlock())
Return .T.
/* ---------------------------------------------------------
Deleta fisicamente uma imagem da Tabela de Imagens
-------------------------------------------------------- */
METHOD Delete( cImgId , lHard ) CLASS APDBIMAGE
Local nRecNo
::cError := ''
If !::bOpened
 ::cError := "APDBIMAGE:Delete() Error: Instance not opened."
 Return .F. 
Endif
If empty(cImgId)
 ::cError := "APDBIMAGE:Delete() Error: ImageId not specified."
 Return .F. 
Endif
If !ZDBIMAGE->(DbSeek(cImgId))
 ::cError := 'Image Id ['+cImgId+'] not found.'
 Return .F.
Endif
// Se a imagem existe, marca o registro para deleção
nRecNo := ZDBIMAGE->(recno())
// Mesmo que a deleção seja fisica, eu garanto 
// o lock do registro na camada do dbaccess
If !ZDBIMAGE->(DbrLock(nRecNo))
 ::cError := 'Image Id ['+cImgId+'] delete lock failed.'
 Return .F.
Endif
// Deleta fisicamente no SGBD
nErr := TcSqlExec("DELETE FROM ZDBIMAGE WHERE R_E_C_N_O_ = " + cValToChar(nRecNo) )
If nErr < 0
 ::cError := 'Image Id ['+cImgId+'] delete error: '+TcSqlError()
Endif
// Solto o lock do registro no DBAccess
ZDBIMAGE->(DBRUnlock())
Return .T.
/* ---------------------------------------------------------
Fecha a tabela de imagens
-------------------------------------------------------- */
METHOD Close() CLASS APDBIMAGE
If Select('ZDBIMAGE') > 0
 ZDBIMAGE->(DbCloseArea())
Endif
::cError := '' 
::bOpened := .F.
Return .T.
/* ---------------------------------------------------------
Metodo Status()
Classe APDBIMAGE
Descrição Monta array por referencia contendo as informações da base 
 de imagens: Quantidade de registros total, tamanho estimado 
 total das imagens, quantidade de registros marcados para 
 deleção e tamanho estimado de imagens marcadas para deleçao 
-------------------------------------------------------- */
METHOD Status( /* @ */ aStat ) CLASS APDBIMAGE
Local cOldAlias := Alias()
Local cQuery 
Local nCountAll := 0
Local nSizeAll := 0
::cError := '' 
aStat := {}
If !::bOpened
 ::cError := "APDBIMAGE:Status() Error: Instance not opened."
 Return .F. 
Endif
// Conta quantas imagens tem na tabela, por tipo 
cQuery := "SELECT ZDB_TYPE, count(*) AS TOTAL"+;
 " FROM ZDBIMAGE GROUP BY ZDB_TYPE ORDER BY ZDB_TYPE"
 
USE (TcGenQry(,,cQuery)) ALIAS QRY EXCLUSIVE NEW VIA "TOPCONN"
While !eof()
 aadd(aStat , {"TOTAL_COUNT_"+QRY->ZDB_TYPE,QRY->TOTAL})
 nCountAll += QRY->TOTAL
 DbSkip()
Enddo
USE
// Acrescenta total de imagens
aadd(aStat , {"TOTAL_COUNT_ALL",nCountAll})
 
// Levanta o total de bytes usados por tipo de imagem
cQuery := "SELECT ZDB_TYPE, SUM(ZDB_SIZE) AS TOTAL"+;
 " FROM ZDBIMAGE GROUP BY ZDB_TYPE ORDER BY ZDB_TYPE"
 
USE (TcGenQry(,,cQuery)) ALIAS QRY EXCLUSIVE NEW VIA "TOPCONN"
While !eof()
 aadd(aStat , {"TOTAL_SIZE_"+QRY->ZDB_TYPE,QRY->TOTAL})
 nSizeAll += QRY->TOTAL
 DbSkip()
Enddo
USE
// Acrescenta total de bytes usados 
aadd(aStat , {"TOTAL_SIZE_ALL",nSizeAll})
If !Empty(cOldAlias)
 DbSelectArea(cOldAlias)
Endif
Return .T.
/* ---------------------------------------------------------
Ler um arquivo de imagem do disco para a memoria
Nao requer que a instancia esteja inicializada / Aberta
--------------------------------------------------------- */
METHOD LoadFrom( cFile, /* @ */ cImgBuffer ) CLASS APDBIMAGE
Local nH, nSize, nRead
::cError := ''
If !file(cFile)
 ::cError := "APDBIMAGE:LoadFrom() Error: File ["+cFile+"]not found."
 Return .F. 
Endif
nH := Fopen(cFile,0)
If nH == -1 
 ::cError := "APDBIMAGE:LoadFrom() File Open Error ( FERROR "+cValToChar( Ferror() )+")" 
 Return .F. 
Endif
nSize := fSeek(nH,0,2)
fSeek(nH,0)
If nSize <= 0 
 ::cError := "APDBIMAGE:LoadFrom() File Size Error : Empty File" 
 fClose(nH)
 Return .F. 
Endif
If nSize > MAX_IMAGE_SIZE
 ::cError := "APDBIMAGE:LoadFrom() File TOO BIG ("+ cValToChar(nSize) +" bytes)" 
 fClose(nH)
 Return .F. 
Endif
// Aloca buffer para ler o arquivo do disco 
// e le o arquivo para a memoria
cImgBuffer := space(nSize)
nRead := fRead(nH,@cImgBuffer,nSize)
// e fecha o arquivo no disco 
fClose(nH)
If nRead < nSize
 cImgBuffer := ''
 ::cError := "APDBIMAGE:LoadFrom() Read Error ( FERROR "+cValToChar( Ferror() )+")" 
 Return .F. 
Endif
Return .T.
/* ---------------------------------------------------------
Gravar um arquivo de imagem no disco a partir de uma imagem na memoria
Nao requer que a instancia esteja inicializada / Aberta
--------------------------------------------------------- */
METHOD SaveTo( cFile, cImgBuffer ) CLASS APDBIMAGE
Local nH, nSize , nSaved 
::cError := ''
If file(cFile)
 ::cError := "APDBIMAGE:SaveTo() Error: File ["+cFile+"] alreay exists."
 Return .F. 
Endif
// Cria o arquivo no disco 
nH := fCreate(cFile)
If nH == -1 
 ::cError := "APDBIMAGE:SaveTo() File Create Error ( FERROR "+cValToChar( Ferror() )+")" 
 Return .F. 
Endif
 
// Calcula tamanho do buffer de memoria
// e grava ele no arquivo 
nSize := len(cImgBuffer)
nSaved := fWrite(nH,cImgBuffer)
// Fecha o arquivo 
fClose(nH)
If nSaved < nSize
 ::cError := "APDBIMAGE:SaveTo() Write Error ( FERROR "+cValToChar( Ferror() )+")" 
 Return .F. 
Endif
Return .T.
Conclusão

Esta classe é só um esboço, com alguns parafusos a mais ela pode ser usada para construir um assistente para, por exemplo, importar uma pasta cheia de imagens para o banco de dados, dando o nome das imagens automaticamente baseado no nome do arquivo original, e o fato dela gerar o MD5 Hash a partir do buffer binário original pode permitir uma busca mais rápida por imagens idênticas repetidas dentro do banco, fazendo apenas uma Query para mostrar quais os ImageID´s que possuem o mesmo HASH !!!

Pessoal, novamente agradeço a audiência, espero que gostem do Post. Já tenho alguma coisa no forno para os próximos posts, mas continuo aceitando sugestões !! Até o próximo post, pessoal 😉