很多同学在使用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注解,拆解一下
-
joinColumns配置为:
@JoinColumn(name = "user_id", referencedColumnName = "id")
即,从中间表跟当前主类的外键描述为:主类(中间表user_class_rel)的user_id字段参考了当前主类(User)的id字段。
-
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;
}