Hibernate - Relacionamento ManyToMany Sem Atributos

Para aqueles que trabalham com Java, é quase que improvável a não utilização do framework de persitência Hibernate. Sabemos que ele é muito poderoso e trabalha de diversas formas, por isso irei mostrar, neste primeiro de 3 artigos, como trabalhar com um relacionamento que atormenta muitos desenvolvedores, o ManyToMany.

Objetivo:

Utilizando o Hibernate, será mostrado uma forma simples de criar uma tabela intermediária em um relacionamento ManyToMany que não possui atributos e estado.

Cenário:

Teremos uma tabela Perfil e uma tabela Acesso, onde poderemos ter um perfil com vários acessos e um acesso poderá estar em vários perfis.

Análise:

Quando estamos fazendo o nosso diagrama de classe e temos uma entidade A que pode estar relacionada a várias entidades B e esta entidade B pode estar relacionada a várias entidades A, temos um relacionamento muitos para muitos:

Este relacionamento requer uma nova entidade para representar uma tabela intermediária para manter o(s) relacionamento(s) da(s) chave(s) da entidade A com a(s) chave(s) da entidade B.

Solução:

Reparem que a tabela intermediária AcessoPerfil não possui nada além das chaves estrangeiras da tabela Perfil e Acesso, ou seja, ela não possui estado já que nem agregaria valor pra gente manipula-lá diretamente. Sendo assim ela só sevirá para armazenar os relacionamentos. Com isso uma surrogate key (Chave Primária) se faz necessária neste caso.

Agora vamos criar as entidades para representar estas tabelas e seus relacionamentos.

Acesso.java

@Entity
public class Acesso {

 @Id
 @GeneratedValue
 private Long id;
 private String nome;

 // getters e setters

}

A entidade Acesso é bem simples, possuindo um nome e uma chave primária gerada automáticamente pelo banco.

Perfil.java

@Entity
public class Perfil {

 @Id
 @GeneratedValue
 private Long id;
 private String nome;

 @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
 @JoinTable(
  name = "AcessoPerfil",
  joinColumns = @JoinColumn(name = "perfil_id"), inverseJoinColumns = @JoinColumn(name = "acesso_id")
 )
 private Collection<Acesso> acessoList;

 // getters e setters

}

No Perfil também temos um nome e uma chave primária gerada pelo banco, porém também possui uma lista de Acessos, já que um perfil TEM vários acessos. É bem simples deduzir que aqui é o lugar da lista, mas podemos nos confundir na hora do mapeamento do Hibernate.

O erro mais comum é pensarmos no @OneToMany, já que um perfil tem vários acesso. Não podemos anotar esta lista assim, pois se pensarmos na nossa necessidade contrária, um acesso deve poder pertencer a vários perfis. Este erro fica mais claro quando tentamos salvar dois perfis distintos relacionado a um mesmo acesso, já que recebemos um erro do Hibernate dizendo que o código do acesso não é único na tabela intermediária, ou seja, acesso_id esta configurado como unique. E é assim que deveria ser, mesmo você querendo salvar vários acessos para um perfil. Veja que se você deixar um acesso pertencer a mais de um perfil, o relacionamento inverso (Acesso -> Perfil) deixará de ser @ManyToOne, já que ele estará ligado a mais de um perfil, e passará a ser @OneToMany. Oras, se precisamos de um relacionamento um para muitos em ambos os lados devemos utilizar o @ManyToMany para que seja criada a tabela intermediária para manter os relacionamentos das chaves e ai sim poderemos ter vários perfis relacionados a vários acessos e vice-verso.

O @ManyToMany deve estar configurado com cascade para PERSIST e MERGE que são as ações de salvar e atualizar, pois assim é possível o Hibernate salvar o Perfil e em uma ação cascateada salvar cada item da lista de acesso, que em nível de tabela é representado por uma chave na tabela intermediária. Não se preocupe com o REMOVE e os orphans, pois isso já é manipulado automáticamente.

O legal desse relacionamento many-to-many sem atributos é que não precisamos criar a tabela intermediária explícitamente, pois o Hibernate já cuida disso pra gente. Mas e se eu quisesse personalizar os nomes das FKs (Foreing Keys) e o nome da tabela? Bem, isso é possível através da anotação @JoinTable que possui um parâmetro chamado name no qual indica o nome da tabela a ser criada, que demos o nome de AcessoPerfil. Além disso podemos dar nome aos campos das FKs através de um outro atributo chamado joinColumns, que será o nome da coluna da tabela principal (Perfil), na qual você esta fazendo a anotação, e o atributo inverseJoinColumns que será o nome da chave da tabela secundária (Acesso). Temos um único detalhe na hora de dar o nome, que em vez de usarmos = "nome", precisamos de uma anotação para encapsular este nome chamada de @JoinColumn que também possui o atributo name. Sendo assim damos o nome da seguinte forma: = @JoinColumn(name = "nome").

Veja um exemplo de como seria o código para salvar dois perfis distintos onde ambos teriam os dois mesmos tipos de acessos.

Programa.java

public static void main(String[] args) {
  EntityManager manager = JPAHelper.getEntityManager();

  Acesso acesso1 = new Acesso();
  acesso1.setNome("acesso-1");
  acesso1 = manager.merge(acesso1);

  Acesso acesso2 = new Acesso();
  acesso2.setNome("acesso-2");
  acesso2 = manager.merge(acesso2);

  Collection<Acesso> acessoList = Arrays.asList(acesso1, acesso2);

  Perfil perfil1 = new Perfil();
  perfil1.setNome("perfil-1");
  perfil1.setAcessoList(acessoList);
  manager.merge(perfil1);

  Perfil perfil2 = new Perfil();
  perfil2.setNome("perfil-2");
  perfil2.setAcessoList(acessoList);
  manager.merge(perfil2);

  JPAHelper.close();
}

Neste código acima criamos dois acessos e os salvamos gerando as chaves número 1 e 2 na tabela Acesso. Em seguida colocamos esses dois acessos em uma lista para vinculá-la aos perfis. Então criamos dois perfis e dissemos que cada um deles estará relacionado com os dois acessos criados. Ao salvarmos os perfils teremos o seguinte resultado:

Assim vemos que o perfil 1 tem o acesso 1 e 2, assim como o perfil 2 também possui o acesso 1 e 2. Deste modo basta alimentar a lista de acesso do perfil e o salvar que a propagação do relacionamento da tabela intermediária será automático.

http://github.com/wbotelhos/hibernate-manytomany-sem-atributos