Guia para JPA com Hibernate – Mapeamento de Relacionamento

Introdução

Neste artigo, vamos mergulhar no Mapeamento de Relacionamento com JPA e Hibernate em Java.

O Java Persistence API (JPA) é o padrão de persistência do ecossistema Java. Ela nos permite mapear nosso modelo de domínio diretamente para a estrutura do banco de dados e depois nos dá a flexibilidade de manipular objetos em nosso código – em vez de mexer com componentes JDBC pesados como Connection, ResultSet, etc.

Estaremos fazendo um guia completo para usar JPA com Hibernate como seu fornecedor. Neste artigo, estaremos cobrindo mapeamentos de relacionamentos.

  • Guia para JPA com Hibernate – Mapeamento Básico
  • Guia para JPA com Hibernate – Mapeamento de Relacionamentos (você está aqui)
  • Guia para JPA com Hibernate: Mapeamento de Heranças (em breve!)
  • Guia para JPA com Hibernate – Consulta (Em breve!)

Nosso Exemplo

Antes de começar, vamos lembrar o exemplo que usamos na parte anterior desta série. A idéia era mapear o modelo de uma escola com alunos fazendo cursos dados por professores.

Aqui está o aspecto deste modelo:

Como podemos ver, há algumas aulas com determinadas propriedades. Estas classes têm relações entre elas. Ao final deste artigo, teremos mapeado todas essas classes para as tabelas de banco de dados, preservando suas relações.

Outras vezes, poderemos recuperá-las e manipulá-las como objetos, sem o incômodo de JDBC.

Relações

Primeiro de tudo, vamos definir uma relação. Se olharmos para o nosso diagrama de classes podemos ver alguns relacionamentos:

Professores e cursos – alunos e cursos – cursos e materiais de curso.

Existem também conexões entre alunos e endereços, mas não são considerados relacionamentos. Isto porque um Address não é uma entidade (ou seja, não é mapeado para uma tabela própria). Portanto, no que diz respeito ao JPA, não é um relacionamento.

Existem alguns tipos de relacionamentos:

  • Um-a-muitos
  • Homem-a-Um
  • Um-a-Um
  • Homem-a-muitos

Vamos abordar estes relacionamentos um a um.

Um-para-muitos/um-para-um

Comecemos com os relacionamentos Um-para-muitos e Muitos-para-Um, que estão intimamente relacionados. Você poderia dizer que eles são os lados opostos da mesma moeda.

O que é uma relação Um-para-muitos?

Como seu nome implica, é uma relação que liga uma entidade a muitas outras entidades.

No nosso exemplo, esta seria uma relação Teacher e a deles Courses. Um professor pode dar vários cursos, mas um curso é dado por apenas um professor (essa é a perspectiva dos Many-to-One – muitos cursos para um professor).

Outro exemplo poderia ser nas redes sociais – uma foto pode ter muitos comentários, mas cada um desses comentários pertence a essa foto.

Antes de mergulhar nos detalhes de como mapear essa relação, vamos criar nossas entidades:

@Entitypublic class Teacher { private String firstName; private String lastName;}@Entitypublic class Course { private String title;}

Agora, os campos da classe Teacher devem incluir uma lista de cursos. Como gostaríamos de mapear essa relação em um banco de dados, que não pode incluir uma lista de entidades dentro de outra entidade – vamos anotá-la com uma @OneToMany anotação:

@OneToManyprivate List<Course> courses;

Usamos um List como o tipo de campo aqui, mas poderíamos ter escolhido um Set ou um Map (embora este requeira um pouco mais de configuração).

Como o JPA reflete essa relação no banco de dados? Geralmente, para este tipo de relacionamento, devemos usar uma chave estrangeira em uma tabela.

JPA faz isso para nós, dada a nossa contribuição sobre como ele deve lidar com o relacionamento. Isto é feito através da anotação @JoinColumn>

@OneToMany@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")private List<Course> courses;

Usando esta anotação irá dizer ao JPA que a tabela COURSE deve ter uma coluna de chave estrangeira TEACHER_ID que referencia a coluna TEACHER da tabela ID.

Vamos adicionar alguns dados a essas tabelas:

insert into TEACHER(ID, LASTNAME, FIRSTNAME) values(1, 'Doe', 'Jane');insert into COURSE(ID, TEACHER_ID, TITLE) values(1, 1, 'Java 101');insert into COURSE(ID, TEACHER_ID, TITLE) values(2, 1, 'SQL 101');insert into COURSE(ID, TEACHER_ID, TITLE) values(3, 1, 'JPA 101');

E agora vamos verificar se a relação funciona como esperado:

