Utilizando Perl para acessar múltiplos bancos de dados

No cenário de hoje a nossa empresa possui diversos departamentos, cada um com seus próprios sistemas, provendo as mais variadas funcionalidades, cada um com suas especificidades, sendo escritos em linguagens diferentes e acessando bancos de dados diferentes, etc.

Nós somos responsáveis por produzir um relatório que possui dados de dois departamentos diferentes cujos sistemas não se conversam.

A solução canônica hoje em dia é ter uma arquitetura baseada em serviços, eventualmente com APIs REST, onde só é preciso enviar um request e está feito o trabalho.

Mas a nossa empresa ainda não chegou lá e vamos precisar lidar com os sistemas legados.

Para resolver esse problema, vamos usar o DBI.

O que é DBI?

O DBI é uma interface pra acesso a bancos de dados que é independente do tipo de banco de dados, ou seja, não importa se é MySQL, MariaDB ou Postgres, ele vai oferecer o mesmo conjunto de métodos.

Dessa forma a gente só precisa se familiarizar com a API do DBI que vamos conseguir interagir com qualquer banco de dados.

Para isso o DBI utiliza um conjunto de "drivers", um para cada banco de dados, os módulos DBD::*, que abstraem as partes que são específicas de cada engine.

Se você for na busca do metacpan, digitar DBD:: e esperar ele sugerir uma lista de opções, vai ver uma quantidade razoável de engines suportadas pelo DBI.

O modus operandi padrão é você se conectar no banco, utilizar os métodos de acesso aos dados e por fim desconectar.

#!/usr/bin/env perl

use DBI;

$dbh = DBI->connect($data_source, $username, $auth, \%attr);

$sth = $dbh->prepare($statement);
$rv = $sth->execute(@bind_values);
$rows = $sth->fetchall_arrayref( $slice, $max_rows );

$rc = $dbh->disconnect;

A documentação do DBI é bem tranquila, especialmente se você está familiarizado com esse passo a passo de lidar com banco de dados.

O DBI também suporta a criação de transactions, ou como conhecemos, transações. Assim você pode utilizar um conjunto complexo de instruções SQL de forma atômica, claro se o banco de dados que você estiver usando permitir esse tipo de coisa.

O DBI é bastante configurável, então você pode personalizar bastante as conexões, desde o encoding padrão, o comportamento do tratamento de erro, ativar ou dessativar funções e muito mais.

Dê sempre uma olhada na documentação, tanto do DBI quanto do módulo DBD::* que você estiver utilizando para poder encontrar a sintaxe correta da opção que você estiver precisando.

O Problema

A nossa empresa fictícia fornece cursos diversos para seus clientes. O Departamento de Vendas nos pediu um relatório com todos os alunos aprovados nos nossos cursos, contendo cada curso onde esses alunos foram aprovados, de modo a poder enviar emails sugerindo outros cursos.

O problema é que a informação parece estar espalhada pela empresa, em departamentos diferentes.

O Departamento de Clientes é responsável pelo cadastro e os dados do clientes. Eles utilizam banco de dados PostgreSQL (postgres para os íntimos) e guardam numa tabela clients, dados como nome, username e email de cada cliente.

Também temos o Departamento de Cursos, que é responsável pelos cursos que a empresa oferece aos clientes. Eles utilizam banco de dados MariaDB (mariadb) e tem numa tabela course_history o nome do curso, o username do aluno e a nota do aluno no curso.

Dado que não existem serviços que possamos acessar, nosso trabalho é criar um programa que se conecte a ambos os bancos de dados[1], obtenha os dados necessários e gere um relatório em formato CSV.

Montando o cenário

Embora esteja fora do escopo deste post explicar como configurar containers, para demonstrar o nosso cenário, utilizamos docker e docker-compose para simular os diferentes bancos de dados.

Para executar o exemplo, partimos do pressuposto que você tenha instalado e configurado na sua máquina o docker, docker-compose e um alias chamado compose apontando para o comando docker-compose.

