Hoje, inicio uma série de artigos aqui na coluna de Clojure do iMasters. Estou contente em poder contribuir com as grandes comunidades apoiadas pelo iMasters e não tenho dúvida de que a comunidade Clojure será uma delas.
Escolhi começar falando sobre uma das dos aspectos quase inevitáveis de uma aplicação nos dias de hoje: acesso a bancos de dados relacionais. É um artigo básico para começarmos a nos ambientar com clojure e caminhar para libs e aplicações da linguagem mais complexas.
Mesmo ainda na sua juventude, clojure conta com diversas libs de boa qualidade para acesso a bancos relacionais. Exemplos: java.jdbc, clojure.contribg.sql, HoenySQL, e SQL Korma . A lib aqui escolhida foi SQL Korma.
Korma é uma lib sofisticada que permite, através de uma DSL, que o desenvolvedor escreva código clojure para manipulação de dados em um banco relacional sem necessitar de strings SQL. No final, essa lib vai deixar seu código mais:
- legível – Não só por permitir o uso puro de clojure, mas pela forma como sua DSL foi construída.
- “testável” – Korma permite que qualquer consulta tenha sua execução simulada, através do uso da função dry-run, que apenas retorna uma string contendo a query que seria executada no banco de dados.
- reutilizável – o que mais me impressiona no SQL Kormal é a possibilidade de compor queries. Isto é, você define uma ou mais queries básicas e vai juntando de acordo com a necessidade.
Antes de vermos a lib com um pouco mais de detalhes, vejamos o que elas nos oferece. Vou tentar manter um paralelo com outros frameworks de acesso a banco para tornar as coisas familiares:
Feature | O que Korma provê? |
Mapeamento entre resultados de uma query para objetos de domínio | Kormal faz o mapeamento para estruturas de dados padrão clojure como maps e vetores. |
Event listeners de cliclo de vida de entidade (postUpdate, preUpdate, postPersist etc.) | Korma oferece apenas 2 possibilidade de interferir no ciclo de vida da entidade: prepare (uma função a ser aplicada a um registro antes de ser persistido) e transform (uma função aplicada a cada item retornado em uma consulta). |
API para Criteria | SQL Kormal permite criar consultas distintas e fazer a composição delas em consultas mais complexas ou que dependam de um contexto. Use o seu próprio código clojure para esse fim. |
Relacionamentos entre entidades | Todos os relacionamentos são possíveis |
Geração de IDs | Isso deve ser cuidado pelo programador diretamente |
Permite agregação | A DSL para consulta permite todas as agregações fornecidas por uma base relacional como AVG, SUM, COUNT |
Transação | O mínimo de transação é disponibilizado. Para isso, o código precisa ser passado como parâmetro para a função transacion. |
Dada a tabela acima, o mais sensato é imaginar que SQL Kormal é uma abstração para geração de SQL. Ele não é um ORM (Object Relational Mapper) e nem um gestor de entidades em um grafo de objetos interconectados. Logo, o conceito de entidade e relacionamento em Kormal são puramente com propósito de gerar consultas corretas e nada mais.
Considere as seguintes entidades:
(defentity email (database mydb)) (defentity person (has-one email) (database mydb))
A entidade email representa a tabela email. A função database logo em seguida ligaria a entidade à uma dada configuração de acesso a um banco de dados, que aqui chamei de mydb. Note que não é necessário sequer definir os campos contidos na tabela. É opcional. Nesse caso, uma consulta às entidades retorna todos os seus campos e respectivos valores como um map. Veja:
(select person (with email))) ; Deve reornar o seguinte: [{:id 1, :name "Paulo Suzart", :age 29, :id_2 1, :email "paulosuzart@gmail.com", :person_id 1} {:id 2, :name "Rafael Felini", :age 27, :id_2 2, :email "rafael.felini@gmail.com", :person_id 2}]
Mesmo sem apresentar de antemão o esquema das tabelas, agora fica claro o que cada uma contém, pois o select retornou todos os campos de cada tabela.
O resultado se assemelha a um objeto java JDBC Resultset. A diferença é que isso é uma estrutura de dados nativa clojure sem nada associado a ela (sessions, connections etc.), por isso você pode repassar esse map para camadas mais altas da aplicação exatamente como retornou do banco de dados.
No exemplo, a função (with email) diz ao Korma que retorne entidade email associada, se houver, à entidade person, que foi previamente mapeada com has-one.
Você pode utilizar a função transform do Korma para transformar o resultado de uma query, ou mesmo criar outros tipos de objetos a partir do resultado. Uma aplicação para essa funcionalidade seria nos casos em que se deseja abstrair os nomes das colunas do banco de dados. Isto é, os valores de um registro não estariam mais acessíveis pelas suas chaves no map retornado pelo Korma, mas por chaves definidas em um record clojure.
(defrecord Person [id name age email]) (defentity person (transform #(Person. (:id %) (:name %) (:age %) (:email %))) (has-one email) (database mydb)) ;; Note a diferença no resultado do mesmo select (select person (with email))) (#:user.Person{:id 1, :name "Paulo Suzart", :age 29, :email "paulosuzart@gmail.com"} #:user.Person{:id 2, :name "Rafael Felini", :age 27, :email "rafael.felini@gmail.com"})
A função transform recebe como parâmetro uma função que irá então transformar cada resultado de uma consulta em um record clojure. Nesse caso, Person é o record – que guarda o e-mail. Kormal se preocupa com o join entre as tabelas, obedecendo à convenção de que a tabela email possui uma coluna de nome person_id.
Por um lado, essa estratégia vai fazer seu código nas camadas mais altas menos acoplado a detalhes do banco de dados. Assim, mudanças nas tabelas provocam alterações apenas na função transform da entidade person. Por outro, ela força com que a função transform (associada à entidade) saiba como transformar qualquer query executada contra aquela entidade, em um record com as informações desejadas.
Por fim, a melhor parte. Compondo consultas:
(def base (-> (select* person) (order :name)) (-> base (with email) (select))
Aqui, em vez de passarmos a query inteira ao Korma toda vez que queremos executá-la, abstraímos o básico da consulta no símbolo base. Isto é, ao efetuar o join com a tabela email, a query acima vai utilizar a query básica, que por sua vez utiliza a ordenação pelo nome da entidade person.
Korma permite criar composições tão complexas e flexíveis o quanto quisermos, aumentando o reuso do código de forma incrível.