Relacionamento muitos-para-muitos na APP

Relacionamento muitos-para-muitos na APP

1. Introdução

Neste tutorial, veremos várias maneiras dedeal with many-to-many relationships using JPA.

Para apresentar as ideias, usaremos um modelo de alunos, cursos e várias relações entre eles.

Para fins de simplicidade, nos exemplos de código, mostraremos apenas os atributos e a configuração JPA que está relacionada aos relacionamentos muitos para muitos.

Leitura adicional:

Mapeando Nomes de Classe de Entidade para Nomes de Tabela SQL com JPA

Aprenda como os nomes de tabela são gerados por padrão e como substituir esse comportamento.

Read more

Visão geral dos tipos de cascata JPA / Hibernate

Uma visão geral rápida e prática dos tipos de cascata JPA / Hibernate.

Read more

2. Muitos-para-muitos básicos

2.1. Modelando um relacionamento muitos-para-muitos

Um relacionamento é uma conexão entre dois tipos de entidades. No caso de um relacionamento muitos para muitos, ambos os lados podem se relacionar com várias instâncias do outro lado.

Observe que é possível que os tipos de entidade tenham um relacionamento com eles mesmos. Por exemplo, quando modelamos árvores genealógicas: todo nó é uma pessoa; portanto, se falarmos sobre o relacionamento pai-filho, os dois participantes serão uma pessoa.

No entanto, não faz tanta diferença se falamos sobre um relacionamento entre tipos de entidades únicas ou múltiplas. Já que é mais fácil pensar sobre as relações entre dois tipos de entidades diferentes, usaremos isso para ilustrar nossos casos.

Por exemplo, quando os alunos marcam os cursos de que gostam: um aluno pode gostar demany cursos emany alunos podem gostar do mesmo curso:

image

Como sabemos, em RDBMSes, podemos criar relacionamentos com chaves estrangeiras. Uma vez que ambos os lados devem ser capazes de fazer referência ao outro,we need to create a separate table to hold the foreign keys:

image

Essa tabela é chamada dejoin table. Observe que, em uma tabela de junção, a combinação das chaves estrangeiras será sua chave primária composta.

2.2. Implementação na JPA

Modeling a many-to-many relationship with POJOs é fácil. Devemosinclude a Collection in both classes, que contém os elementos dos outros.

Depois disso, precisamos marcar a classe com@Entity, e a chave primária com@Id para torná-las entidades JPA adequadas.

Além disso, devemos configurar o tipo de relacionamento. Portanto, anotações dewe mark the collections with @ManyToMany:

@Entity
class Student {

    @Id
    Long id;

    @ManyToMany
    Set likedCourses;

    // additional properties
    // standard constructors, getters, and setters
}

@Entity
class Course {

    @Id
    Long id;

    @ManyToMany
    Set likes;

    // additional properties
    // standard constructors, getters, and setters
}

Além disso, precisamos configurar como modelar o relacionamento no RDBMS.

O lado do proprietário é onde configuramos o relacionamento, que, para este exemplo, escolheremos a classeStudent.

We can do this with the @JoinTable annotation in the Student class. Fornecemos o nome da tabela de junção (course_like) e as chaves estrangeiras com as anotações@JoinColumn. O atributojoinColumn se conectará ao lado do proprietário do relacionamento e oinverseJoinColumn ao outro lado:

@ManyToMany
@JoinTable(
  name = "course_like",
  joinColumns = @JoinColumn(name = "student_id"),
  inverseJoinColumns = @JoinColumn(name = "course_id"))
Set likedCourses;

Observe que o uso de@JoinTable ou mesmo@JoinColumn não é necessário: o JPA irá gerar os nomes de tabela e coluna para nós. No entanto, a estratégia que o JPA usa nem sempre corresponderá às convenções de nomenclatura que usamos. Daí a possibilidade de configurar nomes de tabelas e colunas.

On the target side, we only have to provide the name of the field, which maps the relationship. Portanto, definimos o atributomappedBy da anotação@ManyToMany na classeCourse:

@ManyToMany(mappedBy = "likedCourses")
Set likes;

