sábado, 25 de abril de 2009

Mapeamento objeto-relacional (ORM) com Hibernate – Uma abordagem prática

Nesta postagem será mostrado como fazer o mapeamento objeto-relacional (ORM) utilizando o hibernate através de um exemplo prático. A idéia principal do exemplo é apenas mostrar como fazer o mapeamento objeto-relacional sem ressaltar conceitos envolvidos. Espera-se que ele sirva como uma referência rápida e objetiva para desenvolvedores.

Para iniciar, vamos definir o modelo de dados relacional através de um diagrama entidade-relacionamento (figura abaixo).



Para melhor visualização clique na imagem


Através do diagrama entidade-relacionamento acima, podemos extrair alguns casos a serem mapeados.
  • Herança – Entidades envolvidas: aluno, professor, pessoa;
  • N : M sem atributos intermediários – Entidades envolvidas: disciplina_professor, professor, disciplina;
  • 1 : N – Entidades envolvidas: turma, aluno;
  • N : M com atributo intermediário – Entidades envolvidas: professor_turma, turma, professor.
Abaixo temos as classes java com o mapeamento objeto-relacional exemplificando os casos citados acima.

Mapeamento de Pessoa:

import static javax.persistence.GenerationType.IDENTITY;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.Table;

@SuppressWarnings("serial")
@Entity
@Table(name = "pessoa", catalog = "mapeamentohibernate")
@Inheritance(strategy = InheritanceType.JOINED)
public class Pessoa implements Serializable {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "id", unique = true, nullable = false)
private Integer id;

@Column(name = "nome")
private String nome;
// getters e setters omitidos
}

Mapeamento de Aluno:


import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;


@SuppressWarnings("serial")
@Entity
@Table(name = "aluno", catalog = "mapeamentohibernate")
@PrimaryKeyJoinColumn(name = "id") // id da tabela aluno
public class Aluno extends Pessoa implements Serializable {

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "idturma")
private Turma turma;

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

// getters e setters omitidos
}

Mapeamento de Professor:


import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;

@SuppressWarnings("serial")
@Entity
@Table(name = "professor", catalog = "mapeamentohibernate")
@PrimaryKeyJoinColumn(name = "id") // id da tabela professor
public class Professor extends Pessoa implements Serializable {

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

@OneToMany(fetch = FetchType.LAZY, mappedBy = "professor")
private Set<ProfessorTurma> professorTurma =
new HashSet<ProfessorTurma>(0);

@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "disciplina_professor", catalog = "mapeamentohibernate",
joinColumns = { @JoinColumn(name = "idprofessor", nullable = false,
updatable = false) }, inverseJoinColumns = {
@JoinColumn(name = "iddisciplina", nullable = false, updatable = false) })
private Set<Disciplina> disciplinas = new HashSet<Disciplina>(0);

// getters e setters omitidos
}

Mapeamento de Disciplina:

import static javax.persistence.GenerationType.IDENTITY;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;


@SuppressWarnings("serial")
@Entity
@Table(name = "disciplina", catalog = "mapeamentohibernate")
public class Disciplina implements Serializable {

@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "id", unique = true, nullable = false)
private Integer id;

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

@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "disciplina_professor", catalog = "mapeamentohibernate",
joinColumns = { @JoinColumn(name = "iddisciplina", nullable = false,
updatable = false) }, inverseJoinColumns = {
@JoinColumn(name = "idprofessor", nullable = false, updatable = false) })
private Set<Professor> professores = new HashSet<Professor>(0);

// getters e setters omitidos
}

Mapeamento de Turma:

import static javax.persistence.GenerationType.IDENTITY;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@SuppressWarnings("serial")
@Entity
@Table(name = "turma", catalog = "mapeamentohibernate")
public class Turma implements Serializable {

@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "id", unique = true, nullable = false)
private Integer id;

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

@OneToMany(fetch = FetchType.LAZY, mappedBy = "turma")
private Set<ProfessorTurma> professorTurma = new
HashSet<ProfessorTurma>(0);

@OneToMany(fetch = FetchType.LAZY, mappedBy = "turma")
private Set<Aluno> alunos = new HashSet<Aluno>(0);