Caso você não tenha isso tudo configurado, pode pular essa seção sem prejuízos.

Resumidamente, o compose vai criar containers executando as respectivas engines que vão funcionar como mini servidores e vai mapear as portas 4001 e 4002 da máquina local para as respectivas portas dos bancos de dados em seus respectivos containers.

Assim se você quiser se conectar a cada banco utilizando uma ferramenta como o DBeaver, por exemplo, basta apontar para essas duas portas na máquina local.

Os clientes que o nosso compose disponibiliza, por outro lado, acessam as respectivas bases de dados através da própria rede interna entre os containers que ele mesmo cria.

Pra começar, baixe o código completo direto do repositório de exemplos e navegue até o diretório 2020-09/databases.

Iniciando o Postgres
user@host$ compose up -d pgdb
Creating network "databases_blog-post" with driver "bridge"
Creating databases_pgdb_1 ... done

O comando acima vai iniciar um container com o postgres em segundo plano e executar os scripts de inicialização criando a base de dados de exemplo, usuário, dados, etc.

Note que se for o primeiro comando executado, ele também vai criar uma rede interna entre os containers, chamada "databases_blog-post".

Iniciando o MariaDB
user@host$ compose up -d mydb
Creating databases_mydb_1 ... done

O comando acima, assim como com o postgres, inicializa um container mariadb e executa os scripts de inicialização criando tabelas, usuários, dados, etc.

Recomeçando do zero

Em ambos os casos, a inicialização dos dados só acontece na primeira vez que os containers são iniciados. Se você quiser apagar tudo e começar novamente, basta executar co comando abaixo:

user@host$ compose down && docker volume rm databases_pgdata databases_mydata
Stopping databases_mydb_1 ... done
Stopping databases_pgdb_1 ... done
Removing databases_mydb_1 ... done
Removing databases_pgdb_1 ... done
Removing network databases_blog-post
databases_pgdata
databases_mydata
Acessando Postgres via CLI
user@host$ compose run pg-cli
Starting databases_pgdb_1 ... done
Password:
psql (12.4 (Debian 12.4-1.pgdg100+1))
Type "help" for help.

pgdb_sample=>

Após entrar com a senha do usuário definida no arquivo .env, teminamos no prompt do psql.

Acessando MariaDB via CLI
user@host$ compose run my-cli
Starting databases_mydb_1 ... done
Enter password:
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 4
Server version: 10.5.5-MariaDB-1:10.5.5+maria~focal mariadb.org binary distribution

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [mydb_sample]>

Assim como no caso do postgres, após entrar com a senha do usuário, caímos no prompt do mysql, o cliente de linha de comando do mariadb.

Note que os comandos acima são definidos pelo arquivo docker-compose.yml presente no projeto de exemplo.

Executando o exemplo

Como vamos acessar dois bancos diferentes e os drivers do DBI utilizam as bibliotecas nativas de cada um, é preciso ter instalados os pacotes correspondentes. No ubuntu isso pode ser feito com:

user@host$ sudo apt update && sudo apt install -y build-essential libpq-dev libmariadb-dev

Em seguida utilizamos o carton para instalar as dependências:

user@host$ carton install

E executamos o exemplo com:

user@host$ carton exec -- perl ./report.pl

O relatório

O código completo está disponível no projeto de exemplo. Abaixo comentamos cada parte separadamente.

#!/usr/bin/env perl

use v5.32;
use strict;
use warnings;

use DBI;
use Text::CSV_XS qw( csv );

O trecho acima é apenas o cabeçalho padrão de programas Perl onde indicamos entre outras coisas, que vamos utilizar o DBI para acesso ao banco e o Text::CSV_XS para gerar o arquivo CSV.

## conexão ao postgres
my $pg_host = 'localhost';
my $pg_port = '4001';
my $pg_db = 'pgdb_sample';
my $pg_dsn = "DBI:Pg:dbname=$pg_db;host=$pg_host;port=$pg_port";
my $pg_user = 'blabos';
my $pg_pass = 'blabos';
my $pg_dbh = DBI->connect( $pg_dsn, $pg_user, $pg_pass );

