Programador ajudante e aprendiz da comunidade open source.

Hibernate – Relacionamento ManyToMany Com Atributos e Chave Composta

Neste último post da série de relacionamentos ManyToMany iremos explorar um pouco mais o poder do Hibernate. Desta vez iremos utilizar chave composta na tabela intermediária, além dos atributos próprio da tabela. Antes de ler este post recomendo a leitura dos anteriores: Hibernate – Relacionamento ManyToMany Sem Atributos e o Hibernate – Relacionamento ManyToMany Com Atributos.

Objetivo:

Utilizando o Hibernate, será mostrado como criar uma tabela intermediária com atributos e chave composta. Sendo que estas chaves serão as chaves estrangeiras (Foreign Key) das tabelas relacionadas.

Cenário:

Teremos as entidades Empresa e Usuario, sendo que uma empresa pode ter vários usuários e um usuário pode estar em várias empresas. Porém tendo a observação de que devemos garantir que um usuário nunca se repita para a mesma empresa, além de ser necessário gravar a data de vinculação deste usuário.

Análise:

Empresa x Usuario

Já estamos acostumados com essa situação e sabemos que isso resultará em uma tabela intermediária. Além disso precisamos dizer a data de vínculo do usuário à empresa, logo devemos colocar essa data na tabela intermediária na qual marca tal vínculo:

Empresa x EmpresaUsuario x Usuario

Veja que até aqui fizemos a modelagem da mesma forma que o post passado. Porém temos uma necessidade à mais: garantir que um determinado usuário não tenha mais do que um cadastro na mesma empresa. Se você foi curioso o bastante no post passado para ver o code complete da anotação @JoinColumn, percebeu que existe o atributo unique, que configurado para true torna um valor único para tal coluna. Só que esta configuração não servirá pra gente, pois queremos que o código da empresa ou do usuário se repita na coluna do banco, mas não um par específico. Poderíamos ter, por exemplo, o usuário (1) na empresa (1), assim como o usuario (2) na empresa (1), mas não estes mesmos conjuntos novamente. Para garantir que este conjunto seja único no banco precisamos transformar as chaves usuario_id e empresa_id em chave primária composta (Composite Key).

Como o nome mesmo já diz, uma chave composta se comporta como a chave primária, porém o seu valor é composto por mais de uma coluna do banco, neste caso as FKs, porém você pode adicionar quaisquers campos que desejar. Vejamos como ficará nossas classes:

Empresa:

@Entity
public class Empresa {

  @Id
  @GeneratedValue
  private Long id;

  private String nome;

  @OneToMany // Segura o mappedBy ai por enquanto. (:
  private Collection<EmpresaUsuario> empresaUsuarioList;

  // hashCode e equals

  // getters e setters

}

Não temos nenhuma novidade nesse código, já que temos os dados da empresa e uma lista de valores da tabela intermediária. Quanto a configuração da lista deixaremos para depois. Para esse artigo iremos implementar os métodos hashCode e equals que são métodos que dizem quando um objeto será considerado igual a um outro, neste caso iremos utilizar apenas o id para esta afirmação. Não tente criar na mão estes métodos, utilize a opção "Generate hashCode() and equals()..." do menu Source do Eclipse para isso ou de sua IDE preferida.

Usuário:

@Entity
public class Usuario {

  @Id
  @GeneratedValue
  private Long id;

  private String username;

  @OneToMany // Segura o mappedBy ai por enquanto. (:
  private Collection<EmpresaUsuario> empresaUsuarioList;

  // hashCode e equals

  // getters e setters

}

Na entidade do usuário também não temos novidades por enquanto, sendo que também teremos a lista da tabela intermediária. Aqui também iremos configurar mais adiante a lista e teremos o hashCode e o equals também utilizando o id da entidade.

E então temos a entidade da tabela intermediária:

EmpresaUsuario:

@Entity
public class EmpresaUsuario {

  @ManyToOne
  private Empresa empresa;

  @ManyToOne
  private Usuario usuario;

  private Date dataCadastro;

  // getters e setters

}

Nessa entidade, como esperado, possui os relacionamentos naturais das chaves e o atributo para manter a data de cadastro do usuário na empresa. E a princípio o mappedBy do usuário e da empresa apontaria para esses objetos, mas vamos aguardar, pois ainda haverá modificações.

Agora sim Botelho, você acabou de escrever exatamente o que escreveu no artigo passado. ¬¬

Bem, até aqui nós já conhecemos, mas ainda não esta claro a forma de transformar essas duas FKs em chaves compostas. Mas o Hibernate sabe um jeitinho maroto pra fazer isso através na anotação @Embeddable. Essa anotação será utilizada em uma classe que manterá apenas os atributos que farão parte da chave. Pra isso iremos criar uma nova classe:

EmpresaUsuario:

@Embeddable
public class EmpresaUsuarioId implements Serializable {

  @ManyToOne(fetch = FetchType.LAZY)
  private Empresa empresa;

