很多同学在使用JPA的时候,每次要对实体建模的时候都分不清JPA的注解用哪一个、注解参数填什么。这主要是由于对两个实体之间的关系的理解不到位,以及不清楚JPA如何描述两个实体之间的关联关系。

本文的目的是为了帮助大家重新梳理一遍JPA中的关联映射,用理解代替死记硬背。我会给出两种版本。一种是【精简版】,方便大家在忘记的时候快速复习;一种是【完整版】,用于循序渐进地学习。

一、精简版

(一)基础说明

若想快速了解,基础说明可以直接跳过

两个实体之间的关系可以分为一对一、一对多、多对多。实体之间的关系表达,可以通过中间表去实现、也可以通过非中间表的方式去实现。

所谓非中间表的方式,即二者的关系存二者其中任一个实体中:

学生student(stu_id, stu_name), 课程class (class_id, class_name),他们二者的关系可以在学生表(student)中添加一个外键字段class_id,接student(stu_id, stu_name, class_id)。当然,这种情况下一个学生只能关联一门课。所以一般非中间表的方式,只能在一对一、一对多的场景下使用。

所谓中间表的方式,即二者的关系存在中间表中:

比如 ,

学生student(stu_id, stu_name), 课程class (class_id, class_name),他们二者的关系可以用一张中间表student_class_rel(stu_id, class_id)来维护,中间表的stu_id, class_id都是外键。中间表通常用来维护多对多的关系;有时候也可以维护一对一或一对多的场景。

可以看到,实体关系的表达就是通过外键来表达的。中间表、非中间两种方式的区别就在于外键的位置:外键是存在两个实体中的任一实体中?还是用一种单独的表来存储?

(二)注解一览表

基本注解表达外键关系的注解实现方式外键存放的位置(这里需要死记硬背一下)
@OneToMany
@ManyToOne
@OneToOne
@JoinCulomun(控制外键字段映射)非中间表(基本注解搭配@JoinCulomun)1. 一对一关系中,在哪个类中使用@JoinColumn的name属性指定外键名,这个外键就存在哪个类;
2. 一对多关系,外键必定会存Many的那一方
@OneToMany
@ManyToOne
@OneToOne
@ManyToMany
@JoinTable(指定中间表的表名)
@JoinCulomun (控制中间表中的外键字段映射)
中间表(基本注解搭配@JoinTable、@JoinCulomun )@JoinTable指定中间表的表名,外键就存这张表;
@JoinCulomun用于指定中间表的字段同两个实体的字段的映射关系

(三)示例

1. 非中间表示例(一对一)

假设一个User只能选一门课程

//用户
class User{
  private long id;
}

//班级
class SchoolClass{
  private long id;
  
  //基本注解,表达实体之间的关系类型是一对一
  @OneToOne  
  //外键注解,在班级实体中用name属性指定了外键名(user_id),所以外键(user_id)会存在班级实体表(SchoolClass)中
  //referencedColumnName的值为id,意思是外键(user_id)关联了另外一个实体(即User)中的id字段。
  @JoinColumn(name = "user_id", referencedColumnName = "id") 
  private User user;
  
}

2. 非中间表示例(一对多)

假设一个User有多个Address

//用户
class User{
  private long id;
  
  //方法一:在User使用OneToMany注解
  //基本注解,表示是一对多
  @OneToMany 
  //一对多场景下,外键一定要存Many端,所以name只能填Address中的外键字段,即user_id
  //那么与之对应的,对方就是User,所以referencedColumnName填id,因为外键(user_id)关联了User的id字段
  @JoinColumn(name="user_id", referencedColumnName = "id")
  private List<Address> addresses;
  
}

//地址
class Address{
  private long id;
  private long userId;
  
//  方法二:在Address使用ManyToOne注解
//  此时,记住一对多、多对一,外键都在Many端,所以这里的name也是填user_id,referencedColumnName填User的id字段
//  @ManyToOne
//  @JoinColumn(name="user_id", referencedColumnName = "id")
//  private User user;
  
  ...
}

//上述两种写法都可以,主要看你想要通过User获取对应的地址列表,还是想通过地址获取到对应的用户

3. 中间表示例

class SchoolClass{
    private Long id;
    ...
}

class User{
    ...
            
