sábado, 30 de janeiro de 2021

Você está fazendo isso errado! Tópico #2

 Fala galera! Como estão todos? Sejam bem-vindos ao primeiro post de 2021.

Continuando o tema "Você está fazendo isso errado", vamos ao tópico #2, onde falaremos do uso da recursividade.

Não lembro exatamente a primeira vez que vi o recurso recursividade sendo explicado (acho que foi numa aula de Programação I - tenho a impressão que não vi esse recurso nas aulas de algoritmos), mas lembro que fiquei impressionado, tanto pelo recurso em si, quanto como a palavra recursividade se encaixava bem ao propósito do recurso.

Pois bem, já perdi as contas de quantas vezes escrevi algoritmos baseados em recursividade, então vai novamente a ressalva, pessoal, de que o objetivo deste post não é cravar a regra "não use recursividade", mas sim, avalie se a recursividade realmente é a melhor solução para o seu algoritmo.

A verdade é que, para fins de leitura e simplificação de código, a recursividade muito mais agrega do que prejudica. Normalmente códigos recursivos são simples e elegantes.

A recursividade começa a se tornar um problema a partir do momento que não sabemos ou não é possível determinar quantas vezes a função vai chamar a si mesma (direta ou indiretamente), e não levar isso em consideração pode gerar um gargalo absurdo nos nossos algoritmos.

Sei que o exemplo que vou usar é batido: Descobrir o valor de uma determinada posição na sequência de Fibonacci. Todavia, neste caso em específico, não temos motivo para reinventar a roda, pois o exemplo demonstra claramente três coisas:

  • O quanto um código pode ficar mais legível com recursividade;
  • O quanto a performance pode cair ao usarmos recursividade;
  • Recursividade não necessariamente é a única solução para alguns problemas.
Vamos começar analisando a implementação recursiva para Fibonacci:
1
2
3
4
5
6
7
class function TFibonacci.GetValorDaPosicao(const pPosicao: UInt32): UInt32;
begin
  if pPosicao < 2 then
    Result := pPosicao
  else
    Result := GetValorDaPosicao(pPosicao - 1) + GetValorDaPosicao(pPosicao - 2);
end;

É um código claro, que representa exatamente como a sequência é calculada ou até mesmo explicada para alguém que está sendo apresentado à sequência de Fibonacci.

Vamos agora dar uma olhada em como o algoritmo pode ser implementado de forma iterativa, eliminando a recursividade:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class function TFibonacci.GetValorDaPosicaoSemRecursividade(pPosicao: UInt32): UInt32;
begin
  if pPosicao < 2 then
    Result := pPosicao
  else
  begin
    var lUltimaPosicao: UInt32 := 1;
    Result := 1;
    while pPosicao > 2 do
    begin
      Inc(Result, lUltimaPosicao);
      lUltimaPosicao := Result - lUltimaPosicao;
      Dec(pPosicao);
    end;
  end;
end;

Alguém ficou em dúvida de quanto um código é mais simples que o outro? 😝   A diferença é gritante! Bom, para trocar a primeira opção pela segunda, é bom ter um bom motivo, não é?

Nas apresentações do "Você está fazendo isso errado" em eventos, eu expliquei o problema, a sua origem, e também como resolver o mesmo, mas, em nenhuma das apresentações eu citei números para demonstrar a queda de performance. Pois bem, aqui vamos ver números (lembrando que esses números são específicos para cada caso, e aqui estamos falando da recursividade e complexidade gerada para resolver a sequência de Fibonacci).

Antes dos números, vamos contextualizar que, para realizar este tipo de teste de performance, é importante reduzir ao máximo a quantidade de variáveis. Para isso, os algoritmos acima foram compilados em um exe de 64 bits configurado como release, e este executável foi testado em um servidor que estava ocioso durante os testes, conferindo assim um ambiente mais uniforme possível para os testes. Também é importante dizer que as medidas de performance foram executadas somente durante o processamento da lógica de negócio em si, não considerando atualizações visuais que usei como saída dos resultados. Por fim, os números demonstrados abaixo são as médias obtidas em 3 execuções de cada um dos cenários testados.

Sem mais delongas, vamos aos números:
PosiçãoResultadoRecursivoNão Recursivo
Tempo (ms)TicksTempo (ms)Ticks
00< 144< 141
32< 147< 141
821< 151< 142
15610< 196< 150
206.765< 1556< 144
2575.025< 15.486< 141
30832.040555.810< 142
359.227.46540403.651< 149
40102.334.1554334.331.396< 146
43433.494.4371.81318.131.228< 152
44701.408.7332.93829.383.621< 149
451.134.903.1704.71447.141.376< 149

Vamos às conclusões...

Durante a busca por posições iniciais (até a posição 8), a performance de ambos os algoritmos é muito semelhante, ao ponto das variações nos resultados estarem mais ligadas à variáveis que não temos controle (como o escalonamento de processos do Windows) do que qualquer outro fator.

