JDBI — Uma alternativa além de JDBC e JPA/Hibernate
Recentemente, nos últimos anos, atuei como desenvolvedor backend em projetos de uma empresa do segmento varejista, no qual recebe um volume considerável de transações, principalmente em datas convenientes e que possui uma vasta diversidade de tecnologias, ao qual vão de projetos legados com tecnologias mais antigas e projetos mais novos com tecnologias mais atuais.
Dentro do ecossistema de projetos existentes, a linguagem que foi adotada como principal é o JAVA, e quando olhamos para o aspecto de conexão e interação com banco de dados, existiam duas vertentes implementadas e sendo seguidas, o uso da biblioteca JDBC e o uso do ORM Hibernate.
Além dessas duas opções de tecnologias sendo usadas também existia uma divisão muito clara na equipe entre desenvolvedores que defendiam o uso do JDBC e desenvolvedores que defendiam o uso do Hibernate.
JDBC
O JDBC está presente em sua maioria em projetos mais antigos dentro do sistema, porém ainda ocorre sua implementação em projetos mais recentes sob a alegação de alguns aspectos que buscam justificá-lo, como por exemplo, sua performance, que devido a ser uma biblioteca de baixo nível proporciona uma interação otimizada com o banco, o que se torna muito relevante em um cenário de um ecommerce. A certeza que o desenvolvedor possui de que será executado no banco exatamente o que se escreve, sem ter a ocorrência de queries extras desnecessárias ou não otimizadas que um ORM mal implementado pode facilmente causar, também e uma das justificativas.
Embora existam essas características que apoiem o JDBC, também existem pontos que o desfavorecem, como por exemplo sua sintaxe que em muitos cenários pode exigir a escrita de muito código para tarefas rotineiras e simples, causando dificuldade de manutenção e pouca produtividade.
ORM Hibernate
Diversos projetos foram criados, principalmente os mais recentes, utilizando o framework ORM Hibernate como implementação JPA, juntamente com a abstração Spring Data JPA. O Hibernate é muito popular e tem sido muito presente em projetos desenvolvidos atualmente pela comunidade de desenvolvedores, o que justificam essa adoção é principalmente pela sua promessa de facilidade de desenvolvimento de CRUDs de forma rápida e simples, visando apoiar na produtividade e diminuição de código escrito.
Olhando a primeira vista em tutoriais com exemplos simples dispostos na internet, fica evidente um código claro e simples de escrever para fazer implementações muito rápidas que auxiliam a fazer endpoints de tela de forma muito ágil, porém utilizando-o em um cenário mais complexo que exige queries complexas ou um grande volume de transações, começam a ocorrer problemas principalmente de performance que começam a tornar discutível o seu uso, como por exemplo problemas de N+1 e problemas de mapeamento que se tornam muito difíceis de resolver de forma simples e sem impactar em outros pontos do sistema. Ao utilizar esse ORM em uma equipe que possui diversos integrantes, com diferentes conhecimentos de hibernate é muito comum ocorrerem problemas de performance, de queries que estão sendo executadas a mais ou de forma muito pouco otimizada, devido a passar despercebido pelo desenvolvedor o resultado final das queries geradas automaticamente. E a solução para esses problemas podem não ser muito simples, ao ponto de a manutenção ter um impacto tao significado que acabe diminuindo ou até mesmo eliminando a produtividade que se buscava alcançar.
JDBI
Pensando nas desvantagens e vantagens que considero que ambas as opções, JDBC e Hibernate possuem, encontrei uma terceira opção que ao meu ver consegue unir a performance que o JDBC proporciona e a produtividade que o Hibernate busca oferecer, que é a biblioteca JDBI.
O JDBI é uma biblioteca open source que atua como uma camada de abstração que roda em cima do JDBC. O JDBI fornece uma api de alto nível que facilita a interação do desenvolvedor com o banco, de forma produtiva e performática, sem a realização de queries mágicas sem seu consentimento, ele não é um ORM.
O JDBI possui duas formas de utilização, a Fluent API a nível de serviço e a Declarative API a nível interface, com uso de anotações.
1- Fluent API
2 — Declarative API
JDBI vs JDBC
Conforme comentado, um dos problemas apontados no uso do JDBC, é o fato dele ser baixo nível e trabalhoso para escrever instruções comuns de banco.
Segue abaixo exemplos de escrita de select e insert, utilizando JDBC com a sintaxe da classe NamedParameterJdbcTemplate, e JDBI nos modos interativo e fluente.
1 — Exemplo de select com múltiplas cláusulas:
1.1 — JDBC
1.2 — JDBI
JDBI Fluent API
JDBI Declarative API
2 — Exemplo de insert com múltiplos campos:
2.1 — JDBC
2.2 — JDBI
JDBI Fluent API
JDBI Declarative API
Nesses dois exemplos o JDBI demonstra uma sintaxe mais clara e fluída se comparada ao JDBC.
JDBI vs JPA/Hibernate
Conforme comentado, um dos problemas apontados no uso do JPA/Hibernate é o fato dele acabar executando muitas coisas de forma não muito clara, e que quando o desenvolvedor não possui absoluto conhecimento pode se tornar um problema.
Segue abaixo exemplos da forma simplificada de uso do Hibernate, a abstração Spring Data Jpa comparando com o uso JDBI nos modos interativo e fluente, em uma listagem de uma entidade que contém um relacionamento de 1 para N com outra entidade.
Exemplo de select com relacionamento de 1 x N
Spring data JPA Query Methods
Parece ótimo e bem conveniente, pois com uma linha esta pronta a busca de uma lista de pedidos, porém esse é um cenário clássico de N +1, onde para cada order encontrada sera feita uma segunda query para buscar os itens. Um problema grande onde um programador pouco atento pode não notar.
Então como corrigi-lo?
Alterar o mapeamento para buscar itens de forma LAZY pode parecer resolver para esse caso, mas devemos considerar que alterar o mapeamento de uma model significa impactar em outros pontos que utilizam, onde podem não precisar dos dados de itens e que mudar esse comportamento iria resolver aqui e trazer problemas em outros locais, então que opções temos?
Dentre as opções para contornar esse problema, podemos fazer a consulta utilizando uma query nativa, porém perderíamos a característica importante do hibernate de ser independente de banco de dados.
Outra alternativa mais adequada, seria mapear todas os relacionamentos das entidades com carregamento LAZY e utilizar as consultas na forma JPQL, que é uma linguagem própria do hibernate, ao qual permite sobrescrever o carregamento de items para o modo EAGER, e resolver o problema de N + 1, trazendo junto os itens sem impactar no mapeamento. Porém, será que ainda se torna conveniente e produtivo essa escrita, considerando que não podemos mais usar a sintaxe de query methods do Spring Data Jpa em casos que precisamos trazer não apenas a entidade, mas também alguma entidade relacionada? O que acontece se alguém da equipe alterar o mapeamento de items e configurar outros objetos sendo carregados de forma LAZY que não desejamos? Entramos em um loop de problemas de carregamentos indesejados que o tornam difícil de resolver, isso sem falar em problemas para escrever queries complexas e buscar atributos específicos de objetos.
JDBI Fluent
JDBI Declarative
O JDBI oferece formatos que possibilitam trazer em uma única ida ao banco relacionamento de 1 x N de forma clara, não tao fácil como JPA, mas que proporciona uma confiança muito maior de que o que esta sendo executado é realmente a sua necessidade, sem acontecer surpresas não desejáveis, que implicam em problemas sérios de performance.
Benchmark
Segue abaixo resultados obtidos através de um benchmark feito com a ferramenta JMH, onde foi feito uma série de chamadas testando as 4 operações básicas de forma indivivual: insert, select, update e delete.
Com base nesses resultados, podemos observar que obviamente o JDBC possui uma melhor performance em todas as operações, considerando ter uma escrita em baixo nível e ser a base para os outros dois. Também é possível observar que JPA foi superior nas operações de insert e update, e que JDBI foi superior nas operações de select e delete.
Embora tenhamos a constatação de que JPA/Hibernate demonstrou-se ser mais rápido nas operações de insert e update, devemos considerar os pontos já comentados onde o problema do Hibernate não é visível nesses cenários que são simples, onde não envolvem relacionamentos e montagem de queries complexas.
Conclusão
O objetivo desse artigo foi demonstrar problemas enfrentados com as duas populares opções JAVA de comunicação com o banco existentes no mercado, JDBC e Hibernate e apresentar o JDBI como uma possível opção de estudo para utilização em projetos, visando atender sob a perspectiva de performance e produtividade, resolvendo alguns problemas de suas concorrentes.
O código fonte utilizado para os testes apresentados nesse artigo encontram-se em https://github.com/jandersonrafa/jdbi-benchmark.