Many-to-many com colunas extras, JPA e Hibernate


Bom galera, estou um pouco sem tempo pois estou trabalhando em um projeto pessoal portanto pretendo fazer um tópico rápido sobre um problema que enfrentei essa semana tentando mapear uma tabela ManytoMany com dados extras na terceira tabela.

Encontrei dificuldades em mapear as entidades devido ao loading dos objetos para serializa-los, o que fazer quando um objeto fosse deletado ou atualizado entre outros problemas comuns na hora de se desenvolver aplicações que acessam dados em banco.

Procurando na internet e encontrei diversas pessoas utilizando Eager Loading nas entidades com relacionamento devido ao fato dos atributos da entidade já virem carregador assim que procuramos ele no banco. E isso é um problema, imagine retornar um objeto que faz referência a outros objetos que por final faz referência ao mesmo objeto e temos ai um Stackoverflow (percebeu o loop infinito?). É importante saber quando e porque utilizar Eager Loading pois pode ser um problema que você acaba levando tempo para descobrir(aconteceu comigo). Para solucionar o Eager Loading temos a forma mais preguiçosa de acessar dados em banco, Lazy Loading, onde buscamos pela referência ao objeto apenas quando for necessário e não sempre como acontece com Eager. Outro problema que econtrei foi como fazer para atualizar ou remover as referências às entidades relacionadas entre si quando uma ou outra for atualizada/deletada? Precisamos então ter uma regra bem definida de cascade para evitar que dados fiquem perdidos.

Imagine uma entidade de Alunos que podem assistir diversas Aulas e claro aulas que contém diversos Alunos, até ai tudo bem, porém além das informações básicas é preciso que o Aluno consiga deixar um comentário sobre a Aula que ele assistiu e que seja amarzenado também a nota que aquele determinado aluno obteve. Sempre que descrevemos um relacionamento N:M entre duas tabelas em banco de dados é necessário criar uma terceira tabela contendo os Ids de cada tabela relacionada e caso necessário adicionar campos nessa terceira tabela e é necessa terceira tabela que devemos colocar o comentário e a nota do aluno que fez determinada aula, devemos então mapear além das tabelas Aluno e Aula, também a tabela de AulasParaAlunos para especificar os dados extras que temos nessa terceira tabela.

Vamos então às nossas entidades:

import java.util.LinkedList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import org.hibernate.annotations.Cascade;

@Entity
@Table(name = "tb_aluno")
public class Aluno {

 @Id
 @GeneratedValue(strategy = GenerationType.AUTO)
 @Column(name = "idPessoa")
 private Long idAluno;

 @Column(name = "nome")
 private String nome;

 @Column(name = "endereco")
 private String endereco;

 @Column(name = "nascimento")
 @Temporal(TemporalType.TIMESTAMP)
 private String dataNascimento;

 //Aqui estamos dizendo como a entidade deve ser carregada
 //e o que deve acontecer caso alguma coisa seja deletada ou atualizada.
 @OneToMany(fetch = FetchType.LAZY,
 mappedBy = "pk.aluno",
 cascade = { CascadeType.PERSIST, CascadeType.MERGE })
 @Cascade( { org.hibernate.annotations.CascadeType.SAVE_UPDATE,
 org.hibernate.annotations.CascadeType.DELETE_ORPHAN })
 private List<AulasParaAlunos> aulasParaAlunos = new LinkedList<>();

}
import java.util.LinkedList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import org.hibernate.annotations.Cascade;

@Entity
@Table(name = "tb_aula")
public class Aula {

 @Id
 @GeneratedValue(strategy = GenerationType.AUTO)
 @Column(name = "idAula")
 private Long idAula;
 @Column(name = "nome")
 private String nome;
 @Column(name = "entrada")
 private String entrada;
 @Column(name = "correcao")
 private String correcao;
 @Column(name = "tempoNecessariO")
 private Integer tempoNecessario;

 @OneToMany(fetch = FetchType.LAZY, mappedBy = "pk.aula",
 cascade = { CascadeType.PERSIST, CascadeType.MERGE })
 @Cascade( { org.hibernate.annotations.CascadeType.SAVE_UPDATE,
 org.hibernate.annotations.CascadeType.DELETE_ORPHAN })
 private List<AulasParaAlunos> aulasParaAlunos = new LinkedList<AulasParaAlunos>();
}

Veja que o mapeamento das duas entidades é bem parecido, estamos fazendo referencia a uma terceira entidade que é AulaParaAlunos e é nela que dizemos os campos adicionais, a Id da tabela(que nesse caso é composta) e algumas regras adicionais para ter o relacioamento correto entre elas.

import javax.persistence.AssociationOverride;
import javax.persistence.AssociationOverrides;
import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.Table;
import javax.persistence.Transient;

@Entity
@Table(name = "aulas_para_alunos")
@AssociationOverrides(
 { @AssociationOverride(name = "pk.aluno", joinColumns = @JoinColumn(name = "idAluno")),
 @AssociationOverride(name = "pk.aula", joinColumns = @JoinColumn(name = "idAula")) })