Já a partir da busca pela posição 15 em diante, a performance do algoritmo recursivo começa a degradar significativamente. Na busca pela posição 15, especificamente, ainda estamos na faixa de tempo abaixo do 1ms, mas a quantidade de ticks já começa a apresentar o problema. Claro que daí para frente a deterioração é exponencial, na mesma base da evolução da sequência de Fibonacci (aproximadamente 1,618 - O(1,618 na potência n) - o que fica comprovado ao analisarmos as últimas 3 linhas da tabela).

Em contrapartida, a complexidade do algoritmo iterativo é linear (obviamente), sendo que, para as buscas apresentadas nesta tabela, as variações não estão relacionadas à variável n, mas às mesmas variáveis citadas dois parágrafos acima.

E então? O que acharam? É bom ter atenção aos detalhes dos algoritmos, não?

Abraço e até a próxima!!!

quarta-feira, 30 de dezembro de 2020

Você está fazendo isso errado - Introdução!

Fala galera! Como vocês estão em tempos de pandemia? Espero que bem!

Hoje vou contar um pouco sobre a apresentação "Você está fazendo isso errado!".

Como alguns de vocês já sabem, eu fui instrutor certificado e também consultor Delphi durante muitos anos. Isto me permitiu ver códigos de todos os tipos, foi um período de muito aprendizado, em todos os sentidos possíveis.

Foi neste período que comecei a montar uma pequena lista de erros que desenvolvedores Delphi cometem, pequenos erros, mas com uma frequência grande o suficiente para me chamar a atenção.

Pois bem, separei alguns destes itens, dando prioridade para os que poderiam ser explicados em poucos minutos, e montei uma apresentação que foi rodada a primeira vez ano passado (2019) no Delphi Squad, aqui em Porto Alegre, com o já mencionado título "Você está fazendo isso errado!". Esta apresentação teve bastante repercussão e excelente aceitação.

Pois bem, este ano montei a versão II da apresentação, que foi apresentada na Embarcadero Conference 2020, que também foi muito bem aceita (obrigado comunidade pelas avaliações!).

Ontem, dia 28/12, ainda respondi um e-mail relativo à dúvidas decorrentes da apresentação da Conference, então, decidi aprofundar alguns dos itens (ou todos, vamos ver), aqui no blog também, e quem sabe, atingir mais Delpheiros que, por ventura, ainda praticam estes erros ;D

De imediato, vou começar por falar talvez do item com mais repercussão em todas as vezes que já rodei a primeira edição da apresentação...

#1 A correta relação entre Create x Try x Finally x Free de um objeto!

Uma das primeiras coisas que aprendemos no Delphi é instanciar objetos a partir de classes. Algum breve momento depois, aprendemos que, na maioria dos casos, somos também responsáveis por destruir os objetos que criamos. Depois, um novo elemento é inserido neste aprendizado, que é o fato de que nem sempre tudo acontece segundo o "caminho feliz", e nosso código deve estar preparado para eventuais erros, e aqui me refiro às exceções mesmo, e neste momento, em outras palavras, a destruição destes objetos deve estar protegida quanto à possíveis exceções.

Este é um conhecimento relativamente básico, um desenvolvedor Delphi júnior deve ter este conhecimento, e até aqui tudo bem. O problema surge quando avaliamos as inúmeras variações que os desenvolvedores usam para resolver o cenário acima. Quem me conhece sabe que eu sou contra as receitas definitivas, ou, pior ainda, frases de efeito como "nunca faça isso", "nunca use aquilo", mas, existem alguns casos onde o "nunca" realmente se aplica (vide comando with ;D), e existem também os casos que o desenvolvedor, ao seguir uma receita de bolo, realmente vai resolver 98% dos seus problemas, e os outros 2% passam por entender o que cada dos comandos usados faz na prática, e então o desenvolvedor está livre para escrever a variação que desejar, e este é o caso para construir objetos cujo ciclo de vida é local.

Se o seu objetivo é criar um objeto, usá-lo, e então destruí-lo, esta deveria ser a receita de bolo a ser seguida na grande maioria dos casos:

1
2
3
4
5
6
var lObjeto := TMinhaClasse.Create;
try
  { uso do objeto aqui }
finally
  lObjeto.Free;
end;

Volto a chamar a atenção para o fato de que este código é extremamente simples, muitos desenvolvedores devem estar pensando agora que "é exatamente assim que eu faço" (ótimo!), e tem também a galera que diz "sim, eu faço assim, aaaaahhhh, na realidade eu faço uma coisa aqui ou ali diferente, mas é assim que eu faço!", e aqui entram os códigos que, ou não caracterizam erros, mas poderiam ser melhores, mais otimizados, ou são códigos que realmente podem gerar erros quando a situação sai do caminho feliz.