    //基本注解 
    @OneToMany
    //外键注解,@JoinTable说明用了中间表
    @JoinTable(name = "user_class_rel", 
              joinColumns = {
                @JoinColumn(name = "user_id", referencedColumnName = "id") }, 
              inverseJoinColumns = {
                @JoinColumn(name = "class_id", referencedColumnName = "id") }
    )
    private List<SchoolClass> classes;
    ...
}
//说明:
    //1. @JoinTable的name属性为user_address_rel,表示中间表的表名叫做user_address_rel(如果不指定,会按照JPA的命名规则自动生成一张表)
    //2. joinColumns(表示中间表跟本方的关系,本方就是@JoinTable所在的类,该例子本方即User)配置了一个@JoinColumn
    //    ,该@JoinColumn的name属性为user_id,表示中间表的外键(user_id)参考了User的id字段
    //3. inverseJoinColumns(表示中间表跟对方的关系)配置了一个@JoinColumn
    //    ,该@JoinColumn的name属性为address_id,表示中间表的外键(class_id)字段参考了SchoolClass的id字段
      

二、完整版

(一)从外键说起

为什么要从外键说起,因为JPA操作的其实是数据库的两张表之间的关系。数据库中两张表之间的关系通过外键来表达。

1. 什么是外键

让我们简单回顾一下什么是外键。

某张表中的一个字段,是另外一张表的主键,那么这个字段就叫做外键。

比如存在下面两张表,

学生表student(id) 、班级表class(id, student_id)

其中,student_id的来源就是student表的id,所以student_id是一个主键。

2. 如何描述外键

如果我们想要表达一个外键同另外一张表、字段之间的关系,我们可能会这样描述:

因为一个表的xx字段是另外一个表的主键,那个主键的字段名叫做yy

这样描述虽然没什么错,但是读起来很是费力,所以我们需要一种更简单、直观的方式来描述。因为我们想要表达太多的信息了。

上面的表达信息有:外键字段名、表名、对应的主键字段名

一般来说,我们可以按照下面的方式来描述:

aa表(主类)的xx字段,参考了yy表(参考类)的mm字段

  • 其中,aa表就是主类,表示关系的维护方,也是外键xx字段的存储方;
  • yy表叫做参考类;

还是以上面的学生、班级表举例。

学生表student(id) 、班级表class(id, student_id)

我们可以这样说:

班级表的student_id参考了学生表的id字段。

其中,班级表class就是主类(关系的维护方+外键的存储方),

参考类是学生表student;

(二)表达关联关系的两种方式

从第一节我们知道,两张表之间的关联关系可以通过添加一个字段来表达,这个字段存放的位置依实际场景要求而异。根据是否需要借助中间表来存储,可以划分为两种不同的实现方式:

1. 不借助中间表

意思就是,这个关联字段放在两张表中的其中一张表中(即双方的关系交由其中一张表来负责维护)。

比如,要表达 [学生、班级]的关系,那么可以在班级表添加一个student_id,就能够记录二者的关系,如下所示:

学生表student(id, class_id) 

班级表class(id, student_id)

此时,student_id是一个外键。

班级表的student_id字段参考了student表的id字段。

则班级表class为主类(关系维护方+外键存储方),学生表student为参考类

2. 借助中间表

意思就是创建一个新的关系表来记录二者之间的关联关系。

比如,要表达 [学生、班级的关系],则是:

学生表student(id) 

班级表class(id)

学生-班级关系表student_class_rel(student_id, class_id)

此时,student_id和 class_id字段都是外键。

  • **student_id参考了student表中的id字段。**主类是student_class_rel,参考类是student;
  • **class_id参考了class表中的id字段;**主类是student_class_rel,参考类是class;

3. 二者的异同

  • 最关键的差别:是否需要借助中间表来表达关系,即外键的位置不同;
  • 相同点:都用到了主键;

(三)JPA中如何表达关联关系

JPA中现在通常是通过注解来进行配置,所以一般是通过注解来表达。我们首先来对JPA中同关联关系的有关的注解做一个分类。

1. JPA相关注解的分类

我会将注解划分为两类:

  • 表达实体关系类型的注解:标记两个实体之间属于何种关系(一对一、一对多、多对多等);
  • 表达外键关系的注解:标记外键相关信息;

下面先做一个简单的总结。

基本注解表达外键关系的注解实现方式
@OneToMany
@ManyToOne
@OneToOne
@JoinCulomun(控制外键字段映射)非中间表(基本注解搭配@JoinCulomun)
@OneToMany
@ManyToOne
@OneToOne
@ManyToMany
@JoinTable(指定中间表的表名)
@JoinCulomun (控制中间表中的外键字段映射)
中间表(基本注解搭配@JoinTable、@JoinCulomun )

简单来说,

  • 如果不用中间表,仅需要使用@JoinCulomun注解即可;
  • 若通过中间表来维护关系,必须通过@JoinTable 和@JoinCulomun 配合使用;

可以发现,@JoinTable 和@JoinCulomun两个注解是JPA中表达关联关系的核心注解,我们接下来依次介绍。

2. @ JoinCulomun的使用

主要作用

@JoinCulomun注解的主要作用是配置外键信息。