Teacher foundTeacher = entityManager.find(Teacher.class, 1L);assertThat(foundTeacher.id()).isEqualTo(1L);assertThat(foundTeacher.lastName()).isEqualTo("Doe");assertThat(foundTeacher.firstName()).isEqualTo("Jane");assertThat(foundTeacher.courses()) .extracting(Course::title) .containsExactly("Java 101", "SQL 101", "JPA 101");

Podemos ver que os cursos do professor são reunidos automaticamente, quando recuperamos a instância Teacher.

Se você não está familiarizado com testes em Java, você pode estar interessado em ler Unit Testing in Java com JUnit 5!

Owning Side and Bidirectionality

No exemplo anterior, a classe Teacher é chamada de o lado proprietário da relação One-To-Many. Isto porque ela define a coluna join entre as duas tabelas.

A Course é chamada de lado de referência naquela relação.

Poderíamos ter feito Course o lado proprietário da relação mapeando o campo Teacher com @ManyToOne na classe Course em vez disso:

@ManyToOne@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")private Teacher teacher;

Não há necessidade de ter uma lista de cursos na classe Teacher agora. A relação teria funcionado da forma oposta:

Course foundCourse = entityManager.find(Course.class, 1L);assertThat(foundCourse.id()).isEqualTo(1L);assertThat(foundCourse.title()).isEqualTo("Java 101");assertThat(foundCourse.teacher().lastName()).isEqualTo("Doe");assertThat(foundCourse.teacher().firstName()).isEqualTo("Jane");

Desta vez, usamos a anotação @ManyToOne, da mesma forma que usamos @OneToMany.

Note: É uma boa prática colocar o lado proprietário de uma relação na classe/tabela onde a chave estrangeira será mantida.

Então, no nosso caso, esta segunda versão do código é melhor. Mas, e se ainda quisermos que a nossa classe Teacher ofereça acesso à sua Course lista?

Podemos fazer isso definindo uma relação bidireccional:

@Entitypublic class Teacher { // ... @OneToMany(mappedBy = "teacher") private List<Course> courses;}@Entitypublic class Course { // ... @ManyToOne @JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID") private Teacher teacher;}

Mantemos o nosso @ManyToOne mapeamento na entidade Course. Entretanto, também mapeamos uma lista de Courses para a Teacher entidade.

O que é importante notar aqui é o uso da bandeira mappedBy na @OneToMany anotação no lado de referência.

Sem ela, não teríamos uma relação bidirecional. Teríamos duas relações unidireccionais. Ambas entidades estariam mapeando chaves estrangeiras para a outra entidade.

Com ela, estamos dizendo ao JPA que o campo já está mapeado por outra entidade. Ele é mapeado pelo campo teacher da Course entidade.

Eager vs Lazy Loading

Outra coisa que vale a pena notar é o carregamento ansioso e preguiçoso. Com todas as nossas relações mapeadas, é sábio evitar impacto na memória do software, colocando demasiadas entidades nele se desnecessário.

Imagine que Course é um objeto pesado, e nós carregamos todos Teacher objetos do banco de dados para alguma operação. Não precisamos recuperar ou usar os cursos para essa operação, mas eles ainda estão sendo carregados junto com os objetos Teacher.

Isso pode ser devastador para a performance da aplicação. Tecnicamente, isto pode ser resolvido usando o Data Transfer Object Design Pattern e recuperando Teacher informação sem os cursos.

No entanto, isto pode ser um enorme exagero se tudo o que estamos a ganhar com o padrão for excluir os cursos.

Felizmente, o JPA pensou à frente e fez com que as relações de um-para-muitos sejam carregadas preguiçosamente por padrão.

Isto significa que a relação não será carregada imediatamente, mas apenas quando e se realmente necessário.

No nosso exemplo, isso significaria que até que chamemos o método Teacher#courses, os cursos não estão sendo carregados do banco de dados.

Pelo contrário, relacionamentos Many-to-One são ansiosos por padrão, significando que o relacionamento é carregado ao mesmo tempo em que a entidade é carregada.

Podemos alterar estas características definindo o argumento fetch de ambas as anotações:

@OneToMany(mappedBy = "teacher", fetch = FetchType.EAGER)private List<Course> courses;@ManyToOne(fetch = FetchType.LAZY)private Teacher teacher;

Isso inverteria a forma como ela funcionava inicialmente. Os cursos seriam carregados avidamente, assim que carregássemos um objeto Teacher. Em contraste, o teacher não seria carregado quando buscamos courses se não fosse necessário no momento.

Opcionalidade

Agora, vamos falar sobre opcionalidade.

Uma relação pode ser opcional ou obrigatória.

Considerando o lado One-to-Many – é sempre opcional, e não podemos fazer nada sobre isso. O lado “Many-to-One”, por outro lado, nos oferece a opção de torná-la obrigatória.

Por padrão, a relação é opcional, ou seja, podemos salvar um Course sem atribuir-lhe um professor:

Course course = new Course("C# 101");entityManager.persist(course);

Agora, vamos tornar esta relação obrigatória. Para isso, vamos usar o argumento optional da anotação @ManyToOne e configurá-lo para falsetrue por padrão):