  @ManyToOne(fetch = FetchType.LAZY)
  private Usuario usuario;

  // hashCode e equals

  // getters e setters

}

A nossa classe agora recebe um nome sugestivo de ID junto com a anotação @Embeddable, que diz que ela é uma classe que pode ser embutida em outra. Oras, não é isso que queremos? A idéia é embutir essa classe na nossa entidade EmpresaUsuario em forma de chave composta. Como nossa chave será as FKs, ai estão elas. Perceba que todas as entidades que formam a chave composta devem implementar Serializable, caso contrário seu código nem irá compilar. Além disso, não sendo regra mas de meu gosto, prefiro configurar os objetos como LAZY para evitar possíveis CircularReference, já que diferentemente de uma anotação @OneToMany, o @ManyToOne é EAGER por padrão. Veja que essa entidade necessita da implementação do hashCode e do equals e ai faz sentido termos implementado tais métodos no usuário e na empresa, pois aqui os implementamos usando os objetos na comparação que, no fundo, o que será utilizado é o próprio hashCode e equals das entidades. Sendo assim podemos pensar em um hash externo utilizando o hash interno. Piorou?

Neste ponto já podemos fazer um refactor e retirar os objetos usuario e empresa da entidade EmpresaUsuario, já que eles estão na chave composta e como você ai já pensou, iremos declarar essa chave/classe composta no lugar das duas FKs explícitas. E ai que vem a cereja do bolo, pois já temos uma classe que pode ser embutida, mas isso não quer dizer que ela já é ID. Devemos dizer isso para o Hibernate através da anotação @EmbeddedId ficando assim:

EmpresaUsuario:

@Entity
public class EmpresaUsuario {

  @EmbeddedId
  private EmpresaUsuarioId id;

  private Date dataCadastro;

  // getters e setters

}

Agora temos o ID composto devidamente declarado e configurado. E ai com as chaves naturais devidamente mapeadas, podemos voltar nas classes Empresa e Usuario e configurar a propriedade mappedBy apontando para essas chaves. Se você seguir o relacionamento da Empresa, por exemplo, verá que ela tem uma lista de EmpresaUsuario, porém não temos mais os objetos Empresa e Usuario nessa classe, pois eles foram jogados para dentro da chave composta, então como mapeá-los? Bem, é aqui que você aprende algo legal do Hibernate, pois ele suporta navegação dos objetos no mappedBy, ficando da seguinte forma:

@OneToMany(mappedBy = "id.empresa")
private Collection<EmpresaUsuario> empresaUsuarioList;

Veja que o atributo/classe id nós o temos na entidade EmpresaUsuario, então apenas fizemos a navegação para dentro do mesmo buscando o objeto empresa. Da mesma forma devemos fazer na entidade Usuario:

@OneToMany(mappedBy = "id.usuario")
private Collection<EmpresaUsuario> empresaUsuarioList;

Pronto! Nossa modelagem já esta pronta para ser utilizada. Veja um exemplo a seguir de como seria a ação de adicionar um usuário à uma empresa:

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

  Empresa empresa = new Empresa();
  empresa.setNome("Concrete Solutions");
  empresa = manager.merge(empresa);

  Usuario usuario = new Usuario();
  usuario.setUsername("wbotelhos");
  usuario = manager.merge(usuario);

  EmpresaUsuarioId id = new EmpresaUsuarioId();
  id.setEmpresa(empresa);
  id.setUsuario(usuario);

  EmpresaUsuario empresaUsuario = new EmpresaUsuario();
  empresaUsuario.setDataCadastro(new Date());
  empresaUsuario.setId(id);

  manager.merge(empresaUsuario);

  JPAHelper.close();
}

Neste código iniciamos criando a empresa e depois o usuário. Com eles salvos os utilizamos para formar a chave composta. Em seguida criamos a tabela intermediária com seus dados e setamos a sua chave composta que na verdade é a classe que acabamos de popular com a empresa e o usuário. Em seguida salvamos essa tabela intermediária para fazer o vínculo entre o usuário e a empresa.

Banco - Empresa x EmpresaUsuario x Usuario

Visualmente no banco fica muito parecido com o ManyToMany com atributos, porém agora as chaves estrangeiras formam a chave primária desta tabela, por isso se tentarmos inserir novamente um registro do usuário (1) na empresa (1) não iríamos conseguir, pois isto seria considerado um update para o Hibernate. Sendo assim não haverá registros repetidos no banco.

Pessoalmente eu não gosto de chave composta e até mesmo os corers do Hibernate não recomendam, porém uma hora ou outra acabamos por precisar, seja por estarmos dando manutenção em um sistema legado ou porque precisamos de adicionar integridade no banco por ter mais de um sistema acessando-o e nem todos possuirem regras de validação na camada da aplicação.

Link do projeto:

