JPA: por que você deveria evitar relacionamento bidirecional
Conheça os problemas ao usar relacionamentos bidirecionais na JPA e entenda como resolvê-los de forma orientada a objetos
É muito comum os desenvolvedores ficarem em dúvida se devem mapear o relacionamento bidirecional nas suas entidades ou não. Por mais simples que pareça essa decisão, o uso de relacionamento bidirecional pode dificultar ou facilitar sua vida ao escrever lógicas de negócio, consultas e persistência. Mas no geral, as chances são de que tudo se torne mais complicado para você.
Imagine o modelo de carrinho de compras de uma loja virtual qualquer, onde um carrinho pode ter vários itens adicionados pelo usuário. Nós poderíamos modelar as entidades como abaixo:
@Entity
public class Carrinho {
@Id
private Integer id;
@OneToMany(cascade=CascadeType.ALL, mappedBy="carrinho")
private List<Item> itens = new ArrayList<>();
// getters e setters
}
E a entidade Item
ficaria:
@Entity
public class Item {
@Id
private Integer id;
@ManyToOne
private Carrinho carrinho;
// demais atributos como produto e quantidade
}
Mas e agora? Somente a entidade carrinho deve conhecer seus itens? Um item precisa saber a qual carrinho ele pertence? Ou talvez seja melhor mapear tudo como nas outras entidades do sistema?
Esses questionamentos são muito importantes e sempre deveríamos fazê-los a nós mesmos antes de modelar as entidades. Simplesmente copiar e colar exemplos de mapeamentos de outras entidades sem entender suas motivações levam a erros críticos no sistema, em geral inconsistência de dados e performance.
De qualquer forma, vamos deixar o relacionamento como está, isto é, bidirecional (o atributo mappedBy na anotação @OneToMany
indica o rel. bidirecional). Portanto, para inserir um novo item no carrinho e logo em seguida gravá-lo no banco bastaria um código como abaixo:
Item ipad = new Item("iPad Retina Display", 1);
Carrinho carrinho = dao.busca(id); // recarrega carrinho
carrinho.getItens().add(ipad);
dao.atualiza(carrinho); // atualiza entidade no banco
Só isso seria suficiente para inserir o item no carrinho no banco, certo? Errado!
Mesmo com cascade no mapeamento, o código acima não funcionaria em um relacionamento bidirecional. O INSERT
na tabela de itens não ocorreria! No entanto, se fosse uma relação uni-direcional a JPA se responsabilizaria de persistir corretamente o item associado ao carrinho no banco de dados.
Por que isso acontece?
Em um relacionamento bidirecional nossos objetos estariam em um estado inconsistente, e a JPA não saberia como construir a relação entre eles no banco, pois somente o Carrinho
sabe que a instância do Item
pertence a relação. O item não tem ciência do relacionamento. Para que o Item
também saiba que está em um relacionamento precisamos alterar o código para:
Item ipad = new Item("iPad Retina Display", 1);
Carrinho carrinho = dao.busca(id);
carrinho.getItens().add(ipad);
ipad.setCarrinho(carrinho); // item conhece seu carrinho
dao.atualiza(carrinho);
Repare que somos obrigados a manter as duas pontas do relacionamento cientes. Nesse momento a coisa toda muda de figura, pois em um relacionamento bidirecional a responsabilidade de garantir a consistência dos relacionamentos e dos objetos é do desenvolvedor e não mais da JPA. Caso contrário, a persistência poderá não funcionar como esperado.
Resumindo, a entidade pai precisa conhecer as entidades filhas e a entidade filha precisa conhecer a entidade pai.
O mesmo problema também ocorre ao removermos um item do carrinho! Também precisamos tomar todos os cuidados para manter a consistência do relacionamento, como podemos ver:
Item item = carrinho.getItens().remove(index);
item.setCarrinho(null); // mantém a consistência
Sem estes cuidados a JPA não tem como garantir que o relacionamento será respeitado durante as operações de persistência em cima destas duas entidades. Isso é tão sério que as chances são de que seu banco acabe em um estado totalmente inconsistente. Sabe quando um DELETE
ou INSERT
deveria acontecer mas não acontece? Pois é...
Passamos a ter um código mais complicado, muito mais suscetível a erros e cheio de cascas de bananas, o que muitas vezes leva a problemas difíceis de detectar e nos toma inúmeras horas debugando o código e em listas de discussão.
Para piorar, os frameworks MVC costumam intensificar ainda mais os erros, já que eles foram desenhados para popular as coleções sem ligar as pontas do relacionamento. E isso faz todo sentido, pois a responsabilidade é do desenvolvedor. É sua!
Minimizando o problema com orientação a objetos
Embora o código acima não seja complexo, é muito fácil para um desenvolvedor mais desatento esquecer de definir a relação em uma das pontas. Não é à toa que trabalhar com esse tipo de relação é delicado e propício a erros.
Para minimizar os problemas de inconsistência, podemos usar um pouco de orientação a objetos de tal forma que, ao adicionar um novo item ao carrinho, ambos os objetos fiquem cientes do relacionamento. Para isso, vamos definir um método na classe Carrinho
responsável por adicionar um novo Item
à coleção. No final das contas, o que queremos é um código semelhante a este:
Item ipad = new Item("iPad Retina Display", 1);
Carrinho carrinho = dao.busca(id);
carrinho.adiciona(item); // adiciona item ao carrinho
Por fim, na classe Carrinho
, teríamos a seguinte implementação do método adiciona
:
@Entity
public class Carrinho {
// ...
public void adiciona(Item item) {
this.itens.add(item);
item.setCarrinho(this); // mantém a consistência
}
}
O método adiciona
teria uma responsabilidade bem definida e garantiria a consistência do nosso relacionamento. Basicamente estamos colocando a responsabilidade no lugar certo e garantindo a consistência através de encapsulamento.
Mas e se outro desenvolvedor alterar a coleção diretamente? E se ele não usar meu método? Existem algumas técnicas para diminuir as chances de outro membro da equipe fazer isso, por exemplo...
Para que nenhum desenvolvedor trabalhe diretamente com a coleção de itens sem passar pelo novo método, podemos remover o método setItens
e, se possível, retornar uma cópia segura e imutável da coleção ao invocar o método getItens
. Nosso código ficaria como a seguir:
@Entity
public class Carrinho {
// ...
public void adiciona(Item item) {
this.itens.add(item);
item.setCarrinho(this); // mantém a consistência
}
public List<Item> getItens() {
List<Item> listaSegura = Collections.unmodifiableList(this.itens);
return listaSegura;
}
// remove o método setItens()
}
Com essa abordagem, conseguimos forçar o desenvolvedor a usar sempre o método adiciona
e, caso ele sem querer tente modificar a coleção diretamente, uma exceção será lançada:
Carrinho carrinho = dao.busca(id);
carrinho.getItens().add(new Item("Kindle", 2)); // lança uma UnsupportedOperationException
Embora tenhamos protegido nossa entidade de problemas de integridade e do mau uso da coleção por parte do desenvolvedor, a pergunta que fica é:
será mesmo que precisamos ir tão longe?
Na minha opinião, uma boa conversa e discussão com a equipe evitaria uma abordagem tão rígida... pelo menos isso tem funcionado muito bem na TriadWorks e em projetos de clientes que participamos.
Mas já adianto que ainda assim é possível ter problemas...
Bidirecional é uma decisão de design
Relacionamento bidirecional entre classes é mais uma decisão de design do que necessariamente de persistência ou JPA. Por exemplo, é preciso identificar quem é o lado forte e fraco da relação; como eles trocarão mensagens entre si e quais os tipos de operações existirão entre eles; quais classes clientes trabalharão com eles e em quais camadas. Só para citar alguns casos.
Decidir quando usar ou não vai depender de caso para caso, e em alguns casos pode mais atrapalhar do que ajudar: frameworks que usam reflections? referência circular? DTOs? serialização e deserilização para XML e JSON? registros duplicados? Pois é...
No caso da JPA, se usado corretamente, pode facilitar a escrita de consultas JPQL, diminuir idas ao banco de dados, ajudar em operações em cascata e até permitir que tenhamos regras de negócio mais claras em código. Isso dependerá do cenário e da necessidade do relacionamento.
Mas como já vimos, ter um relacionamento bidirecional também pode trazer algumas dificuldades com relação a inconsistência dos objetos, pois esta responsabilidade passa a ser do desenvolvedor e não mais do framework ORM. Se aplicarmos boas práticas de orientação a objetos, é possível garantir a consistência entre as duas pontas do relacionamento e ainda por cima ter um design mais enxuto das classes.
No geral, recomendamos evitar o mapeamento bidirecional com JPA sempre que possível, devido a estes possíveis problemas de inconsistência. Para saber mais sobre relacionamentos bidirecionais com suas vantagens e desvantagens você pode conhecer nosso curso de persistência com JPA2.
E você, já teve problemas e dores de cabeça com rel. bidirecional? Como você tem trabalhado esses relacionamentos na sua aplicação?
Desenvolvedor e instrutor na TriadWorks