Vamos para algumas das variações incorretas ou que permitem otimizações:

  • Instanciar o objeto dentro do bloco try: Existem situações em que esta prática realmente não configura erro, mas em se tratando de uso de variáveis locais, que não foram inicializadas antes do try, aí realmente trata-se de uma prática bem ruim.
Lembrem-se que, uma vez dentro de um try protegido por finally, o finally vai ser executado independentemente do que acontecer no try (conhecimento básico de tratamento de exceções, eu sei). O problema todo está na possibilidade de acontecer um erro antes da associação da variável com o objeto criado. "Ah, mas a primeira coisa que eu faço dentro do bloco try é criar o objeto na variável", muitos desenvolvedores esquecem que este processo também é executado, no mínimo, em dois passos, e a associação da variável será sempre o último destes passos. Qual seria o primeiro passo? R: A criação do objeto em si. 
 
Sendo assim, sempre temos que assumir que a própria criação do objeto pode levantar uma exceção, e neste caso, a variável nunca vai receber o objeto criado. Como consequência, a variável local vai continuar apontando para uma área de memória "aleatória", e o resultado do bloco vai ser, muito provavelmente, um Access Violation durante o bloco finally. 
 
Um dos motivos para que os desenvolvedores levem a criação para dentro do bloco try, é justamente pensar que é necessário proteger a destruição do objeto para também no caso do construtor levantar uma exceção: Errado! Caso uma exceção seja disparada e não tratada dentro do construtor, o Delphi SEMPRE vai chamar, automaticamente, o destrutor daquela mesma classe, para então voltar a propagar a exceção, e ainda assim, não ficando objeto vazado na memória. No resumo, é isto aqui que acontece com essa prática:
    1
    2
    3
    4
    5
    6
    try
      lObjeto := TMinhaClasse.Create; // se der exceção aqui...
      { uso do objeto aqui }
    finally
      lObjeto.Free; // provavelmente vai dar AV aqui.
    end;
  • Testar se a variável está associada antes de chamar o Free: Quando eu vejo esta prática, eu sempre pergunto ao autor do código, qual é o nome do destrutor padrão do Delphi? R: Destroy (obviamente). E então, por que temos o hábito de usar Objeto.Free e não Objeto.Destroy? Existe alguma diferença prática entre um e outro? R: Existe!
Ainda me impressiono com a quantidade de Delpheiros que não sabe que o método Free, implicitamente, já faz este teste para nós. Não ficou claro? Esta é uma cópia da implementação do método Free de TObject, retirada do Delphi Sydney:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    procedure TObject.Free;
    begin
    // under ARC, this method isn't actually called since the compiler translates
    // the call to be a mere nil assignment to the instance variable, which then calls _InstClear
    {$IFNDEF AUTOREFCOUNT}
      if Self <> nil then
        Destroy;
    {$ENDIF}
    end;
Então, na prática, testar se o objeto está associado ou não, para então chamar o Free, é redundância de código. Não ficou claro? É mais ou menos isso que o Delphi vai fazer quando você coloca o if antes do Free:
    1
    2
    3
    if ObjetoASerDestruido <> nil then
      if ObjetoASerDestruido <> nil then
        ObjetoASerDestruido.Destroy;
  • Usar FreeAndNil para variáveis que vão ser eliminadas do contexto: Assim como o item anterior, este é um caso menos grave, mas me intriga ver como existem desenvolvedores que se atem às suas raízes, hábitos, muitas vezes paixões (:D) ao estilo e práticas de escrita de código, sem se perguntar o porquê de fazerem isso ou aquilo, simplesmente saem implementando daquela maneira porque aprenderam assim, ou tiveram um problema que aparentemente foi resolvido por aquela prática, e então criam a regra do "sempre faço assim, não me pergunte por que.".
O FreeAndNil é um destes casos, existe a igreja do "Uso FreeAndNil sempre", que tem milhares de devotos mundo afora. E vamos deixar bem claro, eu não estou dizendo que não se deve usar FreeAndNil, eu acho que existem casos e casos, e na grande maioria deles, EU NÃO USO FreeAndNil. Isto se deve ao fato de que a maioria dos objetos que eu preciso destruir manualmente tem ciclos de vida local, e consequentemente, uso também variáveis locais. 
 
Faz mal usar FreeAndNil indiscriminadamente? Depende o que você considera como "mal". Vamos começar por analisar a diferença entre chamar o Free diretamente ou chamar o FreeAndNil. 
 
O Free já discutimos o que faz no item acima, beleza. 
 
Do FreeAndNil, quem nomeou o procedimento foi feliz na tarefa, ele descreve exatamente o que vai acontecer com a variável do tipo objeto que passarmos para ele como parâmetro: resumidamente, vai ser chamado o método Free do objeto apontado pela variável, e então a variável vai ser desassociada (vai apontar para nil). Se analisarmos o código do FreeAndNil, é um código de baixa complexidade, que não envolve a chamada de stack (é um procedimento inline), mas que continua dando mais trabalho ao processador do que um Free daria. 
 