@ManyToOne(optional = false)@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")private Teacher teacher;

Assim, não podemos mais salvar um curso sem atribuir-lhe um professor:

Course course = new Course("C# 101");assertThrows(Exception.class, () -> entityManager.persist(course));

Mas se lhe atribuirmos um professor, funciona bem novamente:

Teacher teacher = new Teacher();teacher.setLastName("Doe");teacher.setFirstName("Will");Course course = new Course("C# 101");course.setTeacher(teacher);entityManager.persist(course);

Bem, pelo menos parece que sim. Se tivéssemos executado o código, teria sido lançada uma excepção:

javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: com.fdpro.clients.stackabuse.jpa.domain.Course

Porquê isto? Nós definimos um objeto válido Teacher no objeto Course que estamos tentando persistir. Entretanto, não persistimos o objeto Teacher antes de tentarmos persistir o objeto Course.

Assim, o objeto Teacher não é uma entidade gerenciada. Vamos corrigir isso e tentar novamente:

Teacher teacher = new Teacher();teacher.setLastName("Doe");teacher.setFirstName("Will");entityManager.persist(teacher);Course course = new Course("C# 101");course.setTeacher(teacher);entityManager.persist(course);entityManager.flush();

Executar este código irá persistir ambas as entidades e perserverar a relação entre elas.

Cascading Operations

No entanto, poderíamos ter feito outra coisa – poderíamos ter feito em cascata, e assim propagar a persistência do objeto Teacher quando persistirmos o objeto Course.

Isso faz mais sentido e funciona da forma que esperaríamos no primeiro exemplo que lançou uma exceção.

Para fazer isso, vamos modificar a bandeira cascade da anotação:

@ManyToOne(optional = false, cascade = CascadeType.PERSIST)@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")private Teacher teacher;

Dessa forma, Hibernate sabe persistir o objeto necessário nessa relação também.

Existem múltiplos tipos de operações em cascata: PERSIST, MERGE, REMOVE, REFRESH, DETACH, e ALL (que combina todas as anteriores).

Também podemos colocar o argumento em cascata no lado de um-para-muitos da relação, para que as operações sejam em cascata dos professores para os seus cursos também.

Um-a-um

Agora estabelecemos as bases do mapeamento de relacionamentos no JPA através dos relacionamentos Um-a-muitos/Muitos/Muitos-a-Um e suas configurações, podemos passar para os relacionamentos Um-a-Um.

Desta vez, ao invés de termos um relacionamento entre uma entidade de um lado e um monte de entidades do outro, teremos no máximo uma entidade de cada lado.

Esta é, por exemplo, a relação entre um Course e seu CourseMaterial. Vamos primeiro mapear CourseMaterial, o que ainda não fizemos:

@Entitypublic class CourseMaterial { @Id private Long id; private String url;}

A anotação para mapear uma única entidade para uma única outra entidade é, de forma não chocante, @OneToOne.

Antes de configurá-la em nosso modelo, vamos lembrar que uma relação tem um lado próprio – de preferência o lado que vai segurar a chave estrangeira no banco de dados.

No nosso exemplo, isso seria CourseMaterial pois faz sentido que faça referência a Course (embora pudéssemos ir ao contrário):

@OneToOne(optional = false)@JoinColumn(name = "COURSE_ID", referencedColumnName = "ID")private Course course;

Não vale a pena ter material sem um curso para englobá-lo. É por isso que a relação não é optional nessa direção.

Por falar em direção, vamos tornar a relação bidirecional, para que possamos acessar o material de um curso, se ele tiver um. Na classe Course, vamos adicionar:

@OneToOne(mappedBy = "course")private CourseMaterial material;

Aqui, estamos dizendo ao Hibernate que o material dentro de um Course já está mapeado pelo campo course da CourseMaterial entidade.

Ainda, não há nenhum atributo optional aqui, pois é true por padrão, e poderíamos imaginar um curso sem material (de um professor muito preguiçoso).

Além de tornar a relação bidirecional, também poderíamos adicionar operações em cascata ou fazer entidades carregar avidamente ou preguiçosamente.

Many-to-Many

Agora, por último mas não menos importante: Many-to-Many relacionamentos. Nós os guardamos para o final porque eles requerem um pouco mais de trabalho do que os anteriores.

