Atualizações indevidas com a JPA
Entenda como o contexto de persistência da JPA pode causar atualizações inesperadas no banco de dados
Um dos recursos mais interessantes da JPA está relacionado em como ele gerencia o estado e o ciclo de vida das entidades carregadas na aplicação. Este recurso é conhecido como contexto de persistência. Este contexto é responsável por manter a sincronização das entidades em memória com suas respectivas tuplas no banco de dados.
Toda entidade carregada ou persistida através da EntityManager
é colocada no contexto de persistência e gerenciada pela JPA, dessa forma, qualquer modificação numa entidade que se encontra dentro desse contexto é sincronizada imediatamente com o banco de dados durante o commit da transação. Por exemplo, imagine que temos um método com a finalidade de alterar o status de um determinado bug, como a seguir:
@Service
public class BugsService {
@PersistenceContext
private EntityManager entityManager;
@Transacional
public void fecha(Integer id) {
Bug bug = this.entityManager.find(Bug.class, id);
bug.setStatus(Status.FECHADO);
this.entityManager.merge(bug);
}
}
Ao fim da chamada do método, o Spring se encarregará de comitar a transação e o provider da JPA irá disparar um UPDATE
para o banco de dados. O que muita gente não sabe, é que podemos remover a chamada ao método merge
da EntityManager
e nosso código continuará funcionando:
@Transacional
public void fecha(Integer id) {
Bug bug = this.entityManager.find(Bug.class, id);
bug.setStatus(Status.FECHADO);
}
Apesar de não termos mais a chamada ao método merge
, a sincronização com o banco de dados ainda vai ocorrer e teremos o mesmo resultado. Isso só acontece pois o bug carregado pela EntityManager
está gerenciado e qualquer modificação nele (como a chamada de um método setter) é de conhecimento do contexto de persistência.
Podemos considerar o contexto de persistência como o recurso mais importante e mais poderoso dentro da JPA, pois ele permite que vários outros recursos do framework funcionem e sejam disparados no momento certo, como eventos, operações em cascata, caches, lazy loading, entre outros.
Atualizações indevidas durante as consultas
O não conhecimento sobre o contexto de persistência pode trazer efeitos colaterais críticos e até irreversíveis para uma aplicação. O curioso é que estes efeitos normalmente acontecem onde menos esperamos, como por exemplo durante uma consulta ou geração de um relatório!
Imagine que tenhámos que gerar um relatório com o resultado geral de vendas de um determinado período. Além disso, precisamos aplicar algumas taxas nos valores de cada venda antes de exibir o relatório. O código poderia ser semelhante a este:
@Transactional
public List<Venda> listaAsVendasComTaxasAplicadas(Date inicio, Date fim) {
List<Venda> vendas = this.entityManager.createQuery("select v from Venda v where v.data between :inicio and :fim", Venda.class)
.setParameter("inicio", inicio)
.setParameter("fim", fim)
.getResultList();
// aplica taxas em cada venda
for (Venda venda : vendas) {
BigDecimal valorComTaxa = this.aplicaTaxas(venda.getValorTotal());
venda.setValorTotal(valorComTaxa);
}
return vendas;
}
O código parece inofensivo, contudo estamos modificando o estado de uma entidade gerenciada pela JPA, ou seja, quando a transação for comitada o contexto de persistência vai detectar as mudanças nas entidades, gerando assim um UPDATE
para cada entidade que teve seu estado de fato modificado. Não era bem o que queríamos, certo?
Este tipo de código é muito comum nas aplicações e normalmente o problema é detectado tarde demais, isto é, quando a aplicação já está em produção e os dados do cliente foram corrompidos. Para piorar a situação, este é um tipo de problema muito difícil de detectar, pois a maioria dos desenvolvedores não encararia as consultas de relatórios como algo que pudesse modificar os dados da aplicação.
A atualização indevida pela JPA se intensifica ainda mais quando trabalhamos com determinados frameworks, padrões de projetos ou mesmo convenções, como por exemplo, o padrão Open Session In View, os escopos de conversação longa de alguns frameworks MVC, o contexto de persistência estendido da EntityManager
e até a convenção dos métodos transacionais de um EJB podem induzir ao problema.
Evitando o problema
Existem inúmeras soluções para resolver ou evitar o problema das atualizações indevidas pela JPA. Mas nesse post citaremos apenas 3 delas que requerem pouca ou nenhuma modificação no código, dessa forma, fica a cargo do leitor procurar outras soluções que se adequem a sua necessidade.
Sem mais demora, segue as 3 soluções:
1. Transações read-only
A primeira solução está relacionado ao controle transacional, tanto no Spring quanto no EJB podemos informar que a transação será somente leitura (read-only), como abaixo:
@Transactional(readOnly=true)
public List<Venda> listaAsVendasComTaxasAplicadas(Date inicio, Date fim) {
// mesmo código de antes
}
O atributo readOnly=true
na anotação indica ao framework de persistência, neste caso a JPA, que a transação é somente leitura, dessa forma o provider da JPA se encarregará de retornar apenas entidades em modo read-only. Quaisquer modificações nas entidades serão ignoradas quando a transação for comitada pelo container.
A vantagem de deixar explicito o @Transactional
como readOnly
é que alguns providers aproveitam esta informação e evitam o dirty-checking durante a sincronização com o banco, o que acaba poupando recursos na aplicação e no banco de dados, como memória e cpu, por exemplo.
2. Consultas read-only
Alguns providers, como o Hibernate e EclipseLink, nos permitem configurar as consultas como somente leitura, trazendo o mesmo efeito obtido pela anotação @Transactional
, como vimos acima. No caso do Hibernate, basta invocarmos o método setReadOnly
antes de executarmos a consulta, como no código a seguir:
Session session = this.getSession();
List<Venda> vendas = session.createQuery("select v from Venda v where v.data between :inicio and :fim")
.setParameter("inicio", inicio)
.setParameter("fim", fim)
.setReadOnly(true) // torna consulta somente leitura
.list();
Repare que estamos executando a consulta através da Session
do Hibernate. O mesmo também funciona para a API de Criteria do framework. Se não quiser trabalhar com a API do provider, você pode utilizar hints de consulta no lugar, como por exemplo:
List<Venda> vendas = this.entityManager.createQuery(jpql, Venda.class)
.setParameter("inicio", inicio)
.setParameter("fim", fim)
.setHint("org.hibernate.readOnly", true) // hint somente leitura
.getResultList();
A vantagem de trabalhar com hints é que continuamos usando a API da JPA em vez da API dos providers, o que pode ser muito importante em alguns projetos.
3. Utilize entidades não persistíveis
Em vez da consulta retornar um conjunto de entidades persistíveis, na qual qualquer modificação pode acarretar em atualizações no banco de dados, nós podemos por exemplo usar o recurso JPQL Constructor Expressions da JPA para retornar entidades não gerenciadas, como a seguir:
String jpql = "select
new br.com.triadworks.dto.VendaTaxada(v.id, v.data, v.valorTotal)
from Venda v
where v.data between :inicio and :fim";
List<VendaTaxada> vendas = this.entityManager.createQuery(jpql, VendaTaxada.class)
.setParameter("inicio", inicio)
.setParameter("fim", fim)
.getResultList();
Qualquer modificação nas entidade retornadas pela consulta não acarretará em sincronização com o banco de dados, já que a classe VendaTaxada
representa apenas uma venda com taxas aplicadas dentro do relatório e não possui qualquer vínculo com a JPA.
Podemos ir mais longe e trabalhar com entidades imutáveis do Hibernate através da anotação @Immutable
ou no EclipseLink com a anotação @ReadOnly
, por exemplo. Dessa forma sempre que a entidade for carregada ela estará em modo read-only.
Entenda o contexto de persistência
Como já dissemos, o contexto de persistência da JPA é o conceito mais importante dentro do framework. Ao trabalhar com JPA nós paramos de nos preocupar com SQL, mas em contrapartida somos obrigados a entender o ciclo de vida das entidades e entender o papel do contexto de persistência dentro da aplicação.
Tendo apenas um pouco de conhecimento sobre o contexto já possibilitaria desenvolvedores a evitar o problema das atualizações indevidas comentada nesse post. Além disso, dominá-lo nos permite entender como a JPA funciona de verdade e a rastrear a maioria dos problemas que ocorrem na aplicação, desde atualizações e deleções indesejadas, problemas em operações em cascata e baixa performance em operações com o banco, até o famigerado LazyInitializationException.
Se quiser aprender e entender um pouco mais sobre JPA, Hibernate e seu contexto de persistência, conheça o nosso curso de Persistência com JPA 2 e Hibernate e tenha domínio sobre estas tecnologias.
E aí, já se deparou com o problema de atualizações inesperadas na sua aplicação? Que soluções você utilizou para contornar o problema?
Desenvolvedor e instrutor na TriadWorks