Gosto de fazer uma analogia: você já alugou muitos apartamentos ao longo da vida, sempre que você tem que devolver o apartamento, você sabe que tem que fazer a pintura nova do apartamento, eventuais pequenos reparos, e de tanto fazer isso, você já ficou craque na coisa, toda vez que precisa entregar o apartamento, sai fazendo tudo por impulso, afinal, é simples, sempre fez e deu certo. Então, você recebe uma carta dizendo que você deve deixar o atual apartamento que você aluga, não por pedido do proprietário ou algo assim, mas porque o prédio vai ser implodido. Beleza, você dispara o tradicional script de liberação do apartamento: tira suas coisas, pinta o apartamento, repara o que precisa de reparo, deixa tudo brilhando....   Espeeeeera! Por que raios você vai fazer isso tudo? O apartamento vai ser implodido! Ninguém vai alugar ele depois de você! Concorda? Espero que sim, e sendo assim, bem, o que você faz ao chamar sempre o FreeAndNil é pintar o apartamento toda vez que você desocupa ele, independentemente se vai ser usado por outra pessoa ou se o prédio vai ser implodido. "Ah, é injusta essa comparação, afinal, executar o FreeAndNil é um trabalho simples para o processador.", posso concordar partes com essa frase, mas uma coisa eu tenho certeza pelos vários anos de consultor, é esta falta de preciosismo com o código, de coisas que não mudamos por "sempre fiz assim", preguiça, ou outra desculpa, que nos fazem abrir portas para outras rotinas menos otimizadas ainda onde, por fim, ou impede a migração do mesmo para um cenário onde uma performance melhor seria necessária, como ser reutilizado em um server REST, entre outras situações. Devemos nos preocupar com os grandes gargalos, com os tradicionais grandes erros ao desenvolver um sistema? Não tenho dúvidas, a prioridade é essa. Mas, ao mesmo tempo, continuar escrevendo pequenos códigos que poderiam ser melhores porque "sempre fiz assim", não curto não. ;-)


Enfim, estes foram alguns dos tópicos que demonstrei neste tema do Você está fazendo isso errado". Em um futuro bem próximo vou abordar mais temas da apresentação, bem como podemos falar de outras variações que ainda podem acontecer na relação Create x Try x Finally x Free.


Encerro o post por aqui, desejando que todos tenham crescido de alguma forma com tudo que aconteceu em 2020, e desejando que 2021 seja um super ano para todos, que seja o ano que encerraremos este ciclo triste que estamos vivendo. De coração, muitas felicidades para o ano novo! Até 2021!

sábado, 31 de outubro de 2020

Retomando os trabalhos - Parte 2!

Fala Delpheiros!!!! Outubro foi um mês corrido, tive uma apresentação interna para a Agro1, retomada de viagens para Erechim (onde fica a matriz a matriz da Agro1), e claro, tivemos a Embarcadero Conference Brasil 2020, que infelizmente (e corretamente) teve que ser online neste ano.

Em Setembro contei um pouco do que estava acontecendo por aqui, falei do trabalho na Agro1, e deixei o link pronto para começar a a falar da TMR. Pois bem...

Pouco tempo depois de eu ter acertado com a Agro1, usando palavras do próprio, "o meu brother" Samuel "Muka" David me procurou com o fim de rodarmos um projeto audacioso (e vindo do Muka, era de se esperar o quê? 😜). O objetivo era criar uma empresa para atender demandas de mercado que surgiam a todo momento, montar realmente uma equipe de elite, e se tornar referência no fornecimento de desenvolvedores Delphi!

Começamos o processo de análise de mercado, viabilidade, recrutamento...    e no meio de todos esses processos, recrutamos o terceiro elemento e formamos o tripé que seria fundamental para a sustentação necessária da empresa: entrou na parada o nosso outro brother Luiz "Radical" Sfolia.

Em maio de 2018 começamos a rodar a primeira etapa do projeto, com o Muka e eu na retaguarda organizacional, e o Radical com mais 3 devs mandando ver no Delphi!




Foi somente em outubro daquele mesmo ano que, com a consolidação da empresa, adotamos o nome TMR (criatividade, nota....   10! 😂😂).

Com o tempo, novos projetos foram iniciados, novos clientes, novos integrantes na equipe, e a TMR está, felizmente, cada vez mais forte!

A TMR ainda é uma empresa relativamente nova, mas desde o princípio nossa prioridade maior é proporcionar o melhor ambiente possível para a "nossa galera", seja através de constante atualização técnica da equipe, manter um ambiente legal para trabalhar, ou até mesmo prezar por momentos de integração constante, seja em eventos técnicos e até mesmo no trabalho diário, bem como nos nossos encontros "extra-campo". Dizem que imagens valem mais do que palavras, certo? Então vou parar de escrever...










Infelizmente 2020 não foi um bom ano para mantermos estas atividades, mas espero que logo essa condição que vivemos hoje mude, e assim a primeira medida que tomaremos será reunir toda a galera, saudades da junção 😂!