Efetivamente, em um banco de dados, um relacionamento Many-to-Many envolve uma tabela intermediária referenciando ambas as outras tabelas.

Felizmente para nós, JPA faz a maior parte do trabalho, nós só temos que jogar algumas anotações lá fora, e ele cuida do resto para nós.

Então, para nosso exemplo, a relação Muitos-Muitos será aquela entre Student e Course instâncias como um estudante pode frequentar vários cursos, e um curso pode ser seguido por vários estudantes.

A fim de mapear uma relação Muitos-Muitos usaremos a anotação @ManyToMany. No entanto, desta vez, também vamos usar uma anotação @JoinTable para configurar a tabela que representa a relação:

@ManyToMany@JoinTable( name = "STUDENTS_COURSES", joinColumns = @JoinColumn(name = "COURSE_ID", referencedColumnName = "ID"), inverseJoinColumns = @JoinColumn(name = "STUDENT_ID", referencedColumnName = "ID"))private List<Student> students;

Agora, veja o que está acontecendo aqui. A anotação leva alguns parâmetros. Antes de mais nada, devemos dar um nome à tabela. Escolhemos para ser STUDENTS_COURSES.

Depois disso, precisamos dizer a Hibernate quais colunas unir para poder povoar STUDENTS_COURSES. O primeiro parâmetro, joinColumns define como configurar a coluna de junção (chave estrangeira) do lado proprietário da relação na tabela. Neste caso, o lado proprietário é um Course.

Por outro lado, o parâmetro inverseJoinColumns faz o mesmo, mas para o lado de referência (Student).

Vamos configurar um conjunto de dados com estudantes e cursos:

Student johnDoe = new Student();johnDoe.setFirstName("John");johnDoe.setLastName("Doe");johnDoe.setBirthDateAsLocalDate(LocalDate.of(2000, FEBRUARY, 18));johnDoe.setGender(MALE);johnDoe.setWantsNewsletter(true);johnDoe.setAddress(new Address("Baker Street", "221B", "London"));entityManager.persist(johnDoe);Student willDoe = new Student();willDoe.setFirstName("Will");willDoe.setLastName("Doe");willDoe.setBirthDateAsLocalDate(LocalDate.of(2001, APRIL, 4));willDoe.setGender(MALE);willDoe.setWantsNewsletter(false);willDoe.setAddress(new Address("Washington Avenue", "23", "Oxford"));entityManager.persist(willDoe);Teacher teacher = new Teacher();teacher.setFirstName("Jane");teacher.setLastName("Doe");entityManager.persist(teacher);Course javaCourse = new Course("Java 101");javaCourse.setTeacher(teacher);entityManager.persist(javaCourse);Course sqlCourse = new Course("SQL 101");sqlCourse.setTeacher(teacher);entityManager.persist(sqlCourse);

De curso, isto não vai funcionar fora da caixa. Teremos que adicionar um método que nos permita adicionar alunos a um curso. Vamos modificar um pouco a Course aula:

public class Course { private List<Student> students = new ArrayList<>(); public void addStudent(Student student) { this.students.add(student); }}

Agora, podemos completar nosso conjunto de dados:

Course javaCourse = new Course("Java 101");javaCourse.setTeacher(teacher);javaCourse.addStudent(johnDoe);javaCourse.addStudent(willDoe);entityManager.persist(javaCourse);Course sqlCourse = new Course("SQL 101");sqlCourse.setTeacher(teacher);sqlCourse.addStudent(johnDoe);entityManager.persist(sqlCourse);

Após este código ter sido executado, ele vai persistir as nossas Course, Teacher e Student instâncias, bem como as suas relações. Por exemplo, vamos recuperar um aluno de um curso persistente e verificar se está tudo bem:

Course courseWithMultipleStudents = entityManager.find(Course.class, 1L);assertThat(courseWithMultipleStudents).isNotNull();assertThat(courseWithMultipleStudents.students()) .hasSize(2) .extracting(Student::firstName) .containsExactly("John", "Will");

De um curso, ainda podemos mapear o relacionamento como bidirecional da mesma forma que fizemos para os relacionamentos anteriores.

>

Também podemos definir operações em cascata, bem como definir se as entidades devem carregar preguiçosa ou avidamente (Many-to-Many relationships are lazy by default).

Conclusion

Que conclui este artigo sobre relacionamentos de entidades mapeadas com JPA. Nós cobrimos as relações Homem-a-Um, Um-a-muitos, Muitos-a-muitos e Um-a-Um. Além disso, exploramos operações em cascata, bidirecionalidade, opcionalidade e fetch-types de carregamento ávido/lazy.

O código para esta série pode ser encontrado em GitHub.

Deixe uma resposta

O seu endereço de email não será publicado.