Aqui, de forma bem vertical pra ser mais fácil de visulalizar, fazemos a conexão com o banco e obtemos um database handler que guardamos da variável $pg_dbh. É através desse handler que vamos chamar os métodos de acesso ao banco do DBI.

## conexão ao mariadb
my $my_host = '127.0.0.1';
my $my_port = '4002';
my $my_db = 'mydb_sample';
my $my_dsn = "DBI:MariaDB:database=$my_db;host=$my_host;port=$my_port";
my $my_user = 'blabos';
my $my_pass = 'blabos';
my $my_dbh = DBI->connect( $my_dsn, $my_user, $my_pass );

Analogamente, conectamos no banco e obtemos o handler em $my_dbh.

Se você estiver executando o exemplo via docker-compose, por conta de uma especificidade do mariadb, é preciso utilizar o ip ao invés no nome localhost.

As diferenças ficam por conta das duas DSN que determinam qual é o driver (DBI::Pg ou DBI::MariaDB) e as opções específicas de cada um.

## pegando a lista de estudantes no postgres
my $clients = $pg_dbh->selectall_arrayref(
'select * from "pgdb_schema"."clients" order by name',
{ Slice => {} },
);

## pegando o histórico no mariadb
my $history = $my_dbh->selectall_arrayref(
'select * from course_history where grade > 5.0',
{ Slice => {} },
);

No trecho acima utilizamos o mesmo método selectall_arrayref() pra executar as queries nos bancos, mas cada uma a partir do handler correspondente ($pg_dbh ou $my_dbh).

Em ambos os casos, o resultado vai ser um ArrayRef onde cada elemento é um HashRef correspondente a uma linha do resultado.

Na primeira query pegamos todos os alunos da base do Departamento de Clientes e na segunda, todos os cursos cujos alunos tiveram nota maior que 5.0 na base do Departamento de Cursos.

Por fim, processamos o resultado e geramos o CSV com o código abaixo:

## gerando o relatório!
my @rows = ();
foreach my $student ( $clients->@* ) {
my @courses =
map { $_->{course} } ## 3. pega o nome do curso
grep { $_->{student} eq $student->{user} } ## 2. filtra para este estudante
$history->@*; ## 1. pega todos os cursos

## estrutura de dados para o csv
push @rows,
{
name => $student->{name},
email => $student->{email},
courses => join( ', ', @courses ),
};

## apenas mostrando na tela
say '=' x 80;
say "Parabéns $student->{name} por sua aprovação em:";
say ' * ' . $_ foreach @courses;
}

## uma última linha na tela
say '=' x 80;

## gerando o arquivo csv
csv( in => \@rows, out => 'report.csv' );

Iteramos pela lista de alunos e filtramos os cursos de cada um, montando os dados do CSV com as colunas name, email e course.

Depois de iterar pelos dados e popular o array @rows, geramos o arquivo report.csv no diretório corrente, utilizando a função csv() que mostramos no post anterior.

user@host cat report.csv
email,courses,name
beltro@examle.com,"COBOL for Dummies","Beltrano de Souza"
blabos@gmail.com,"Introduction to Perl, Mastering Regexes","Blabos de Blebe"
fulano.tal@email.com,"Basics of Machine Learning","Fulano de Tal"

Note como a função csv() do Text::CSV_XS é espertinha o suficiente para lidar corretamente com os casos onde há vírgulas (mais de um curso).

Conclusão

No post de hoje mostramos como usar o módulo DBI para acessar mais de um banco de dados diferente, de forma uniforme, utilizando o mesmo conjunto de métodos.

Com a API padronizada do DBI você não precisa se preocupar em aprender como lidar com bibliotecas diferentes toda vez que precisar acessar um banco de dados diferente.

Notas

[1]. Sim, sabemos que hoje em dia não é uma boa prática deixar todo mundo sair conectando diretamente no banco de dados, mas você se surpreenderia com a quantidade de casos reais onde esse exemplo fictício é uma necessidade...