Enfim, em outro momento volto a escrever sobre as novidades da TMR. Enquanto isso, se quiser conhecer um pouco mais sobre a empresa, pode enviar um mail para contato@tmrti.com.br.

Por hoje era isto. Até o próximo post!

quarta-feira, 30 de setembro de 2020

Memory leak inesperado

Essa é uma séria de posts que estava na minha mira há bastante tempo, sendo que hoje tomei vergonha na cara para começar. Nesta série tentarei compartilhar as coisas que me acontecem, surpreendem, e aprendo no dia a dia de desenvolvedor. Então hoje começo a série "hoje eu aprendi...".

Já há alguns vários dias eu fui surpreendido por um vazamento de memória (memory leak) em um sistema da Agro1. O vazamento de memória estava em um código semelhante a este:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var  
  [weak] lAlgumaCoisa: IAlgumaCoisa;  
begin  
  If Supports(pAlgumObjeto, IAlgumaCoisa, lAlgumaCoisa) then
  begin  
    RegistrarMetodoQueSeraExecutadoDepois(  
      procedure  
      begin  
        lAlgumaCoisa.ExecutarAlgumaCoisa;  
      end);  
  end;  
end;


Pois bem, uma das bases do novo framework em que estamos trabalhando na Agro1 é o uso massivo de interfaces, e felizmente o Delphi Berlin trouxe a opção de declarar variáveis do tipo interface (ou fields, parâmetros, etc.) como sendo referências fracas ou não seguras, e um dos efeitos destes atributos é fazer com que estas variáveis não passem pelos métodos _AddRef e _Release de IInterface durante sua manipulação (você pode ler um pouco mais sobre estes atributos aqui).

Claro que o método acima é um resumo do que precisava ser feito e pode parecer confuso, mas trouxe para este exemplo somente o básico para mostrar o problema.

O que aprendi neste dia foi que os metadados da variável passada como terceiro parâmetro do método Supports não são considerados no momento de fazer a atribuição da referência para a variável, logo, a variável estar marcada ou não com o atributo weak não faz a menor diferença. Isto tudo porque o método Suppoprts chama diretamente o método QueryInterface, que por sua vez chama o método GetInterface, que chama explicitamente o _AddRef. Como a implementação do _AddRef, neste caso, fazia uso de contagem de referência, a variável entrou na contagem, fechou-se um circuito fechado entre as variáveis envolvidas no processo, e consequentemente geramos um vazamento de memória.

Solução: tivemos que declarar uma segunda variável usada somente com escopo local, sem o atributo weak, e outra variável dedicada ao closure, esta sim com o marcador weak. Resumindo, ficou algo parecido com isto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var  
  lAlgumaCoisaClosure: IAlgumaCoisa;  
  [weak] lAlgumaCoisaClosure: IAlgumaCoisa;  
begin
If Supports(pAlgumObjeto, IAlgumaCoisa, lAlgumaCoisa) then begin
    lAlgumaCoisaClosure := lAlgumaCoisa;
    RegistrarMetodoQueSeraExecutadoDepois(  
      procedure  
      begin  
        lAlgumaCoisaClosure.ExecutarAlgumaCoisa;  
      end);  
  end;  
end;


Bom, se você passar por situação parecida, já sabe, está aí o problema e possível solução, espero que este post lhe ajude.

Grande abraço e até o próximo!

Retomando os trabalhos - Parte 1!

Fala galera! Há quanto tempo não passava por aqui?!

Pois é, o blog já teve um hiato anteriormente, foi durante um período de várias mudanças na minha vida, principalmente profissional. Na época, eu tinha saído da Aquasoft e entrado na Softplus.

Por circunstâncias de mercado, minha estada na Softplus foi curta, encerrei minhas atividades com aproximadamente um ano de trabalho na Softplus. A Softplus foi uma empresa que gostei muito de trabalhar, aprendi muito sobre diversas tecnologias, principalmente processos de automatização como CI e CD. Mas o mais importante, tive ótimos colegas, e fiz amigos de verdade por lá (no aguardo do próximo xis no Xodó ;-D).

Enfim, não querendo estender muito a história, eu encerrei minhas atividades na Softplus para começar a trabalhar na Agro1, empresa dedicada ao desenvolvimento de soluções para o agronegócio, e felizmente estou na Agro1 desde Abril de 2018.

Na Agro1, eu tenho trabalhado com projetos incríveis, estamos evoluindo diariamente em diversos aspectos, muitas coisas legais sendo implementadas (baixíssimo nível de acoplamento com interfaces e injeção de dependência, testes unitários, outros testes automatizados, melhorias nos processos de versionamento, a lista é longa), a equipe abraçou projetos de qualificação, e a busca por inovação e outras melhorias tem sido uma constante.

