Many-To-Many Relationship in JPA – JPA中的多对多关系

最后修改: 2018年 11月 15日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll see multiple ways to deal with many-to-many relationships using JPA.

在本教程中,我们将看到使用JPA处理多对多关系的多种方法。

We’ll use a model of students, courses, and various relationships between them.

我们将使用一个学生、课程和它们之间的各种关系的模型。

For the sake of simplicity, in the code examples, we’ll only show the attributes and JPA configuration that’s related to the many-to-many relationships.

为了简单起见,在代码示例中,我们将只展示与多对多关系有关的属性和JPA配置。

2. Basic Many-to-Many

2.基本的多对多

2.1. Modeling a Many-to-Many Relationship

2.1.建立多对多关系的模型

A relationship is a connection between two types of entities. In the case of a many-to-many relationship, both sides can relate to multiple instances of the other side.

关系是两种类型的实体之间的连接。在多对多的关系中,双方都可以与另一方的多个实例相联系。

Note that it’s possible for entity types to be in a relationship with themselves. Think about the example of modeling family trees: Every node is a person, so if we talk about the parent-child relationship, both participants will be a person.

请注意,实体类型有可能与自己发生关系。想一想为家庭树建模的例子。每个节点都是一个人,所以如果我们谈论父子关系,两个参与者都将是一个人。

However, it doesn’t make such a difference whether we talk about a relationship between single or multiple entity types. Since it’s easier to think about relationships between two different entity types, we’ll use that to illustrate our cases.

然而,无论我们谈论的是单个或多个实体类型之间的关系,都不会有这样的区别。由于思考两个不同的实体类型之间的关系比较容易,我们将用它来说明我们的案例。

Let’s take the example of students marking the courses they like.

让我们以学生给他们喜欢的课程打分为例。

A student can like many courses, and many students can like the same course:

一个学生可以喜欢许多课程,而许多学生可以喜欢同一门课程。

simple-er

As we know, in RDBMSs we can create relationships with foreign keys. Since both sides should be able to reference the other, we need to create a separate table to hold the foreign keys:

正如我们所知,在RDBMS中,我们可以用外键创建关系。由于双方都应该能够引用对方,我们需要创建一个单独的表来保存外键

simple-model-updated

Such a table is called a join table. In a join table, the combination of the foreign keys will be its composite primary key.

这样的表被称为连接表。在连接表中,外键的组合将是其复合主键。

2.2. Implementation in JPA

2.2.在JPA中的实现

Modeling a many-to-many relationship with POJOs is easy. We should include a Collection in both classes, which contains the elements of the others.

用POJO模拟多对多的关系很容易。我们应该在两个类中都包含一个Collection,它包含其他的元素。

After that, we need to mark the class with @Entity and the primary key with @Id to make them proper JPA entities.

之后,我们需要用@Entity标记类,用@Id标记主键,使其成为合适的JPA实体。

Also, we should configure the relationship type. So, we mark the collections with @ManyToMany annotations:

另外,我们应该配置关系类型。所以,我们用@ManyToMany注解来标记集合。

@Entity
class Student {

    @Id
    Long id;

    @ManyToMany
    Set<Course> likedCourses;

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

@Entity
class Course {

    @Id
    Long id;