// getters e setters omitidos
}

Mapeamento de ProfessorTurma:

import java.io.Serializable;
import java.util.Date;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@SuppressWarnings("serial")
@Entity
@Table(name = "professor_turma", catalog = "mapeamentohibernate")
public class ProfessorTurma implements Serializable {

@EmbeddedId
@AttributeOverrides( { @AttributeOverride(name = "idturma", column =
@Column(name = "idturma", nullable = false)), @AttributeOverride(name =
"idprofessor", column = @Column(name = "idprofessor", nullable = false)) })
private Id id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "idturma", nullable = false, insertable = false,
updatable = false)
private Turma turma;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "idprofessor", nullable = false, insertable = false,
updatable = false)
private Professor professor;

@Temporal(TemporalType.TIMESTAMP)
@Column(name = "horario", length = 19)
private Date horario;

// getters e setters omitidos

//Id da associacao (chave composta no banco)
@Embeddable
public static class Id implements Serializable {

@Column(name = "idTurma", nullable = false)
private Integer idTurma;

@Column(name = "idProfessor", nullable = false)
private Integer idProfessor;

public Integer getIdTurma() {
return this.idTurma;
}

public void setIdTurma(Integer idturma) {
this.idTurma = idturma;
}

public Integer getIdProfessor() {
return this.idProfessor;
}

public void setIdProfessor(Integer idprofessor) {
this.idProfessor = idprofessor;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + idProfessor;
result = prime * result + idTurma;
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Id other = (Id) obj;
if (idProfessor != other.idProfessor)
return false;
if (idTurma != other.idTurma)
return false;
return true;
}
}
}


Note que, no caso N:M que não tem atributo intermediário (tabela disciplina_professor) , não há necessidade de uma classe para representar a associação. No caso em que há atributos intermediários, como o da tabela professor_turma, temos que criar uma classe para representar a associação (classe ProfessorTurma).

O banco de dados utilizado no exemplo foi o MySQL. Quem quiser fazer o download do script do banco basta acessar o link:
http://sites.google.com/site/csscode/Home/mapeamentohibernate.sql

Uma dica para quem não tem muita familiaridade com o mapeamento objeto-relacional é usar o hibernate tools. O hibernate tools trata-se de um plugin do eclipse que auxilia na árdua e massante tarefa de mapear. Vale a pena conferir!!!


Thiago Baesso Procaci

45 comentários:

Lellis disse...

Ótimo post cara, facil de entender e com possiveis problemas enfrentados no NparaM, estava com problemas aqui para solucionar este problema, ja havia consultado diversas documentações e nada, agora deu tudo certo.
Parabens e obrigado.

Thiago disse...

Valeu mestre!
Qualquer problema entre em contato

Unknown disse...

cara muito bom teu tutorial... agora eu tow apanhando numa coisa somente

:(

Como faço pra pegar os dados dessa tabela gerada do relacionamento??

tipo, eu quero saber quais sao as turmas q o professor "tal" estar??

Thiago disse...

Fala aí mestre!
Isso pode ser feito com criteria.
Veja um exemplo:
public List<Turma> findByProfessor(Professor professor) {
Criteria criteria = getSession().createCriteria(Turma.class);
criteria.createCriteria("professorTurma", "pt");
criteria.add(Restrictions.eq("pt.professor.id",professor.getId()));
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY) return criteria.list();
}

Unknown disse...

Perfeito o post Thiago.
Parabéns.
Só uma dúvida:
Eu tenho uma chave composta de um Long(id) e uma @ManyToOne.
O campo Long(id), eu preciso incrementar dados, mas se eu não mapeá-lo com (insertable=false, updatable=false), eu recebo uma exceção:
org.hibernate.MappingException
Repeated column in mapping for entity. Sabe como devo proceder? Obrigado. Abs

Thiago disse...

Olá Rafael!
Verifique se a sua chave manyToOne está com o mesmo nome de sua chave id. Já que elas são compostas, creio que elas devem ter nomes diferentes.

Dê nomes diferentes para elas.

Unknown disse...

Como pegar a disciplina "tal" de um professor em Criteria?

Thiago disse...