Estamos sempre buscando por novos integrantes para a equipe, então, se você quer ser meu colega na Agro1, me manda um mail no tatu@taturs.com. Bora!?

Para a parte 2, vou contar um pouquinho de como estão as coisas na TMR.

quarta-feira, 7 de março de 2018

DROP Wiki (English) #2 - Connecting to DBMSs

Você pode ler este post em português clicando aqui.

Continuing the DROP Wiki Series, the current topic is the connection establishment with a data source (usually a DBMS) and the choice of the best class to perform the task.

Further on, we will see that for the DROP ORM to work, a few prerequisites are necessary. Of them all, the most important is to create an object of TAqDBConnection, and, trough it, to establish a connection with the data source.

The TAqDBConnection class (in unit AqDrop.DB.Connection) is almost entirely abstract. This class goal is to abstract the minimum required to execute commands in the supported DBMSs. Obviously, this abstraction exists to provide the framework extension to different data access engines, being that we already have implementations for DBX and FireDAC.

Basically, the interfaces provided by TAqDBConnection are:
  - Transaction manipulation routines;
  - Command preparing (Prepare e Unprepare);
  - Cursor opening routines;
  - Command execution routines (usually DMLs);

The routines of transaction manipulation, command preparation and execution are not so different from the routines provided by any other data access engine (including third party components). The most significant difference is that of the cursor opening routines, which were built to provide interfaces similar to the readers  in DBX Framework, providing a code that is more suitable to O.O. characteristics (summing up: no DataSets ;-)

Aiming to split the specific code to support DBX and FireDAC, two classes were implemented and, as VCL style, both of them have the prefix 'Custom' on their names (TAqDBXCustomConnection e TAqFDCustomConnection), doing the heavy work so as to achieve their respective goals. Two direct inheritances, omitting the 'Custom' from their names (TAqDBXConnection e TAqFDConnection), have only the job of publishing the parameters provided by DROP and the mapped engines, in order to allow the complete configuration to access the desired DBMS.

Our first example will use Interbase as the DBMS and DBX as the internal engine, so, we will use the generic class TAqDBXConnection, as shown in the example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function TfrmDW0002.GetConnection
  (const pDatabasePath: string): TAqDBConnection;
var
  lConnection: TAqDBXConnection;
begin
  lConnection := TAqDBXConnection.Create;

  try
    lConnection.DriverName := 'InterBase';
    lConnection.VendorLib := 'GDS32.DLL';
    lConnection.LibraryName := 'dbxint.dll';
    lConnection.GetDriverFunc := 'getSQLDriverINTERBASE';
    lConnection.Properties[TDBXPropertyNames.Database] := pDatabasePath;
    lConnection.Properties[TDBXPropertyNames.Username] := 'SYSDBA';
    lConnection.Properties[TDBXPropertyNames.Password] := 'masterkey';
    lConnection.DBXAdapter := TAqDBXIBAdapter.Create;

    lConnection.Connect;
  except
    lConnection.Free;
    raise;
  end;

  Result := lConnection;
end;

The above example shows that to configuring a DROP connection or a TSQLConnection, for example, are similar tasks. However, a clear difference between the DROP connection and any other connection component provided by Delphi, is in the building of an adapter, which is necessary for a complete operation of the DROP features. The adapters tell DROP how to solve some tasks, or else, how to read some DBMS data types.

As the DROP aims to work more as a library  and less as a component, the above task can be simplified through the use of other set of classes, which easily wrap some distinctions of the supported DBMSs. Bellow we'll see the creation of a connection with the same database, however, using another class, which wraps DBX and the Interbase particularities.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function TfrmDW0002.GetConnectionBySpecificClass
  (const pDatabasePath: string): TAqDBConnection;
var
  lConnection: TAqDBXIBConnection;
begin
  lConnection := TAqDBXIBConnection.Create;

  try
    lConnection.DataBase := pDatabasePath;
    lConnection.UserName := 'SYSDBA';
    lConnection.Password := 'masterkey';

    lConnection.Connect;
  except
    lConnection.Free;
    raise;
  end;

  Result := lConnection;
end;

In the above example we can notice that the class TAqDBXIBConnection takes responsibility of configuring the properties as standards defines for Interbase, and exposes properties that are specific to IB. These properties allow us to statically setup some data like the GDB path, user and password, unlike the first example, where this information was dynamically provided.

Well, this was the example using the Interbase, and examples with other DBMSs simply mean a little bit of the same, talking specifically of the code. However, it is important at least to provide the list of specialized classes already available in DROP, each of them setting up the connection as the known standards of the supported DBMSs, and providing their particular properties. Here goes the complete list of classes, grouped by the data access engine:

DBX:
 - TAqDBXIBConnection;
 - TAqDBXMSSQLConnection;
 - TAqDBXMySQLConnection;
 - TAqDBXFBConnection;
 - TAqDBXOraConnection;
 - TAqDBXSQLiteConnection;