    @ManyToMany
    Set<Student> likes;

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

Additionally, we have to configure how to model the relationship in the RDBMS.

此外,我们还必须配置如何在RDBMS中建立关系模型。

The owner side is where we configure the relationship. We’ll use the Student class.

业主一方是我们配置关系的地方。我们将使用Student类。

We can do this with the @JoinTable annotation in the Student class. We provide the name of the join table (course_like) as well as the foreign keys with the @JoinColumn annotations. The joinColumn attribute will connect to the owner side of the relationship, and the inverseJoinColumn to the other side:

我们可以通过@JoinTable注解在Student类中做到这一点。我们提供连接表的名称(course_like)以及带有@JoinColumn注释的外键。joinColumn属性将连接到关系的所有者一方,而inverseJoinColumn连接到另一方。

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

Note that using @JoinTable or even @JoinColumn isn’t required. JPA will generate the table and column names for us. However, the strategy JPA uses won’t always match the naming conventions we use. So, we need the possibility to configure table and column names.

注意,使用@JoinTable甚至@JoinColumn都不是必须的。JPA将为我们生成表和列的名称。然而,JPA使用的策略并不总是与我们使用的命名惯例相匹配。因此,我们需要有可能配置表和列的名称。

On the target side, we only have to provide the name of the field, which maps the relationship.

在目标方,我们只需提供字段的名称,它映射了关系。

Therefore, we set the mappedBy attribute of the @ManyToMany annotation in the Course class:

因此,我们在Course类中设置@ManyToMany注解的mappedBy属性。

@ManyToMany(mappedBy = "likedCourses")
Set<Student> likes;

Keep in mind that since a many-to-many relationship doesn’t have an owner side in the database, we could configure the join table in the Course class and reference it from the Student class.

请记住,由于多对多关系在数据库中没有所有者一方,我们可以在Course类中配置连接表,并从Student类中引用它。

3. Many-to-Many Using a Composite Key

3.使用复合键的多对多

3.1. Modeling Relationship Attributes

3.1.关系属性的建模

Let’s say we want to let students rate the courses. A student can rate any number of courses, and any number of students can rate the same course. Therefore, it’s also a many-to-many relationship.

假设我们想让学生给课程评分。一个学生可以给任何数量的课程评分,任何数量的学生也可以给同一门课程评分。因此,这也是一种多对多的关系。

What makes this example a bit more complicated is that there 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.

让这个例子变得有点复杂的是,评级关系除了存在这一事实外,还有更多的内容。我们需要存储学生对课程的评价分数。

Where can we store this information? We can’t put it in the Student entity since a student can give different ratings to different courses. Similarly, storing it in the Course entity wouldn’t be a good solution either.

我们在哪里可以存储这些信息?我们不能把它放在Student实体中,因为一个学生可以对不同的课程给予不同的评价。同样地,将其存储在Course实体中也不是一个好的解决方案。

This is a situation when the relationship itself has an attribute.

这是一种关系本身具有属性的情况。

Using this example, attaching an attribute to a relation looks like this in an ER diagram:

使用这个例子,将一个属性附加到一个关系上,在ER图中看起来像这样。

relation-attribute-er

We can model it almost the same way as the simple many-to-many relationship. The only difference is that we attach a new attribute to the join table:

我们可以用几乎与简单的多对多关系相同的方式来模拟它。唯一的区别是,我们给连接表附加了一个新的属性:

relation attribute model updated

3.2. Creating a Composite Key in JPA

3.2.在JPA中创建一个复合键

The implementation of a simple many-to-many relationship was rather straightforward. The only problem is that we cannot add a property to a relationship that way because we connected the entities directly. Therefore, we had no way to add a property to the relationship itself.

一个简单的多对多关系的实现是相当直接的。唯一的问题是,我们不能以这种方式向关系添加属性,因为我们直接连接了实体。因此,我们没有办法为关系本身添加一个属性。

Since we map DB attributes to class fields in JPA, we need to create a new entity class for the relationship.

由于我们将DB属性映射到JPA中的类字段,我们需要为关系创建一个新的实体类。

Of course, every JPA entity needs a primary key. Because our primary key is a composite key, we have to create a new class that will hold the different parts of the key:

当然,每个JPA实体都需要一个主键。由于我们的主键是一个复合键,我们必须创建一个新的类来保存键的不同部分

@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
}

Note that a composite key class has to fulfill some key requirements:

请注意,复合钥匙类必须满足一些钥匙要求

