De many-to-many para many-to-one com JPA
Introdução
Relacionamentos em bancos de dados dificilmente são tão simples quanto parecem, especialmente quando você começa a utilizar relacionamentos “N:N” (muitos-para-muitos), esse tipo de relacionamento, um extremamente comum no mundo real, normalmente é um pouco mais complicado quando é abstraído para um banco de dados relacional. Neste material você vai entender como transformar um relacionamento N:N em um N:1 utilizando JPA.
Para continuar você deve ter conhecimentos da biblioteca de persistência do Java, a JPA, e do framwework Hibernate. Os exemplos mostrados são, na verdade, testes do JUnit, então ter conhecimento conhecimento básico de o que ele é e para que serve vão lhe ajudar a entender melhor os exemplos. Você pode fazer o download do projeto de exemplo aqui, o projeto é um projeto comum do Eclipse mas também é um projeto do Maven.
Relacionamento “muitos-para-muitos”
Quando estamos iniciando a análise dos nossos sistemas orientados a objetos e começando a montar o banco de dados que vai dar suporte e persistência a esse modelo, é comum que encontremos relacionamentos do tipo “muitos-pra-muitos” (many-to-many – N:N). Nesse tipo de relacionameto entre duas tabelas, nós criamos uma tabela de ligação, que contém sempre um par de colunas, onde cada uma aponta para uma chave primária de uma das tabelas que fazem parte do relacionamento, como no diagrama do nosso exemplo abaixo:
Imagem 1 – Diagrama de exemplo com many-to-many
O nosso relacionamento demonstra que um cliente pode ter vários produtos, algo que poderia ser utilizado em um sistema de inventário.
Vejamos o código em Java utilizando JPA que exemplifica esse diagrama:
Listagem 1 – Persistivel.java
package alinhavado; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.MappedSuperclass; @MappedSuperclass public class Persistivel { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id; // métodos get/set }
A nossa primeira classe não é exatamente uma classe do sistema, mas uma classe básica para evitar a repetição de código desnecessária, nela nós declaramos o código que define a propriedade “id” que é o identificador de cada linha das tabelas no banco de dados e também definimos o tipo de gerador para a coluna como sendo “identity”, que auto-incrementa automaticamente o valor da coluna.
Como essa classe não representa uma tabela ou uma entidade no sistema mas nós queremos que as suas propriedades existam para as suas subclasses, nós definimos ela com a annotation “@MappedSuperclass”, assim, qualquer objeto que herdar dela vai automaticamente herdar os campos que foram definidos com as anotações do JPA, portanto nenhum dos objetos do nosso exemplo precisa definir uma propriedade “id”, ela já foi definida na superclasse. Usando @MappedSuperclass você evita repetição de código desnecessária par seus objetos e ainda garante que todos vão ter as mesmas propriedades e comportamentos, graças a herança.
Listagem 2 – Cliente.java
package alinhavado; import java.util.HashSet; import java.util.Set; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.OneToMany; @Entity @Table(name=”clientes”) public class Cliente extends Persistivel { private String nome; @ManyToMany(cascade=CascadeType.ALL) @JoinTable(name="clientes_produtos", joinColumns= @JoinColumn( name = "cliente_id"), inverseJoinColumns= @JoinColumn(name = "produto_id") ) private Set<Produto> produtos = new HashSet<Produto>(); // métodos get/set }
A classe Cliente herda de Persistivel (e consequentemente tem como @Id a propriedade “id” de Persistivel) além de ter uma coleção de produtos relacionados a ela na forma de um relacionamento N:N.
Listagem 3 – Produto.java
package alinhavado; import javax.persistence.Entity; import javax.persistence.Table; @Entity @Table(name="produtos") public class Produto extends Persistivel { private String nome; //métodos get/set }
O nosso objeto Produto também herda de Persistivel e não tem nada além de uma única propriedade, ele não precisa estar relacionado diretamente nem a Cliente nem a Item. Quando estiver fazendo o mapeamento de relacionamentos, evite utilizar relacionamentos bidirecionais, só faça com que os dois lados de um relacionamento se conheçam se isso for estritamente necessário para que o código esteja correto, no nosso exemplo não interessa a um produto saber quais clientes o tem, interessa apenas ao cliente saber quais produtos ele possui, então o melhor a se fazer é não colocar o relacionamento também em Produto.
Transformando um “muitos-para-muitos” em um “muitos-para-um”
Continuando com o nosso exemplo, também é comum que conforme o nosso conhecimento sobre o problema em si aumente e a modelagem evolua, esses relacionamentos N:N comecem a tomar corpo de forma que eles deixam de ser apenas um simples relacionamento e se transformam em uma entidade própria, com suas próprias informações e ciclo de vida. Relacionamentos N:N são, no fim, incomuns em sistemas reais, porque na maior parte das vezes nós temos que guardar informações sobre o relacionamento em si e simplesmente colocar novos atributos na tabela de qualquer forma torna o modelo do banco de dados difícil de se lidar e complica o modelo de objetos que vai agir sobre ele. Vejamos um teste de exemplo do código que faria uso dessa modelagem:
Listagem 4 – Teste do relacionamento many-to-many
Cliente cliente = new Cliente(); cliente.setNome( "José" ); Produto produto = new Produto(); produto.setNome("Camisa de banda"); cliente.getProdutos().add(produto); EntityManager manager = HibernateLoader.createEntityManager(); manager.getTransaction().begin(); manager.persist( cliente ); manager.getTransaction().commit(); Assert.assertTrue( mensagem( "clientes" ) , contarClientes(manager) > quantidadeDeClientes ); Assert.assertTrue( mensagem( "produtos" ) , contarProdutos(manager) > quantidadeDeProdutos ); manager.close(); manager = HibernateLoader.createEntityManager(); Cliente clienteDoBanco = manager.find( Cliente.class , cliente.getId()); Assert.assertTrue( "A quantidade de produtos do cliente deve ser maior que zero", clienteDoBanco.getProdutos().size() > 0 ); manager.close();
Testar o código que se escreve é não apenas normal, como também obrigatório pra que se consiga software de qualidade nos dias de hoje, por isso o nosso exemplo é um teste escrito utilizando a biblioteca de testes JUnit. O código cria um Cliente, um Produto e relaciona o produto ao cliente, após isso nós começamos a testar as funcionalidades implementadas, primeiro nós testamos se a quantidade de clientes e produtos no banco de dados se alterou (os métodos “contarProdutos()“ e “contarClientes()”, “mensagem()” são métodos utilitários da nossa classe de testes que você pode conferir nos arquivos desse tutorial, a classe HibernateLoader é apenas uma classe utilitária criada no exemplo para criar os EntityManagers), após garantir que as quantidades foram alteradas, nós vemos se o produto realmente foi relacionado ao cliente. Para fazer esse último teste, nós criamos um novo EntityManager, isso foi necessário porque algumas implementações da JPA (como o Hibernate) mantém os objetos em um cache no próprio EntityManager, portanto se eu tentasse carregar o Cliente com o mesmo EntityManager que o salvou ele simplesmente me retornaria o objeto “cliente” que estava no seu cache em vez de fazer uma nova consulta no banco de dados.
Tomando como base o nosso exemplo anterior, digamos que agora nós precisemos saber exatamente qual a quantidade de um produto específico um cliente tem, com o nosso diagrama anterior nós precisaríamos fazer uma contagem dos produtos relacionados ao cliente específico, o que é possível mas pouco prático, o melhor seria se o próprio relacionamento entre produtos e clientes já trouxesse esse relacionamento, dessa forma nós não precisaríamos ter produtos repetidos no relacionamento como também não seria necessário fazer contagens manuais, no próprio relacionamento a contagem já estaria feita. Vejamos como esse diagrama ficaria agora:
Imagem 2 – Diagrama de exemplo com many-to-one
Agora nós não temos apenas uma tabela que liga os dois objetos, mas uma entidade própria, que tem seus próprios atributos e representação dentro do sistema. O nosso item representa o relacionamento entre as tabelas clientes e produtos, além de conter informações que caracterizam o relacionamento, que no nosso caso é a quantidade de produtos que o cliente tem. A tabela de relacionamento “clientes_produtos” não precisa mais existir, pois a nova tabela “itens” já faz o trabalho dela. Vejamos agora como ficariam os códigos para esse nosso novo modelo:
Listagem 5 – Novo Cliente.java
package alinhavado; import java.util.HashSet; import java.util.Set; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.OneToMany; import javax.persistence.Table; @Entity @Table(name="clientes") public class Cliente extends Persistivel { private String nome; @OneToMany(mappedBy="cliente", cascade=CascadeType.ALL) private Set<Item> items = new HashSet<Item>(); // métodos get/set }
O nosso cliente agora não mais se relaciona diretamente com os produtos, agora ele se relaciona com os itens, que por fim vão ser o relacionamento com os produtos. E já que falamos neles, vejamos a nossa classe Item:
Listagem 6 – Item.java
package alinhavado; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; @Entity @Table(name="itens") public class Item extends Persistivel { private Integer quantidade; @ManyToOne(cascade=CascadeType.ALL) @JoinColumn(name="produto_id") private Produto produto; @ManyToOne(cascade=CascadeType.ALL) @JoinColumn(name="cliente_id") private Cliente cliente; // métodos get/set }
É na classe Item que reside agora o nosso relacionamento, ela contém uma referência para um Cliente e também para um Produto, além de guardar a quantidade de produtos que esse Item representa. Vejamos o exemplo de código que mostra esse relacionamento sendo utilizado:
Listagem 7 – Exemplo do relacionamento many-to-one
Cliente cliente = new Cliente(); cliente.setNome( "José" ); Produto produto = new Produto(); produto.setNome("Camisa de banda"); Item item = new Item(); item.setQuantidade( 10 ); item.setCliente(cliente); item.setProduto(produto); EntityManager manager = HibernateLoader.createEntityManager(); manager.getTransaction().begin(); manager.persist( item ); manager.getTransaction().commit(); Assert.assertTrue( mensagem( "clientes" ) , contarClientes(manager) > quantidadeDeClientes ); Assert.assertTrue( mensagem( "produtos" ) , contarProdutos(manager) > quantidadeDeProdutos ); Assert.assertTrue( mensagem( "itens" ) , contarProdutos(manager) > quantidadeDeItens ); manager.close(); manager = HibernateLoader.createEntityManager(); Cliente clienteDoBanco = manager.find( Cliente.class, cliente.getId()); Assert.assertTrue( "A quantidade de itens deve ser maior do que zero", clienteDoBanco.getItems().size() > 0); manager.close();
Como você pode perceber, as diferenças do código são pequenas, nós criamos um Cliente, um Produto e em vez de simplesmente relacionar os dois, nós criamos um novo objeto, o Item, que guarda uma referência para o Cliente e outra para o Produto, além disso ele também conta com uma propriedade, a quantidade. Seguindo no teste nós validamos que agora existem mais clientes e produtos que antes, além de ver se o item foi realmente relacionado ao cliente em questão no último teste.
Conclusão
Relacionamentos N:N podem ser transformados de forma simples em relacionamentos N:1 quando você precisa guardar informações sobre a relação em si, você não deve, em momento algum, criar uma nova coluna em uma tabela de ligação e continuar tratando ela como sendo apenas uma tabela de ligação, se o relacionamento começar a ter propriedades próprias, é porque ele não é mais apenas um relacionamento, mas uma entidade real do seu sistema e deve começar a ser tratado como tal.
Referencias
Documentação oficial do Hibernate. Disponível em: http://hibernate.org/, acesso em 30/12/2007.
Muito bom o post… me ajudou bastante. Parabéns
Marcos Jordão''
fevereiro 5, 2008
Obrigado! 🙂
Bastante interessante seu texto, valeu a leitura!
Renan
março 22, 2008
Muito bom o post Maurício, simples e bem objetivo!
Só tenho um questionamento, você se utilizou da classe concreta Persistivel para aproveitar códido e digitar menos. Dai te pergunto, você se utilizou por questões didáticas apenas? Pois acredito que não seja uma boa prática se utilizar de herança somente para “reaproveitar” código, principalmente em entidades que representam o seu domain model.
Enfim, parabéns pelo post!
Abraços!
Rafael Ponte
julho 8, 2008
Opa Rafael,
Eu não vejo problema em usar essa classe base, ela só tem implementações default de coisas que você teria que implementar de qualquer forma e ela não é obrigatória, você poderia simplesmente remover ela da sua árvore de herança se necessário. D equalquer forma, reaproveitar código é um dos motivos da herança existir 🙂
Não devemos fugir da herança, devemos apenas ter cuidado ao utilizá-la.
Maurício
julho 8, 2008
Só uma opinião (ou pergunta) relacionada a Classe Persistivel.
A idéia é legal: “uma classe básica para evitar a repetição de código desnecessária”.
Contudo, java não suporta herança multipla(comexeceção da simulação com interfaces que neste caso aumentariam a complexidades dos pojos seus, sem necessidade).
Sendo assim, não poderemos utilizar outras heranças, que às vezes modelo exige ( ex.: Classes funcionário e servidor que herdam de usuário, isto é , funcionário e servidor são usuários do sistema).
marcos eduardo
novembro 29, 2008
Considerando que Usuario já herdaria de Persistivel, Funcionário e Servidor já teriam herdado.
Outra coisa importante é que muitas vezes esse tipo de relacionamento é feito com herança (Usuario/Servidor/Cliente/Fornecedor) quando na verdade eles deveriam ser tratados como os diversos papéis que um usuário deve ter, é por isso que muitas vezes existem Clientes que são também Fornecedores dentro de um sistema, apenas porque a herança foi utilizada de forma incorreta.
Maurício Linhares
novembro 29, 2008
Muito bom mesmo
quem dera se todos os post´s fossem assim
simples e objetivo
Alexandre
dezembro 11, 2008
Muito explicativo… Só estou tendo um problema…. Ao persistir o objeto acontece esse erro:
Foreign key (FKDDA83D7695B348FD:objetos_perfil_web [perfil_web_id])) must have same number of columns as the referenced primary key (objetos_perfil_web [perfil_web_id,objeto_web_id])
Alguém passou por isso?
Daian Henz
março 20, 2009
Muito bom o tópico Maurício, de fato me ajudou, já que pude abandonar todos aqueles aparatos de EmbeddedId e afins.
Só alerto para não deixar o CascadeType.ALL na tabela Item, pois ao remover um Cliente que tenha o Produto, por exemplo, macbook, todos os clientes que também tenham este produto serão removidos. É sempre importante evitar o cascade quando possível.
Também utilizo o ID por herança e costumo colocar o nome de GenericEntity. (:
Washington Botelho
março 13, 2010
shoowwwwwww, parabéns cara!
salvou a minha vida!!!
Sergio
setembro 29, 2010