public class AulasParaAlunos {

 @EmbeddedId
 private AulasParaAlunosPK pk = new AulasParaAlunosPK();
 @Column(name = "comentario")
 private String comentario;
 @Column(name = "nota")
 private Integer nota;

 public boolean equals(Object o) {
 if (this == o)
 return true;
 if (o == null || getClass() != o.getClass())
 return false;

 AulasParaAlunos that = (AulasParaAlunos) o;

 if (getPk() != null ? !getPk().equals(that.getPk()) : that.getPk() != null)
 return false;

 return true;
 }

 @Transient
 public Aluno getAluno() {
 return this.getPk().getAluno();
 }

 @Transient
 public Aula getAula() {
 return this.getPk().getAula();
 }

 public int hashCode() {
 return (getPk() != null ? getPk().hashCode() : 0);
 }
}

Essa tabela possui uma Id composta pelo Id do Aluno e uma Id da Aula, descrita pela classe AulasParaAlunosPK:

import java.io.Serializable;

import javax.persistence.Embeddable;
import javax.persistence.FetchType;
import javax.persistence.ManyToOne;

@Embeddable
public class AulasParaAlunosPK implements Serializable {

 private static final long serialVersionUID = -5869094934725857817L;
 @ManyToOne(fetch = FetchType.LAZY, optional = false)
 private Aluno aluno;
 @ManyToOne(fetch = FetchType.LAZY, optional = false)
 private Aula aula;

 // Precisamos implementar corretamente o equals() na classe de Id composta.
 public boolean equals(Object o) {
 if (this == o)
 return true;
 if (o == null || getClass() != o.getClass())
 return false;
 if (!(o instanceof AulasParaAlunosPK))
 return false;

 AulasParaAlunosPK that = (AulasParaAlunosPK) o;

 if (this.aluno != null ? !this.aluno.equals(that.aluno) : that.aluno != null)
 return false;
 if (this.aula != null ? !this.aula.equals(that.aula) : that.aula != null)
 return false;

 return true;
 }

 public int hashCode() {
 int result;
 result = (this.aluno != null ? this.aluno.hashCode() : 0);
 result = 31 * result + (this.aula != null ? this.aula.hashCode() : 0);
 return result;
 }
}

Agora temos o mapeamento correto com lazy loading e cascading respeitando regras de negocio. Todos os objetos serão carregados apenas quando dermos um GET em cada variável e caso alguma coisa seja deletada ou atualizada o Hibernate vai saber o que fazer. Bom, tutorial rápido pois estou sem tempo, semana que vem tento parar para escrever algo mais bacana, espero conseguir abordar o SpringSecurity. Dúvidas por favor deixem recados.

About these ads

8 Comentários on “Many-to-many com colunas extras, JPA e Hibernate”

  1. wbissi disse:

    Fala Diego blz?
    É sempre bom publicar algo do tipo, muitos tem dúvidas sobre como fazer esses tipos de mapeamentos. E excelente observação sobre os Lazy Loadings, já tive muita dor de cabeça com eles.

    Abraços.

  2. Darlan disse:

    É o primeiro tutoria de mapeamento que funcionou para mim…

    Muito bom mesmo , valeu, Muito obrigado….

  3. James disse:

    Adorei as explicações, e como eu estava com um pouco sem tempo de fazer, me saiu uma mão na roda =]

    Só que no caso para mim, a tabela OpcoesUsuario não é criada, e nem no código, e em nenhum lugar aponta erro, teria alguma dica?

    Embora a minha tabela não tenha nenhum campo na associativa, a unica coisa que mudei praticamente foi a posição das anotações que coloquei nas propriedades (e a criação dos gets e sets), já que no netbeans apontava “erro”, que creio isso nao interferir muito.

  4. James disse:

    aaaaa, não me crucifiquem, eu nao conseguia criar a tabela pq eu tinha os id com o mesmo nome (id), my bad

  5. willian disse:

    Salve galera, como eu criaria uma action agora com seam para fazer o seguinte:
    1 – Em uma tela eu tenho um Select que é preenchido com alunos;
    2 – Ao selecionar um aluno ele preenche um rich:listShuttle com os itens associados e não associados, ou seja ele faz uma consulta no banco passando o id do aluno como parametro e retorna de um lado os selecionados e de outro os não selecionados.
    3 – O usuário manipula as aulas pelo controle rich:listShuttle e no final salva, persistindo as aulas selecionadas no banco.

    Se alguém conseguir me ajudar fico muito grato.

  6. Cerli disse:

    MUito bom o exemplo, mas como faço, por exemplo, o DAO para persistir o Aluno?

  7. dchohfi disse:

    Reblogged this on Diego Chohfi.


Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

WordPress.com Logo

Você está comentando usando sua conta WordPress.com. Sair / Mudar )

Imagem do Twitter

Você está comentando usando sua conta Twitter. Sair / Mudar )

Foto do Facebook

Você está comentando usando sua conta Facebook. Sair / Mudar )

Conectando a %s

Seguir

Obtenha todo post novo entregue na sua caixa de entrada.