  • We have to mark it with @Embeddable.
  • It has to implement java.io.Serializable.
  • We need to provide an implementation of the hashcode() and equals() methods.
  • None of the fields can be an entity themselves.

3.3. Using a Composite Key in JPA

3.3.在JPA中使用复合键

Using this composite key class, we can create the entity class, which models the join table:

使用这个复合键类,我们可以创建实体类,它为连接表建模。

@Entity
class CourseRating {

    @EmbeddedId
    CourseRatingKey id;

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

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

    int rating;
    
    // standard constructors, getters, and setters
}

This code is very similar to a regular entity implementation. However, we have some key differences:

这段代码与普通实体的实现非常相似。然而,我们有一些关键的区别。

  • We used @EmbeddedId to mark the primary key, which is an instance of the CourseRatingKey class.
  • We marked the student and course fields with @MapsId.

@MapsId means that we tie those fields to a part of the key, and they’re the foreign keys of a many-to-one relationship. We need it because, as we mentioned, we can’t have entities in the composite key.

@MapsId意味着我们将这些字段与键的一部分联系起来,它们是多对一关系的外键。我们需要它,因为正如我们提到的,我们不能在复合键中拥有实体。

After this, we can configure the inverse references in the Student and Course entities as before:

之后,我们可以像以前一样在StudentCourse实体中配置反向引用。

class Student {

    // ...

    @OneToMany(mappedBy = "student")
    Set<CourseRating> ratings;

    // ...
}

class Course {

    // ...

    @OneToMany(mappedBy = "course")
    Set<CourseRating> ratings;

    // ...
}

Note that there’s an alternative way to use composite keys: the @IdClass annotation.

请注意,还有一种使用复合键的方法:@IdClass注解。

3.4. Further Characteristics

3.4.进一步的特征

We configured the relationships to the Student and Course classes as @ManyToOne. We could do this because with the new entity we structurally decomposed the many-to-many relationship to two many-to-one relationships.

我们将与StudentCourse类的关系配置为@ManyToOne。我们可以这样做,因为通过新的实体,我们在结构上将多对多的关系分解为两个多对一的关系。

Why were we able to do this? If we inspect the tables closely in the previous case, we can see that it contained two many-to-one relationships. 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.

为什么我们能够做到这一点?如果我们仔细检查前面案例中的表,我们可以看到它包含两个多对一的关系。换句话说,在RDBMS中不存在任何多对多的关系。我们把用连接表创建的结构称为多对多的关系,因为这就是我们的模型。

Besides, it’s more clear if we talk about many-to-many relationships because that’s our intention. Meanwhile, a join table is just an implementation detail; we don’t really care about it.

此外,如果我们谈论多对多的关系会更清楚,因为那是我们的意图。同时,连接表只是一个实现细节;我们并不真正关心它。

Moreover, this solution has an additional feature we haven’t mentioned yet. The simple many-to-many solution creates a relationship between two entities. Therefore, we cannot expand the relationship to more entities. But we don’t have this limit in this solution: we can model relationships between any number of entity types.

此外,这种解决方案还有一个我们还没有提到的特点。简单的多对多解决方案在两个实体之间建立了一种关系。因此,我们不能将这种关系扩展到更多的实体。但是我们在这个解决方案中没有这个限制。我们可以在任何数量的实体类型之间建立关系模型。

For example, when multiple teachers can teach a course, students can rate how a specific teacher teaches a specific course. That way, a rating would be a relationship between three entities: a student, a course and a teacher.

例如,当多个教师可以教授一门课程时,学生可以对特定教师教授特定课程的情况进行评分。这样一来,评级将是三个实体之间的关系:一个学生、一门课程和一个教师。

4. Many-to-Many With a New Entity

4.使用新实体的多对多

4.1. Modeling Relationship Attributes

4.1.关系属性的建模

Let’s say we want to let students register for courses. Also, we need to store the point when a student registered for a specific course. On top of that, we want to store what grade she received in the course.

比方说,我们想让学生注册课程。另外,我们需要存储学生注册特定课程的时间点。除此之外,我们还想存储她在该课程中获得的成绩。

In an ideal world, we could solve this with the previous solution, where we had an entity with a composite key. However, the world is far from ideal, and students don’t always accomplish a course on the first try.

在一个理想的世界里,我们可以用以前的解决方案来解决这个问题,即我们有一个带有复合键的实体。然而,这个世界远非理想,学生并不总是在第一次尝试时就完成课程。

In this case, there are multiple connections between the same student-course pairs, or multiple rows, with the same student_id-course_id pairs. We can’t model it using any of the previous solutions because all primary keys must be unique. So, we need to use a separate primary key.

在这种情况下,相同的学生-课程对之间有多个连接,或多条记录,有相同的student_id-course_id对。我们不能用之前的任何解决方案来建模,因为所有的主键必须是唯一的。所以,我们需要使用一个单独的主键。

Therefore, we can introduce an entity, which will hold the attributes of the registration:

因此,我们可以引入一个实体,它将持有注册的属性。

relation entity-er updated

In this case, the Registration entity represents the relationship between the other two entities.

在这种情况下,注册实体代表其他两个实体之间的关系

Since it’s an entity, it’ll have its own primary key.

由于它是一个实体,它将有自己的主键。

In the previous solution, remember that we had a composite primary key we created from the two foreign keys.

在前面的解决方案中,记得我们有一个由两个外键创建的复合主键。

Now the two foreign keys won’t be part of the primary key:

现在这两个外键不会成为主键的一部分。

relation entity model updated

4.2. Implementation in JPA

4.2.在JPA中的实现

Since the course_registration became a regular table, we can create a plain old JPA entity modeling it:

由于course_registration成为一个普通的表,我们可以创建一个普通的JPA实体对其进行建模。

@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
}