http://github.com/wbotelhos/hibernate-manytomany-com-atributos-e-chave-composta

  1. Bruno Judson 28 Mai 2015 17:25

    @Entity
    public class EmpresaUsuario {

    @EmbeddedId  //Estou com erro aqui Embedded ID class should not contain relationship mappings, como resolver?
    private EmpresaUsuarioId id;
    private Date dataCadastro;
    
  2. Renato Vieira 5 Out 2014 15:19

    Washington, ótimo artigo, consegui entender o que não aprendi em sala de aula, obrigado.

  3. Jonathan A. S. 12 Mai 2014 07:27

    Olá, gostaria de apenas contribuir com uma observação, não seria interessante usar um padrão nos nomes dos campo das tabelas ? por exemplo, você usou "username", um termo em inglês, e "nome", um termo em português, sei que é apenas didático seu exemplo, mas por que não manter uma nomenclatura padrão ? tudo em português ou tudo em inglês.

  4. rafael 14 Fev 2014 14:28

    Uma outra abordagem é criar uma nove classe normal com um id único e mapear com OneToMany e ManyToOne, porém a duplicação dos registros não seria evitada.

    Vê algo de ruim nisto ou como vc mesmo disse os coeres do Hibernate não recomendam a chave composta, então sendo esta uma solução melhor.

  5. Wesley 23 Out 2012 08:56

    Bom dia!
    Eu sou o cara do Twitter. Desde já agradeço a ajuda.
    Seguinte meu brother, eu tenho um relacionamento.
    E queria saber, como faço coloco na pesquisa, esse campo relacionando.
    Porque no hibernate, o campo fica o nome da outra classe.
    Olha só, o que eu tenho.

    Minha classe Produto.java

    @Entity
    public class Produto {
    
      //Variáveis
     @Id @GeneratedValue
     private Long id;
      ...
     //Relacionamento com ProdutoEmpresa
       @OneToMany(mappedBy = "produto",targetEntity=                      ProdutoEmpresa.class,
        fetch = FetchType.LAZY, cascade = CascadeType.ALL)
        private List<ProdutoEmpresa> produtoEmpresa;
      //Método set e get
    

    ProdutoEmpresa.java

    @Entity
    public class ProdutoEmpresa {
    
      //Variáveis
     @Id @GeneratedValue
     private Long id;
            private double preco;
     .....
    
      //Relacionamento com Produto
        @ManyToOne
        @JoinColumn(name="cod_produto")
       private Produto produto;
    

    Eu preciso pegar o código do produto, e pesquisar o menor preço.

    Agora, como eu faço para passar o código?
    Meu dao.

    //Consulta com o Hiberante para Localizar a empresa que tem o menor preço
          public List<ProdutoEmpresa> buscamenorpreco(Long id) {
            return session.createCriteria(ProdutoEmpresa.class).add(Restrictions.eq("id",id))
               .addOrder(Order.asc("preco")).list();
         }
    

    Meu Controller

    //Página do menor preço
        @Get("/produtoEmpresa/menorpreco")
        public List<ProdutoEmpresa> menorpreco(Long id) {
         return dao.buscamenorpreco(id);
    
       }
    

    Assim, eu consigo pesquisar utilizando o código id da tabela ProdutoEmpresa, só que eu preciso pegar o código do produto.

    Ja tentei.

    Produto produto passando como paramentro.
    produto.getId também não funciou.

    Por ser um relacionamento, eu não sei como chamar no Hibernate, os campos da tabela Produto, na Tabela ProdutoEmpresa.

    Valeu pela atenção.

  6. Diogo Rosin 5 Jun 2012 11:59

    Boa tarde Washington.

    Primeiramente agradeço pelo seu artigo, foi de grande valia para a minha compreensão sobre mapeamentos. Porém ainda tenho uma dúvida. Vi que a sua tabela EmpresaUsuario possui os campos empresa_id e usuario_id. Caso seja necessário criar esses mesmos campos na tabela EmpresaUsuario usando uma nomenclatura diferente, por exemplo empresa ao invés de empresa_id e usuario ao invés de usuario_id, como faço o mapeamento? O seu exemplo utiliza uma forma padrão que o hibernate realiza os joins automaticamente entre as tabelas.

    Grato pela atenção.

    1. Washington Botelho autor 25 Jun 2012 20:11

      Oi Diogo,

      Se não me engano é só utilizar o @JoinColumn passando o name.
      Mas não te recomendo sair da convenção, já que o Hibernate tenta organizar as coisas.

      1. Domingos 11 Nov 2015 15:16

        Washington Botelho, Boa tarde .

        Como faço para pegar uma lista de usuário de uma determinada empresa.

  7. Douglas Silva 18 Jan 2012 12:46

    Washington ótimo seu artigo.
    Só uma correção, faltou a Entity Usuario. Você acabou colocando a Empresa duas vezes.

    1. Washington Botelho autor 18 Jan 2012 17:15

      Opa,

      Valeu Douglas, já corrigi.
      Muito obrigado. (;

Em resposta:
(cancelar)
Formate seu código utilizando Markdown.