Observe que, comoa many-to-many relationship doesn’t have an owner side in the database, podemos configurar a tabela de junção na classeCourse e referenciá-la a partir da classeStudent.

3. Muitos para muitos usando uma chave composta

3.1. Modelando Atributos de Relacionamento

Digamos que queremos que os alunos avaliem os cursos. Um aluno pode avaliar qualquer número de cursos e qualquer número de alunos pode avaliar o mesmo curso. Portanto, também é um relacionamento muitos para muitos. O que o torna um pouco mais complicado é quethere is more to the rating relationship than the fact that it exists. We need to store the rating score the student gave on the course.

Onde podemos armazenar essas informações? Não podemos colocá-lo na entidadeStudent, pois um aluno pode dar diferentes classificações para diferentes cursos. Da mesma forma, armazená-lo na entidadeCourse também não seria uma boa solução.

Esta é uma situação quandothe relationship itself has an attribute.

Usando este exemplo, anexar um atributo a uma relação se parece com isso em um diagrama de ER:

image

Podemos modelá-lo quase da mesma forma que fizemos com o relacionamento simples de muitos para muitos. A única diferença é que anexamos um novo atributo à tabela de junção:

image

3.2. Criando uma chave composta na JPA

A implementação de um relacionamento simples de muitos para muitos foi bastante direta. O único problema é que não podemos adicionar uma propriedade a um relacionamento dessa maneira, porque conectamos as entidades diretamente. Portanto,we had no way to add a property to the relationship itself.

Como mapeamos atributos de banco de dados para campos de classe em JPA,we need to create a new entity class for the relationship.

Obviamente, toda entidade JPA precisa de uma chave primária. Because our primary key is a composite key, we have to create a new class, which will hold the different parts of the key:

@Embeddable
class CourseRatingKey implements Serializable {

    @Column(name = "student_id")
    Long studentId;

    @Column(name = "course_id")
    Long courseId;

    // standard constructors, getters, and setters
    // hashcode and equals implementation
}

Observe que existem algunskey requirements, which a composite key class has to fulfill:

  • Temos que marcá-lo com@Embeddable

  • Tem que implementarjava.io.Serializable

  • Precisamos fornecer uma implementação dos métodoshashcode()eequals()

  • Nenhum dos campos pode ser uma entidade em si

3.3. Usando uma chave composta na JPA

Usando essa classe de chave composta, podemos criar a classe de entidade, que modela a tabela de junção:

@Entity
class CourseRating {

    @EmbeddedId
    CourseRatingKey id;

    @ManyToOne
    @MapsId("student_id")
    @JoinColumn(name = "student_id")
    Student student;

    @ManyToOne
    @MapsId("course_id")
    @JoinColumn(name = "course_id")
    Course course;

    int rating;

    // standard constructors, getters, and setters
}

Este código é muito semelhante a uma implementação de entidade regular. No entanto, temos algumas diferenças importantes:

  • usamos@EmbeddedId, to mark the primary key, que é uma instância da classeCourseRatingKey

  • marcamos os camposstudentecourse com@MapsId

@MapsId significa que vinculamos esses campos a uma parte da chave e eles são as chaves estrangeiras de um relacionamento muitos para um. Precisamos disso porque, como mencionamos acima, na chave composta não podemos ter entidades.

Depois disso, podemos configurar as referências inversas nas entidadesStudenteCourse como antes:

class Student {

    // ...

    @OneToMany(mappedBy = "student")
    Set ratings;

    // ...
}

class Course {

    // ...

    @OneToMany(mappedBy = "course")
    Set ratings;

    // ...
}

Observe que há uma maneira alternativa de usar chaves compostas: a anotação@IdClass.

3.4. Outras características

Configuramos os relacionamentos para as classesStudenteCourse como@ManyToOne. Poderíamos fazer isso porque, com a nova entidade, decomponhamos estruturalmente o relacionamento muitos para muitos em dois relacionamentos muitos para um.

Por que fomos capazes de fazer isso? Se examinarmos de perto as tabelas no caso anterior, podemos ver que ela continha dois relacionamentos muitos-para-um. In other words, there isn’t any many-to-many relationship in an RDBMS. We call the structures we create with join tables many-to-many relationships because that’s what we model.