核心参数

还记得我们在上面是如何描述外键的吗?

aa表(主类)的xx字段,参考了yy表(参考类)的mm字段

@JoinCulomun注解注解中有两个参数,分别是name和referencedColumnName,其中

  • name,指的就是外键的字段名,也就是上面的xx;
  • referencedColumnName,指的是参考的字段名,也就是上面的mm;

嗯哼,这不是还少了个主类和参考类吗?是滴,在JPA中,关系主类和参考类是通过上下文来确定的,这个默认的规则是什么呢,请接着往下看。

外键位置的确定

在JPA建模的过程中,最关键的就是确定外键是什么,以及外键存哪里,维护关系的主类是谁。大部分时间里,我们可以主动自己去控制外键的位置,这里做一个简单的小结:

关系类型外键位置如何确定维护关系的主类
一对一1. 存任意的一方都可以(简单,不需要额外的表);
2. 存中间表(有特殊需要时可选);
1. 若使用中间表,则外键存中间表,中间表就是维护关系的主类;
2. 若不使用中间表,在哪个类中使用@JoinColumn指定外键名,这个外键就存在哪个类,那么这个类就是维护关系的主类;
一对多、多对一1. 存多(Many)的一方(简单,不需要额外的表);
2. 存中间表(有特殊需要时可选);
1. 若使用中间表,则外键存中间表,中间表就是维护关系的主类;
2. 若不使用中间表,则外键就会存到Many一方,则Many就是维护关系的主类(这个地方很容易混错!);
多对多中间表(多对多的场景下,只能用中间表)维护关系的是中间表,中间表就是主类

示例

来看一个例子:

//用户
class User{
  private long id;
}

//班级
class SchoolClass{
  private long id;
  
  @OneToOne  
  @JoinColumn(name = "user_id", referencedColumnName = "id") 
  private User user;
  
}

上面用到了两个注解,可以根据下面的思路去理解:

  • 先确定当前的主类,这是一对一的场景,因为@JoinColumn是写在SchoolClass类中的,所以主类是SchoolClass,那么被参考类就是User;
  • @OneToOne 表示在当前主类中,主类(即SchoolClass)和User是一对一的关系;
  • @JoinColumn 表示,当前类(SchoolClass)的user_id字段参考了被参考类(即User)的id字段;

其实在实际操作中,我们会发现,只要我们使用了@JoinColumn注解,指定了name参数,就会在当前主类所对应的表(user表)中自动生成一个user_id字段。

上述的实现,我们是把关联关系放到了SchoolClass表中维护,如果你想要在User中去维护一对一的关系,也是可以的。代码如下:

//用户
class User{
  private long id;
  

  @OneToOne  
  // 意思是当前主类(User)参考了SchoolClass的id字段
  // JPA会自动在user表中添加名为class_id的字段
  @JoinColumn(name = "class_id", referencedColumnName = "id") 
  private SchoolClass clazz;

}

//班级
class SchoolClass{
  private long id;
}

另一个示例

在一对多和多对一的场景下,很容易犯错。我们来看一下是为什么。

假设

User(用户)和Address(地址),User和Address是一对多的关系。

尝试考虑一下下面例子中的@JoinColumn的name应该填什么?

class User{
  private long id;
  
  @OneToMany 
  @JoinColumn(name="猜猜这里应该填写什么")
  private List<Address> addresses;
  
}

class Address{
  private long id;
  private long userId;
  ...
}

上面也说过了,最关键的是:外键字段是什么,外键存在哪里,谁是维护关系的主类?

所以,这道完形填空题的解题思路是:

  • User(一)和Address(多)的是一对多的关系;
  • 图中没有使用@JoinTable,说明没有使用中间表,根据【不同关系的外键位置】中的表格可以知道,外键会存在Many的一方;
  • Address是Many的一方,所以外键会存在Address,则Address就是维护关系的主类;
  • 既然外键字段存在Address中,那么一般来说这个字段就是User的id了,就叫做user_id即可;
  • 根据之前的讲述,@JoinColumn中的name要填主类的外键数据库字段名,所以应该填user_id;

结果如下所示:

class User{
  private long id;
  
  @OneToMany 
  @JoinColumn(name="user_id")
  private List<Address> addresses;
  
}

class Address{
  private long id;
  private long userId
  ...
}

3.@JoinTable的使用

主要作用

主要作用有两个:

  • 指定中间表的表名;
  • 配置中间表中的2个外键信息(因为中间表中是记录两张表的关系,所以会有两个主键);

核心参数

@JoinTable的核心参数有3个:分别是name、joinColumns、inverseJoinColumns。