FD:
 - TAqFDIBConnection;
 - TAqFDMSSQLConnection;
 - TAqFDMySQLConnection;
 - TAqFDFBConnection;
 - TAqFDOraConnection;
 - TAqFDSQLiteConnection;
 - TAqFDPGConnection;

You may notice that FireDAC has an additional variation, which is PostgreSQL.

And, for the time being, that will be all. As a follow-up, we will have the episode #3, which will show how to use these objects, and in future episodes, how to use them inside the ORM aspect.

Until the next post!

quarta-feira, 28 de fevereiro de 2018

DROP Wiki #5 - Abstração de comandos SQL

Uma das premissas do DROP é fornecer interfaces uniformes para que o código seja escrito uma vez, e este opere sobre múltiplos SGBDs. O tema deste post é sobre um dos principais pilares do DROP para garantir a veracidade desta premissa, que são as interfaces de abstração de comandos SQL.

Sabemos que os recursos oferecidas pelos SGDBs e respectivas DMLs são muito parecidos. Todos entregam praticamente as mesmas funcionalidades, por estruturas de SQLs ligeiramente diferentes.

As units AqDrop.DB.SQL.Intf e AqDrop.DB.SQL fornecem, respectivamente, interfaces e classes que abstraem as funcionalidades de DML, sendo que as instâncias destas classes são traduzidas para a respectiva DML do SGBD apontado pela conexão do DROP que for executar o comando.

Vamos começar por algo simples: 'select * from country'. Sabemos que praticamente todo SGBD relacional aceitará este comando, desde que exista a uma tabela chamada country no esquema selecionado. O código abaixo demonstra como é simples usar as interfaces e classes de abstração de comandos SQL para executar o comando:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
procedure TfrmDW0005.TestSelect;
var
  lSelect: IAqDBSQLSelect;
  lReader: IAqDBReader;
begin
  MemoCountries.Lines.Clear;
  MemoCountries.Lines.Add('List of all registered contries:');

  lSelect := TAqDBSQLSelect.Create('country');

  lReader := FEmployeeConnection.OpenQuery(lSelect);
  while lReader.Next do
  begin
    MemoCountries.Lines.Add(lReader.Values['country'].AsString);
  end;
end;

Detalhes importantes sobre o código acima:
  • Note que a variável lSelect, que armazena a referência para o comando, é do tipo IAqDBSQLSelect;
  • Na linha 9, é instanciado um objeto do tipo select, onde a origem deste select (utilizada na cláusula from) é uma tabela de nome country (a origem pode ser também uma view, uma string contendo um subselect, ou um subselect na forma de outra interface do tipo IAqDBSQLSelect);
  • Não é informada nenhuma coluna de retorno no select, logo, o DROP assume o * como lista de colunas a serem retornadas;
  • Tanto o comando OpenQuery quanto o funcionamento do reader foram apresentados no DROP Wiki #3, mas na oportunidade, a sobrecarga utilizada do comando foi um select escrito manualmente em uma constante string. Já neste caso, estamos vendo o uso da sobrecarga que recebe uma interface do tipo IAqDBSQLSelect (linha 11);
  • A partir do início do while segue a mesma lógica já apresentada no Wiki #3.
Vamos partir agora para um comando que continua simples, mas que já exige um select diferente para (quase) cada um dos SGDBs suportados, que é a limitação da quantidade de registros que devem ser retornados pelo select. Segue novo método que retorna no máximo 3 registros da tabela country:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
procedure TfrmDW0005.TestSelectLimited;
var
  lSelect: IAqDBSQLSelect;
  lReader: IAqDBReader;
begin
  MemoCountries.Lines.Clear;
  MemoCountries.Lines.Add('List of up to 3 countries:');

  lSelect := TAqDBSQLSelect.Create('country');
  lSelect.Limit := 3;

  lReader := FEmployeeConnection.OpenQuery(lSelect);
  while lReader.Next do
  begin
    MemoCountries.Lines.Add(lReader.Values['country'].AsString);
  end;
end;

Ao executar o comando acima, o DROP traduz a informação para os recursos nativos de cada um dos SGDBs, limitando a quantidade de registros retornados.

Visando uma melhor performance do comando, vamos tornar explícito que o select deve trazer somente a coluna country de cada registro, e somente para ampliar os recursos demonstrados, vamos fazer com que esta coluna seja retornada com o alias 'country_name'. Segue método demonstrando as alterações necessárias (para deixar claro, o alias não é obrigatório ao definir uma coluna):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
procedure TfrmDW0005.TestSelectWithColumnAndAlias;
var
  lSelect: IAqDBSQLSelect;
  lReader: IAqDBReader;
begin
  MemoCountries.Lines.Clear;
  MemoCountries.Lines.Add('List of up to 3 countries:');

  lSelect := TAqDBSQLSelect.Create('country');
  lSelect.AddColumn('country', 'country_name');
  lSelect.Limit := 3;

  lReader := FEmployeeConnection.OpenQuery(lSelect);
  while lReader.Next do
  begin
    MemoCountries.Lines.Add(lReader.Values['country_name'].AsString);
  end;