Acho que isso funciona..

public List findByProfessor(Professor professor) {
Criteria criteria = getSession().createCriteria(Disciplina.class);
criteria.createCriteria("professores", "p");
criteria.add(Restrictions.eq("p.id",professor.getId()));
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)
return criteria.list();
}

David . disse...

Ótimo post Thiago..mas estou com dúvida:

No relacionamento Professor, Turma e ProfessorTurma, como funciona o insert?

Eu criei aqui no projeto da empresa, mas se eu não mudar a anotação para:

@OneToMany(fetch = FetchType.LAZY, mappedBy = "pessoa", cascade=CascadeType.ALL)
private Set idiomas = new HashSet(0);

Ele não faz nada na hora do insert...e quando eu mudo o CascadeType, eu recebo um nullpointer no hashCode() pq os objetos idPessoa e idIdioma estão nulos....a solução q eu encontrei foi deixar a anotação como está aqui no site e salvar o objeto Pessoa e depois percorrer a lista e salvar cada filho separadamente..

Tem alguma outra sugestão?

Abs

Thiago disse...

Olá David!
O cascade é bom, porém há horas que ele não funciona corretamente (como vc pode perceber, além de vários outros problemas).
Eu particularmente, na hora do insert, prefiro salvar entidade por entidade (uma a uma), pois internamente o hibernate faria o mesmo.

Para salvar o ProfessorTurma, basta vc setar as propriedades do relacionamento que são professor e turma (lembre-se que estes já devem estar salvos) e depois mandar salvar ProfessorTurma.

Abraço!

JavaNewbie disse...

Olá Thiago,

Primeiramente parabéns pelo post!