其中:

  • name:用于指定中间表的表名;
  • joinColumns:指的是在中间表中,同主类的外键信息;
  • inverseJoinColumns:指的是在中间表中,同参考类的外键信息;

示例

下面是通过中间表来实现一对多关系的例子。

class SchoolClass{
    private Long id;
    ...
}

class User{
    ...
    //1. @JoinTable的name属性为user_address_rel,表示中间表的表名叫做user_address_rel(如果不指定,会按照JPA的命名规则自动生成一张表)
    //2. joinColumns配置了一个@JoinColumn
    //                  ,该@JoinColumn的name属性为user_id,表示中间表的user_id参考了User的id字段
    //3. inverseJoinColumns配置了一个@JoinColumn
    //                  ,该@JoinColumn的name属性为address_id,表示中间表的class_id字段参考了SchoolClass的id字段
    @OneToMany
    @JoinTable(name = "user_class_rel", 
              joinColumns = {
                @JoinColumn(name = "user_id", referencedColumnName = "id") }, 
              inverseJoinColumns = {
                @JoinColumn(name = "class_id", referencedColumnName = "id") }
    )
    private List<SchoolClass> classes;
    ...
}

上面用到了两个注解 @OneToMany和@JoinTable,要准确理解注解的含义可以按照下面步骤来思考:

  • 先确定主类。因为关系是维护在user_class_rel表中,所以主类不是User也不是SchoolClass,而是中间表user_class_rel;

  • 使用了 @OneToMany,意味着主类同参考类的关系是一对多,即User(1) → SchoolClass(n);

  • 使用了 @JoinTable注解,拆解一下

    1. joinColumns配置为:@JoinColumn(name = "user_id", referencedColumnName = "id")

      即,从中间表跟当前主类的外键描述为:主类(中间表user_class_rel)的user_id字段参考了当前主类(User)的id字段。

    2. inverseJoinColumns配置了:@JoinColumn(name = "class_id", referencedColumnName = "id")

      即,从中间表到参考类的外键描述为:**主类(中间表user_class_rel)**的class_id字段参考了参考类(SchoolClass)的id字段。

JPA的双向关联

上面的例子中,都是一方持有另一方的引用。有时候,我们会碰到需要关联的场景,比如:

有User(用户)和Address(地址),User和Address是一对多的关系。
我们希望通过Movie实体直接拿到电影所对应的Cinema,也想通过Cinema实体直接获取到对应的Movie。

上面的场景中,Movie和Cinema存在关联关系,并且通过其中的任意一方都要能够获取到另外一方的引用,这就属于双向关联。

首先,一对多的场景中,关系一般考虑放在多(Many)端维护,在上述场景中,就是Address,那么我们就把Address当做关系的维护主类,对于Address来说,应该使用@ManyToOne注解

所以我们先定义好Address

class Address{
  private long id;
  
  @ManyToOne  
  //Many端是Address,则Address是主类
  //关系主类的字段user_id,参考了User中的id字段
  //此时,会在关系维护主类Address对应的表中自动添加user_id字段
  @JoinColumn(name = "user_id", referencedColumnName = "id") 
  private User user;
  
}

这时候,根据需求,Address有了User的引用。但是我们想要做双向关联,所以我们希望在User中也有Address的引用。

  • 对于User来说,它跟Address的关系是OneToMany;
  • 上一步操作中,已经在Address维护了双方的关系了,并且已经自动往Address表添加一个user_id字段;
  • 所以对于User来说,只需要添加添加一个mappedBy参数即可,mappedBy参数填写的不是数据库字段,而是是关系维护方的字段名。

代码如下:

class User{
  private long id;
  
  @OneToMany  
  //表示关联关系由对方(Address)的user字段负责
  @JoinColumn(mappedBy = "user") 
  private List<Address> addresses;
  
}

(四)总结

使用JPA表示实体关系的步骤:

  • 首先,明确两个实体之间的关联关系(一对多、多对多、多对一、一对一);
  • 然后,明确谁来维护关联关系,也就是外键字段所要存放的主类;
  • 接着,在主类使用@JoinColumn或者 @JoinTable维护关联信息;
  • 如果需要双向关联,那么在参考类中使用mappedBy参数来提醒JPA,关系是由对方来维护的;

1. 非中间表示例(一对一)

//用户
class User{
  private long id;
}

//班级
class SchoolClass{
  private long id;
  
  //基本注解,表达实体之间的关系类型是一对一
  @OneToOne  
  //外键注解,在班级实体中用name属性指定了外键名(user_id),所以外键(user_id)会存在班级实体表(SchoolClass)中
  //referencedColumnName的值为id,意思是外键(user_id)关联了另外一个实体(即User)中的id字段。
  @JoinColumn(name = "user_id", referencedColumnName = "id") 
  private User user;
  
}