end;

Também no DROP Wiki #3 comentei que o objetivo de um ORM não é fazer com que o usuário escreva manualmente os comandos SQL. Pois bem, o objetivo do DROP também não é fazer o desenvolvedor criar os próprios comandos de SQL através destas interfaces e classes. Então, por que fiz questão de passar por este conteúdo antes de chegar no ORM propriamente dito?

Eventualmente (ou algo com frequência muito maior que eventualmente), teremos que customizar os comandos que serão disparados contra o SGDB. Para fazer esta customização, o DROP conta diversas variações e recursos, no entanto, a variação mais indicada passa por conhecermos as interfaces de abstração de SQLs.

Mais adiante veremos que o ORM do DROP pode nos retornar objetos instanciados e configurados para as interfaces dos tipos IAqDBSQLSelect, IAqDBSQLInsert, IAqDBSQLUpdate e IAqDBSQLDelete. Manipular estas referências já devidamente configuradas é a maneira mais rápida e simples para customizar os comandos enviados para o SGDB.

Através destas interfaces, é possível realizar uma série de customizações, como adicionar cláusulas where, joins (use com moderação, afinal você está em um ambiente que visa o uso do ORM ;-), funções de agregação de dados, ordenação, etc. E sempre que for necessário mudar o texto resultante do comando de DML, o DROP faz isso por você.

Segue uma última variação do método de busca de países, onde uma cláusula where é adicionada, trazendo somente países em que a moeda seja o Euro:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
procedure TfrmDW0005.TestSelectWithCondition;
var
  lSelect: IAqDBSQLSelect;
  lReader: IAqDBReader;
begin
  MemoCountries.Lines.Clear;
  MemoCountries.Lines.Add('List of countries where the currency is Euro:');

  lSelect := TAqDBSQLSelect.Create('country');
  lSelect.CustomizeCondition.AddColumnEqual('currency', 'Euro');

  lReader := FEmployeeConnection.OpenQuery(lSelect);
  while lReader.Next do
  begin
    MemoCountries.Lines.Add(lReader.Values['country'].AsString);
  end;
end;

Sobre o método acima, é importante dizer que foi utilizado o método AddColumnEqual, que monta uma condição com o operador de igualdade entre valores, mas existem diversos outros métodos que mudam o operador a ser utilizado. Cada um destes métodos possui várias sobrecargas, sendo que cada uma das sobrecargas permite o uso de tipos primitivos para seus respectivos fins, e o DROP traduz o tipo primitivo do Delphi para o respectivo tipo primitivo do SGDB, se este existir.

Por exemplo, alguns SGDBs trazem suporte nativo ao tipo Boolean, no entanto, o uso deste tipo varia de SGDB para SGDB. Em alguns deles a constante True se escreve desta forma, em outros se escreve como se fosse uma string ('True'), e em outros ela sequer existe. Para todos estes casos, o DROP faz a devida tradução para a constante do SGDB utilizado (ou para o formato padrão, como no caso de datas), e para os SGDBs sem suporte a esta constante, o DROP propõe outras como 1 ou 'T'. E se você não estiver satisfeito com estas propostas, você ainda pode informar ao DROP como traduzir cada um dos tipos de dados do Delphi para os tipos de dados do seu SGDB.

Finalizando, segue uma pequena demonstração destas variações de métodos e suas sobrecargas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
procedure TfrmDW0005.ShowSQL;
var
  lSelect: IAqDBSQLSelect;
begin
  lSelect := TAqDBSQLSelect.Create('any_table');

  lSelect.CustomizeCondition.AddColumnEqual('intcolumn', 1).
    AddColumnEqual('boolcolumn', True).
    AddColumnGreaterEqualThan('datecolumn', EncodeDate(2018, 2, 28)).
    AddColumnIsNull('nullablecolumn');

  ShowMessage(FEmployeeConnection.Adapter.SQLSolver.SolveSelect(lSelect));
end;

E o resultado obtido (formatado para melhor leitura):

1
2
3
4
5
6
7
8
9
select
  *
from
  any_table
where (
  intcolumn = 1 and
  boolcolumn = True and
  datecolumn >= '2018.02.28 00:00:00:000' and 
  nullablecolumn is null)


E para o momento era isto! Em algum momento da série voltaremos às interfaces de abstração para cobrir todas as variações implementadas, mas acredito que o conteúdo até aqui descrito já seja suficiente para orientar o usuário do DROP, mostrando de onde estas interfaces vêm, como vivem, como se reproduzem, e do que se alimentam.

Até o próximo episódio!

Episódio Anterior

Você está fazendo isso errado! Tópico #2

 Fala galera! Como estão todos? Sejam bem-vindos ao primeiro post de 2021. Continuando o tema "Você está fazendo isso errado", va...