Segui as suas dicas e consegui fazer com que o hibernate gerasse as tabelas conforme eu queria. Só q aí eu fui tentar persistir alguns dados e o problema apareceu =(

Ao tentar gravar os dados, a seguinte exceção é lançada:

javax.persistence.PersistenceException: org.hibernate.id.IdentifierGenerationException: null id generated for:class model.UsuarioServico

Eu descrevi todo o problema no fórum do guj: http://www.guj.com.br/posts/list/213093.java

Você poderia me ajudar??

Valeu!

Thiago disse...

Fala aí mestre!
Pelo visto vc já conseguiu resolver o problema no forum. Parabéns.

De qualquer forma segue uma dica da minha experiencia com o hibernate:

Apesar do hibernate suportar chaves compostas é aconselhado que cada entidade tenha um atribudo id (long ou integer).
Isso evita dores de cabeça no futuro..

Desculpe a demora na resposta. Eu estou de férias e nao tenho ligado o pc..
Abraço!

Unknown disse...
Este comentário foi removido pelo autor.
Unknown disse...

Olá Thiago!!

Ótimo post!!

Gostaria de saber como faço em Criteria para retornar uma lista de alunos que tenham em sua turma a descrição de turma passada no parâmetro de pesquisa. Não é uma situação muito lógica! Mas eu tenho uma situação desse tipo (com outro contexto).

Se ajudar: Eu mando pesquisar por descrição de turma, e ele verifica em alunos se o id de turma se refere a alguma turma cuja descrição foi passada como pesquisa.

Obrigadaa!

Thiago disse...

Olá Patrícia!

Acho que esse criteria resolve para recuperar um aluno através da descrição da turma.

public List findByDescricaoTurma(String descricao) {
Criteria criteria = getSession().createCriteria(Aluno.class);
// faz join com turma
criteria.createCriteria("turma", "t");
// adiciona restricao na descricao da turma
criteria.add(Restrictions.eq("t.descricao",descricao));
return criteria.list();
}

Cyro disse...

Opa! Um projeto pronto é ótimo de parâmetro.
Eu tenho uma tabela com chave composta, e pra mapear ela fiz uma classe interna pro Id, que nem no exemplo. Mas agora tá dando um erro pois essa classe interna não tá mapeada. Como fica o persistence.xml pra isso? Ou do jeito q vc faz nem vai esse xml?
Valeu!

Cyro disse...

Agh esquece, erro meu, já dei um jeito aqui. pode apagar meu comentário -_-

jairo disse...

tudo bom thiago.. em primeiro lugar muito obrigado pelo post.. segundo.. vc fazendo relacionamentos MamyToMany nao fica deletando os dados das tabelas relacionais qd vc salva uma lista vazia? no meu caso qd eu salvo um produto com um Set manytomany que é nulo, logo apos o update o hibernate apaga todos os dados da tabela do set :/
sabe como resolver isso?

Thiago disse...

Olá Jairo.

Não entendi muito bem o seu problema.
Mas verifica se vc está usando a anotação @cascade no seu set de produtos.
Usando o cascade o hibernate pode deletar dados de outras entidades sim.

jairo disse...

muito obrigado pela rapida resposta thiago.. eu postei essa duvida no forum do guj.. o link logo abaixo... vc jah passou por isso?
http://www.guj.com.br/java/233294-manytomany-deletando-tabelas-associativas-no-saveorupdate

Thiago disse...

Oi Jairo.
Esse é o comportamento normal mesmo.
A tabela PRODUTOS_X_GRUPOS só existe para fazer a ligação entre a entidade produto e grupo. Quando vc limpa o set de produto o hibernate entende que vc não quer que exista mais o relacionamento entre produto e grupo.
Logo, os dados da tabela PRODUTOS_X_GRUPOS serão apagados.

Experimente não deixar o produto.grupos nulo e veja o resultado.

Edu Marques disse...

Olá Thiago, sou novo no pedaço, primeira mente parabéns, muito didático e funcional seu blog, agora vamos a dúvida...
gostaria de saber de que forma se popula a tabela DISCIPLINA_PROFESSOR do relacionamento?? mapeio primeiro a disciplina, depois professor, separadamente, e como faço para persistir a disciplina_professor... sou novo com jpa.
Desde já agradeço!

Anne disse...

Oi Thiago. Muito bom o post. Queria ter achado ele antes de apanhar tanto pra fazer um mapeamento super parecido aqui (no meu caso, é uma árvore com varios tipos de nós).
Só tenho um problema: deletar. Eu sempre recebo um erro mais ou menos assim: supondo q vc quer deletar um objeto do tipo Professor. Eu tenho uma classe generica que chama o delete para o objeto Pessoa, mas ai da um erro pq o objeto pessoa nao tem um atributo formacao.
Tem como vc postar o modo como vc deleta objetos pra esse modelo? Preciso ter uma classe service pra cada subclasse de Pessoa? (eu tenho + ou - 10 subclasses no meu modelo, isso n seria uma boa opcao).
PS: eu uso EclipseLink ao inves de Hibernate.
Desde ja, obrigada e parabens.

Thiago disse...

Olá Edu. Primeiramente desculpe a demora pela resposta.
Para colocar dados na tabela disciplina professor basta:

1) Ter salvo previamente uma entidade disciplina e professor.

2) Recuperar as entidades já salvas e adicionar nas suas respectivas listas (set no caso) os dados que compoem o relacionamento de disciplina_professor.

Veja o exemplo:

// recupera entidades do banco
Professor professor = professorDao.getById(1);

Disciplina disciplina = disciplinaDao.getById(2);

// faz o relacionamento
professor.getDisciplinas().add(disciplina);
disciplina.getProfessores().add(professor);

// salva o relacionamento
professorDao.merge(professor);
disciplinaDao.merge(disciplina);


Note que a tabela disciplina_professor não existe em nosso modelo orientado a objetos. Ela é simplesmente representada atraves dos sets nas entidades.

abraço!

Thiago disse...

Oi Anne.

O hibernate geralmente resolve esse polimorfismo de classes e subclasses.
Tanto é, se vc analisar como ele funciona internamente verá que a assinatura do método que usa deletar recebe como parametro um Object (classe mais geral).
Não sei como o eclipseLink funciona. Experimente criar uma subclasse de service que deleta o objeto para ver se funciona.

Por alto assim fica dificil saber onde é realmente o problema.

abraço!

Anne disse...

