6 dicas para manter a portabilidade da sua aplicação com JPA
Desenvolva e matenha aplicações multi-banco com JPA
Uma das maiores vantagens quando trabalhamos com a JPA é que não nos prendemos a detalhes específicos do banco de dados, ou melhor, do SGBD. Tanto é que em teoria só precisaríamos modificar algumas poucas configurações no persistence.xml
que nossa aplicação deveria funcionar de maneira idêntica com outros bancos de dados.
A JPA cuida de 80% dos problemas quando nossa aplicação tem que se integrar com bancos de dados distintos, ela garante a tal da portabilidade da aplicação entre bancos, ou seja, nossa aplicação passa a rodar tanto com MySQL como Oracle 11g ou outro SGBD. Nós desenvolvedores simplesmente trabalhamos com um modelo orientado a objetos e o framework faz todo o trabalho sujo de persistência. No entanto, precisamos tomar alguns cuidados para garantir que estes outros 20% da conta não dificultem a evolução e a portabilidade da aplicação.
Estes cuidados vão desde a geração automática da chave primária, consultas com SQL nativo, uso de procedures, funções e mapeamentos específicos para determinado tipo de dado de uma coluna. Iremos enumerar 6 dicas que podem ajudar a manter a portabilidade entre bancos da sua aplicação. Elas também valem se você pretende tirar um maior proveito da JPA.
Sem mais demora, as 6 dicas:
1. Deixe a JPA escolher como gerar as chaves primárias
A geração automática da chave primária depende do banco de dados. O Oracle, por exemplo, usa sequências, o MySQL usa auto-incremento, enquanto outros vão mais longe e suportam ambos os métodos, neste caso o PostgreSQL. Na JPA estes diferentes métodos de gerar e gerenciar as chaves primárias são chamados de estratégias. A especificação JPA define 4 tipos de estratégias (AUTO
, IDENTITY
, SEQUENCE
e TABLE
), na qual pode ser facilmente configurada no mapeamento da entidade. Por exemplo, para mapearmos a estratégia auto-incremento no MySQL nós teríamos o código abaixo:
@Entity
public class Bug {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
}
O problema de mapear a geração da chave primária como IDENTITY
é que este mapeamento não seria portável para Oracle, já que ele trabalha apenas com sequências. Para manter a portabilidade, precisamos usar a estratégia AUTO
, na qual a JPA decide a estratégia de acordo com o banco de dados. No caso do MySQL e MS SQL Server seria IDENTITY
, enquanto Oracle e PostgreSQL seria SEQUENCE
por padrão. Dessa forma, nosso mapeamento poderia mudar para algo como o código a seguir:
@Entity
public class Bug {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;
}
Quando não definimos a estratégia, esse é o valor utilizado por padrão.
2. Evite consultas com SQL nativo
A JPA nos permite executar consultas com SQL nativo, por esse motivo é muito tentador para um desenvolvedor partir para SQL quando ele não possui um bom domínio de JPQL ou Criteria. No entanto, é importante entender que devemos evitar isto, pois quanto mais SQL nativo utilizamos na aplicação menos portável ela é.
Obviamente que podemos escrever as consultas tentando seguir o padrão SQL ANSI, mas as chances disso acontecer são muito poucas, pois se houve a necessidade de partir para SQL é porque provavelmente não conseguimos escrever uma JPQL ou Criteria que satisfizesse os requisitos da consulta, seja performance, complexidade, integração com algum schema legado etc.
O ideal é que todas as consultas da sua aplicação sejam feitas com JPQL ou mesmo Criteria.
Se não tiver jeito e você for obrigado a escrever SQL proprietário, em vez de escrever ifs
no seu código, tente aproveitar os recursos do framework de IoC/DI da sua aplicação para que ele injete a implementação correta do seu DAO de acordo com o banco de dados.
3. Evite tipos de dados proprietários
Quando mapeamos um atributo de uma entidade para uma coluna com a JPA nós podemos definir o tipo da coluna através do atributo columnDefinition
da anotação @Column
, como a seguir:
@Column(columnDefinition="TEXT NOT NULL")
private String conteudo;
Dessa forma, se a JPA for responsável por gerar o schema do banco de dados, a coluna conteudo
será criada com o tipo TEXT
, na qual é um tipo específico do MS SQL Server. Logo, este mapeamento não funcionaria com outro banco de dados, como o Oracle, por exemplo.
O ideal é deixar a JPA inferir o tipo da coluna de acordo com o tipo Java do atributo. Neste caso, em que o atributo é uma String
, a JPA conseguiria mapeá-lo para uma coluna do tipo VARCHAR2(2000)
no Oracle ou TEXT
no MS SQL Server, por exemplo. Uma outra possível solução para o mapeamento acima seria anotar o atributo com @Lob
.
Não menos importante, também precisamos ficar atentos com o tamanho das colunas. Pois o tamanho máximo de um determinado tipo pode variar de um banco para outro.
4. Não coloque regras de negócio no banco
Não é difícil encontrar sistemas com regras de negócio dentro do banco de dados na forma de procedures, functions ou triggers. O problema desta abordagem é que matamos literalmente a portabilidade da aplicação, já que teremos que reescrever a mesma procedure para cada banco que nossa aplicação precisar se integrar.
O melhor que podemos fazer é manter as regras de negócio dentro da aplicação, dentro do código Java e tirando o máximo de proveito dos recursos da JPA. Por exemplo, em vez de usarmos triggers no banco de dados nós podemos usar listeners ou métodos de callbacks da JPA, como @PrePersist
ou @PostUpdate
.
Em casos excepcionais, onde é indicado usar procedures para usufruir do poder de processamento do banco de dados, podemos usar o novo recurso da JPA 2.1 (JSR 138) para invocar procedures.
5. Evite funções não portáveis nas consultas
O uso de funções na JPA é um caso bem especial e até delicado, apesar da JPQL definir algumas funções portáveis entre bancos de dados, algumas delas podem não ser 100% portáveis dependendo da situação, como TRIM
e LOCATE
.
Usar uma função não suportada pela JPQL pode ter diferentes resultados de acordo com o provedor de persistência. Vamos tomar como exemplo a JPQL abaixo:
select YEAR(b.criadoEm) as ano, COUNT(b) as total
from Bug b
group by YEAR(b.criadoEm)
A função YEAR
não está entre as funções suportadas pela JPA, mas ela é suportada pelo Hibernate. Além disso, quando o Hibernate encontra uma função desconhecida no meio da JPQL ele simplesmente delega para o banco na esperança de ele entender o comando. A maioria dos provedores de persistência permitem que o desenvolvedor configure programaticamente suas próprias funções, dessa forma a função passa a ser reconhecida pela JPQL.
Por esse motivo, antes de usar alguma função não suportada pela JPQL ou seu provedor de persistência, verifique se ela é portável entre bancos de dados. Se ela não o for, encontre outra função similar ou lembre-se de criar a mesma função nos demais bancos e, se necessário, configurá-la no seu provedor de persistência.
6. Tenha testes automatizados
Ter uma boa bateria de testes automatizados na aplicação é fundamental hoje em dia, principalmente se sua aplicação for multi-banco. Seu servidor de integração contínua deve rodar, de preferência, toda a bateria de testes para cada banco de dados que sua aplicação for se integrar. Dessa forma, fica fácil encontrar problemas de portabilidade em consultas e mapeamentos da JPA, além de facilitar a rastreabilidade de bugs do seu provedor de persistência durante o desenvolvimento.
No nosso curso de TDD e Testes Automatizados você aprende como escrever testes para sua camada DAO, suas consultas JPQL e acesso a dados.
Para que os problemas de portabilidade não sejam detectados somente produção, é importante que os testes na camada de persistência sejam em sua maioria testes de integração, ou seja, os testes precisam tocar o banco de dados.
Nem todas as aplicações são multi-banco
Provavelmente a maioria das aplicações que usam JPA não precisam se preocupar com portabilidade, já que normalmente elas se comunicam com um único banco de dados. Contudo, quanto mais recursos você usa da JPA ou de seu provedor de persistência maiores são os benefícios que sua aplicação pode ter, como cache de segundo nível e de consultas, lazy loading, auditoria, interceptors, multi-tenancy, entre outras.
Em casos além do trivial, nós podemos estender componentes dos provedores de persistência para registrar novas funções, tipos de dados, estratégias de geração de chave e muito mais.
Essas dicas foram aprendidas e são utilizadas pelos desenvolvedores da TriadWorks durante os serviços de consultoria em seus clientes. Além disso, a maioria delas é abordada com profundidade no curso de Persistência com JPA 2 e Hibernate, como geração de chaves primárias e escrita de consultas com JPQL e Criteria, por exemplo.
E você, já desenvolveu ou está desenvolvendo alguma aplicação multi-banco com JPA? Teria outras dicas interessantes para compartilhar?
You might also be interested in these articles...
Desenvolvedor e instrutor na TriadWorks