Além disso, fica mais claro se falarmos sobre relacionamentos muitos para muitos, porque essa é a nossa intenção. Enquanto isso, uma tabela de junção é apenas um detalhe de implementação; nós realmente não nos importamos com isso.

Além disso, essa solução tem um recurso adicional que ainda não mencionamos. A solução simples de muitos para muitos cria um relacionamento entre duas entidades. Portanto, não podemos expandir o relacionamento para mais entidades. No entanto, nesta solução não temos este limite:we can model relationships between any number of entity types.

Por exemplo, quando vários professores podem ministrar um curso, os alunos podem avaliar como um professor específico ensina um curso específico. That way, a rating would be a relationship between three entities: a student, a course, and a teacher.

4. Muitos para muitos com uma nova entidade

4.1. Modelando Atributos de Relacionamento

Digamos que queremos permitir que os alunos se inscrevam em cursos. Além disso,we need to store the point when a student registered for a specific course. Além disso, também queremos armazenar a nota que ela recebeu no curso.

Em um mundo ideal, poderíamos resolver isso com a solução anterior, quando tivéssemos uma entidade com uma chave composta. No entanto, nosso mundo está longe de ser ideal e os alunos nem sempre concluem um curso na primeira tentativa.

Nesse caso, hámultiple connections between the same student-course pairs, ou várias linhas, com os mesmos paresstudent_id-course_id. Não podemos modelá-lo usando nenhuma das soluções anteriores porque todas as chaves primárias devem ser exclusivas. Portanto, precisamos usar uma chave primária separada.

Portanto,we can introduce an entity, que conterá os atributos do registro:

image

Nesse caso,the Registration entity represents the relationship entre as outras duas entidades.

Por ser uma entidade, terá sua própria chave primária.

Observe que na solução anterior tínhamos uma chave primária composta, que criamos a partir das duas chaves estrangeiras. Agora, as duas chaves estrangeiras não farão parte da chave primária:

image

4.2. Implementação na JPA

Como ocoure_registration se tornou uma tabela regular, podemos criar uma entidade JPA antiga e simples modelando-a:

@Entity
class CourseRegistration {

    @Id
    Long id;

    @ManyToOne
    @JoinColumn(name = "student_id")
    Student student;

    @ManyToOne
    @JoinColumn(name = "course_id")
    Course course;

    LocalDateTime registeredAt;

    int grade;

    // additional properties
    // standard constructors, getters, and setters
}

Além disso, precisamos configurar os relacionamentos nas classesStudenteCourse:

class Student {

    // ...

    @OneToMany(mappedBy = "student")
    Set registrations;

    // ...
}

class Course {

    // ...

    @OneToMany(mappedBy = "courses")
    Set registrations;

    // ...
}

Novamente, configuramos o relacionamento antes. Portanto, precisamos apenas informar ao JPA, onde ele pode encontrar essa configuração.

Observe que poderíamos usar esta solução para resolver o problema anterior: os alunos classificando os cursos. No entanto, é estranho criar uma chave primária dedicada, a menos que seja necessário. Além disso, do ponto de vista de RDBMS, não faz muito sentido, uma vez que combinar as duas chaves estrangeiras fez uma chave composta perfeita. Além disso, essecomposite key had a clear meaning: which entities we connect in the relationship.

Caso contrário, a escolha entre essas duas implementações geralmente é simplesmente uma preferência pessoal.

5. Conclusão

Neste tutorial, vimos o que é um relacionamento muitos para muitos e como podemos modelá-lo em um RDBMS usando JPA.

Vimos três maneiras de modelá-lo no JPA. Todos os três têm vantagens e desvantagens diferentes quando se trata de:

  • clareza de código

  • Clareza do banco de dados

  • capacidade de atribuir atributos ao relacionamento

  • quantas entidades podemos vincular ao relacionamento e

  • suporte para múltiplas conexões entre as mesmas entidades

Como de costume, os exemplos estão disponíveisover on GitHub.