O problema é que eu nao tenho a opcao de usar o Hibernate, ja que nao eh um projeto pessoal, eh no trabalho. =/
Pelo mesmo motivo tb, nao posso postar o codigo.
A questao eh q eu nao consigo deletar meus objetos por cascada. Se eu tento deletar so um objeto da sub tabela, ele nao deleta a super (claro!). Se eu tento deletar atraves da super, recebo erro por causa da foreign key na sub tabela.
Ja tentei usar a anotacao @CascadeOnDelete mas tb nao funciona.
Quando vc faz a remocao, vc chama atraves do objeto Professor ou Pessoa? Ou nao era p fazer diferenca isso?
Abraco

Thiago disse...

Oi Anne.
Geralmente eu faço a remoção pelo objeto da classe mais específica. Mas não deveria fazer diferença...

Edu Marques disse...

Olá Thiago, não precisa se desculpar... e agradeço a atenção, muito obrigado, me ajudou muito.

Ederson disse...

Opa thiago, estou com uma duvida...
como salvar o professor_turma... imagine uma turma ja cadastrada, apenas quero cadastrar o professor e seu relacionamento...

Obrigado!!

Thiago disse...

Olá Ederson!

Para isto, são necessários dois passos:

Primeiro, basta instanciar o professor, setar seus atributor e salvar o professor.

Depois, deve-se instanciar a entidade ProfessorTurma, setar seus atributos e salvar.

abraço

Éderson disse...

Tiago como fica o controle de transicao neste caso.. se realizar o SAVE entidade por entidade a transicao sera controlada corretamente?

Ex:
Supomos que de falha no momento do save dos professorTurma.

valeu abraços

Thiago disse...

Oi Ederson.

Vc pode anotar o método que faz toda essa lógica para salvar com @Transactional. Essa anotação é do spring framework e, usando ela, vc terá o método como uma transação.

Outra opção é realizar a transação programaticamente usando o próprio hibernate.

Segue boas referencias:

http://static.springsource.org/spring/docs/2.0.x/reference/transaction.html

http://docs.jboss.org/hibernate/core/3.5/reference/pt-BR/html/transactions.html

Ederson disse...

Opaa esclarecido as dúvidas...valeu e sucesso!

Eduardo disse...

Blz Thiago,

Excelente post, explicação muito clara, parabéns.
Mas...hehehe
Estou com um senário diferente e gostaria de saber se é possível me ajudar.
Preciso mapear duas entidades com chave composta onde um campo é uma foreing key (entidade externa) e o outro campo pertence à própria entidade. vide link:
http://www.guj.com.br/java/251942-hibernate-annotation--chave-composta#1309078

Desde de já agradeço.

Edu Marques disse...

Olá Thiago, recomendas algum material bom sobre Criteria, algo mais aprofundado e detalhado?!

Desde já agradeço!!


Edu Marques

Thiago disse...

Olá Edu.

Uma boa referência sobre criteria é o blog da caelum. Lá tem uns bons exemplos:

http://blog.caelum.com.br/divisions-com-hibernate-uso-avancado-da-criteria-api/

Anônimo disse...

Ola Thiago,

Excelente post, explicação muito boa, parabéns.

To precisando de uma ajuda.Na hora de salvar um aluno queria ter a opção de escolher a turma, que seria carregada em um como box, da tabela turma coma faria, usei o jsl mas não carregou as chaves, com faço???

Thiago disse...
Este comentário foi removido pelo autor.
Thiago disse...

Oi. tente algo assim:
http://www.guj.com.br/java/224656-hibernate-carregando-combobox-jstl

motok4 disse...

ÔôO bodim... surpresa demais cair no seu blog irmãozinho!
Ficou mt bom mesmo a explicação!!
Abraços

Thiago disse...

Valeu mestre! -)

Ed disse...

Thiago,
Poderia me dar uma ajuda em:
// recupera entidades do banco
Professor professor = professorDao.getById(1);
estou apanhando um bocado nesta parte.

grato

dinho1983 disse...

Cara adorei este post , esta sendo muito útil no entendimento !

Unknown disse...

Pessoal,
Como eu faço a anotação e o uso de uma VIEW Postgres com Hibernate?

VAleu!

Channel Information Technology disse...

Uauuuuuu parabens, muito esclarecedor, ajudou muito, obrigado....