We also need to configure the relationships in the Student and Course classes:

我们还需要配置StudentCourse类中的关系。

class Student {

    // ...

    @OneToMany(mappedBy = "student")
    Set<CourseRegistration> registrations;

    // ...
}

class Course {

    // ...

    @OneToMany(mappedBy = "course")
    Set<CourseRegistration> registrations;

    // ...
}

Again, we configured the relationship earlier, so we only need to tell JPA where can it find that configuration.

同样,我们在前面配置了这个关系,所以我们只需要告诉JPA在哪里可以找到这个配置。

We could also use this solution to address the previous problem of students rating courses. However, it feels weird to create a dedicated primary key unless we have to.

我们也可以用这个方案来解决之前学生对课程评级的问题。然而,除非有必要,否则创建一个专门的主键感觉很奇怪。

Moreover, from an RDBMS perspective, it doesn’t make much sense since combining the two foreign keys made a perfect composite key. Besides, that composite key had a clear meaning: which entities we connect in the relationship.

此外,从RDBMS的角度来看,这并没有什么意义,因为将两个外键结合起来就形成了一个完美的复合键。此外,那个复合键有一个明确的含义:我们在关系中连接哪些实体。

Otherwise, the choice between these two implementations is often simply personal preference.

否则,在这两种实现方式之间的选择往往只是个人的偏好。

5. Conclusion

5.总结

In this article, we saw what a many-to-many relationship is and how can we model it in an RDBMS using JPA.

在这篇文章中,我们看到了什么是多对多的关系,以及我们如何在RDBMS中使用JPA对其进行建模。

We saw three ways to model it in JPA. All three have different advantages and disadvantages when it comes to these aspects:

我们看到了三种在JPA中建模的方式。当涉及到这些方面时,三者都有不同的优势和劣势。

  • code clarity
  • DB clarity
  • ability to assign attributes to the relationship
  • how many entities we can link with the relationship
  • support for multiple connections between the same entities

As usual, the examples are available over on GitHub.

像往常一样,这些例子可以在GitHub上找到