[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007
@[toc]
概述
在Java程序访问关系型数据库这个领域,在国内使用最多的应该是MyBatis与MyBatisPlus,但是老外却特别中意JPA。我以前大多时候也是一直在使用MyBatis与MyBatisPlus,偶尔使用一点。最近公司项目使用了JPA,在使用过程中发现多表关联那块有点蒙,网上的中文资料还比较少,也有点不成体系,所以此处总结一下。
概念
JPA 全称为 Java persistence Api。是一套Java持久化规则,没有具体实现,Java在定义了JDBC的基础上又提供了更高层次的抽象 JPA,本意是统一各种ORM。因为我们目前主要使用Spring生态,所以这里谈论的内容是Spring实现的Jpa版本Spring Data Jpa 结合Hibernate 呈现的 。
Spring Data 是一个伞形项目,里面包含了大量与数据相关的项目,其中Spring Data JAP就是实践Java提出的标准JPA的项目,本文也是基于它实践的。
文本主要内容:
- JPA 主键生成策略
- JPA 多表关联
- JPA多表关联时级联类型
主键生成类型
我们在创建JPA实体类的时候会被要求指定一个id
,一般是数据表的主键。我们需要告诉数据库生成主键的策略,其使用GenerationType
枚举来表示,例如下面代码中指定主键生成策略为IDENTITY
,那这些主键生成策略都有什么区别呢?
@Entity
@Table(name = "student", schema = "public")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Integer id;
}
下面是GenerationType
源码,你可以看下能不能读懂注释,如果没有用过一般都是看不懂的。看不懂不要紧,下面我们举个例子就懂了
public enum GenerationType {
/**
* Indicates that the persistence provider must assign
* primary keys for the entity using an underlying
* database table to ensure uniqueness.
*/
TABLE,
/**
* Indicates that the persistence provider must assign
* primary keys for the entity using a database sequence.
*/
SEQUENCE,
/**
* Indicates that the persistence provider must assign
* primary keys for the entity using a database identity column.
*/
IDENTITY,
/**
* Indicates that the persistence provider should pick an
* appropriate strategy for the particular database. The
* <code>AUTO</code> generation strategy may expect a database
* resource to exist, or it may attempt to create one. A vendor
* may provide documentation on how to create such resources
* in the event that it does not support schema generation
* or cannot create the schema resource at runtime.
*/
AUTO
}
JPA(具体实现hibernate)是可以根据代码自动生成数据库表的,所以如果你愿意的话,开发的时候都不用自己建表。我们给主键设置不同的生成策略,然后看下JPA生成的sql是什么样的,我此次使用的数据库是PostgresSql。
- TABLE
用单独的一张表来生成id
@Id
@GeneratedValue(strategy = GenerationType.TABLE)
@Column(name = "id", nullable = false)
private Integer id;
//创建student表
create table public.student (
id int4 not null,
stu_name varchar(255),
primary key (id)
)
//自动创建一张用于生成id的表
create table hibernate_sequences (
sequence_name varchar(255) not null,
next_val int8,
primary key (sequence_name)
)
//给 hibernate_sequences表插入一条数据
insert into hibernate_sequences(sequence_name, next_val) values ('default',0)
------------------------插入数据-----------------------------
//先给hibernate_sequences表的某一行数据value加1
select
tbl.next_val
from
hibernate_sequences tbl
where
tbl.sequence_name=? for update
of tbl
//更新回hibernate_sequences表
update
hibernate_sequences
set
next_val=?
where
next_val=?
and sequence_name=?
//获取新生成的id,然后给student表插入数据
insert
into
public.student
(stu_name, id)
values
(?, ?)
- SEQUENCE
使用序列来生成主键id,这个概念有的数据库没有,例如mysql。
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,generator = "mySchSeq")
@SequenceGenerator(name = "mySchSeq", sequenceName = "school_seq")
@Column(name = "id", nullable = false)
private Integer id;
//创建表
create table public.student (
id int4 not null,
stu_name varchar(255),
primary key (id)
)
//生成一个 sequence,有些数据库没有这个概念,例如MySQL
// 插入数据,先从序列中获取一个id
select
nextval ('hibernate_sequence')
//插入数据
insert
into
public.student
(stu_name, id)
values
(?, ?)
- IDENTITY
在postgreSQL 中还是通过生成一个序列来实现的:student_id_seq
,但是与SEQUENCE
那种方式还有一定的差别,student表会对生成的序列产生依赖,当删除表的时候,这个序列也就被删除了。MySql使用自增主键的方式,不论是在postgres还是mysql,建议使用这种。
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Integer id;
//创建表以及依赖的序列
create table public.student (
id int4 generated by default as identity,
stu_name varchar(255),
primary key (id)
)
//插入数据
insert
into
public.student
(stu_name)
values
(?)
- AUTO
由采用的数据库自己去决定,像postgresSQL用SEQUENCE,MySql用IDENTITY。下面是postgresSQL测试的情况,可见使用的是SEQUENCE
类型。
create table public.student (
id int4 not null,
stu_name varchar(255),
primary key (id)
)
select
nextval ('hibernate_sequence')
:
insert
into
public.student
(stu_name, id)
values
(?, ?)
关联关系
由于我们使用的是关系型数据库,例如MySql、PostgresSQL等,所以映射到领域对象时就会存在关联。还有一点必须要认识到:Spring data JPA中的的关联关系是由Hibernate实现的。
例如有一张student类,一张account类,一张school表,一张teacher表。
学生与账户是一对一关系,学生与当前学校是多对一关系,反过来学习与学生是一对多关系。学生与老师是多对多的关系,一个学生可以有语文老师,数学老师...,一个老师也会同时教很多学生。
JPA的关联关系又有双向和单向之分,如果双方都申明和对方的关系,那就是双向关联了,如果只有一方申明那就是单向关联了。
例如,学生和账户是一对一关系,如果只在Student里面申明拥有Account,那就是单向关联。如果我们同时在Account中也申明一个Student,那就是双向关联了。你可以从下面的代码中感受一下:
@Entity
@Table(name = "student", schema = "public")
public class Student {
...
@OneToOne(cascade = {CascadeType.ALL})
@JoinColumn(name = "account")
private Account account;
}
//单向关联
@Entity
@Table(name = "account")
public class Account {
...
}
//双向关联
@Entity
@Table(name = "account")
public class Account {
@OneToOne(mappedBy = "account",cascade = CascadeType.ALL)
private Student student;
}
关联后,操作其中一方,就会自动操作另一方。例如我们查询Student的时候其Account属性也会自动被查询出来的。当我们用Mybatis的时候就比较麻烦点,通常我们会为每张表建立一个DAO,然后先查student表得到account的id,再查account,最后转换成我们的领域对象。或者手动join两张表,然后再转换成领域对象。
值得一提的是,关联关系是有主次之分的。例如Student和Account,我们一般认为Student拥有Account,所以我们会将account表的Id作为外键存储在student表里面。然后将Student作为聚合根来操作,如果采用DDD的设计模式的话,修改Account也是通过Student实现的。
那么单向关联和双休关联有什么区别呢?
单向关联:你可以在查询student的时候会自动将account也查出来,反之不行。
双向关联:你即可以在查询student的时候自动将account查出来了,也可以在查询account时将其关联的student查出来。
OneToOne
学生与账户是一对一关系,每个学生有一个唯一的账户,一个账户也只属于一个学生。
- 单向关联
@Entity
@Table(name = "student", schema = "public")
public class Student {
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "account_id")
private Account account;
}
这里的student表关联了account表, student表里面有一列叫account_id
,存放的是account表的id
,作为外键。
- 双向关联
@Entity
@Table(name = "account")
public class Account {
@OneToOne(mappedBy = "account", cascade = CascadeType.ALL)
private Student student;
}
同时给Account实体也加上Student的关联就变成了双向关联。那个mappedBy = "account"
里的account
是Student类里面对应的属性名称,在IDEA甚至可以导航。mappedBy将两边联系了起来,通过它找到了account,然后又通过account上面的@JoinColumn(name = "account_id")
就知道他们的关系了。
ManyToOne
一对多以多方为参照物来说就是多对一,例如学生与学校就是多对一关系。
- 单向关联
@Entity
@Table(name = "student", schema = "public")
public class Student {
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "school_id")
private School school;
}
student表关联了school表,student表里面存在一列school_id
,存放的是school表的id
,作为外键。
- 双向关联
@Entity
@Table(name = "school")
public class School {
@OneToMany(mappedBy = "school", cascade = CascadeType.ALL)
private List<Student> students;
}
请参照OneToOne的说明
OneToMany
学校与学生就是一对多关系,一个学校可以包含很多学生
- 单向
@Entity
@Table(name = "school")
public class School {
@OneToMany( cascade = CascadeType.ALL)
@JoinColumn(name = "school_id")
private List<Student> students;
}
注意那个@JoinColumn(name = "school_id")
中的school_id
是student表中的字段,是它的外键,不是school表的字段。是不是很奇怪?我最开始对@JoinColumn
也很蒙圈,外键具体放在哪张表里面是要依照数据库设计的原则来的,不论一对多,还是多对一都是将一方的id作为外键存放在在多方的表中。一般情况下,我们会将@JoinColumn
放在拥有外键那张表的实体里,像OneToOne与ManyToOne 都比较自然。如果你非要使用OneToMany的单向联合的话,就要注意那个name的列是存放在代表Many的那张表的。
- 双向绑定
当双向绑定时,请使用上一步ManyToOne部分介绍的方法。
ManyToMany
学生与老师之间是多对多的关系。 王二狗同时有语文老师孔夫子,数学老师祖冲之...,而孔夫子也可能同时教学生王二狗,牛翠华...
- 单向绑定
@Entity
@Table(name = "student", schema = "public")
public class Student {
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "student_teacher_relation",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "teacher_id"))
private List<Teacher> teachers;
}
我们知道关系数据库的多对多关系要借助中间表实现的。所以我们使用@JoinTable
来配置这张中间表。student_teacher_relation
是中间表的名称,student_id
是student表的id,teacher_id
是teacher表的id。值得注意的是,这个注解可以省略,然后JPA会按照自己的规则来命名。
- 双向绑定
@Entity
@Table(name = "teacher")
public class Teacher {
@ManyToMany(mappedBy = "teachers",cascade = CascadeType.ALL)
private List<Student> students;
}
和其他级联关系一样。
以上就是JPA中多种关联关系的情况了,他们都是由hibernate实现的,这块也认为是它的精华,也是我们用惯mybaitis人员最不适应的地方。
级联方式
在上面各种关联关系中,我们一直在给关联注解的cascade
属性配置CascadeType.ALL
,像下面这样。
@OneToMany( cascade = CascadeType.ALL)
其实这块也比较难以理解,反正我第一次使用的时候又是蒙圈的,有的小伙伴要问了:你怎么总蒙圈啊?因为长了一颗爱蒙圈的脑袋,哈哈...。言归正传,下面我们遛一遛几个常用的吧
public enum CascadeType {
//所有操作
ALL,
//插入
PERSIST,
//修改
MERGE,
//删除
REMOVE
}
注意这个级联关系只有在非查询的情况下才起作用,查询的时候不涉及。级联其实很好理解,例如Student关联了Account,那我们给student表插入一条数据的话是不是需要一条account,此时你需要告诉JPA先帮你插入一条account数据,再插入一条student数据,像下面代码那样。
@Entity
@Table(name = "student", schema = "public")
public class Student {
...
@OneToOne(cascade = {CascadeType.PERSIST})
@JoinColumn(name = "account")
private Account account;
}
- PERSIST
给student表插入一条数据的时候,当account不为null,那么就需要这个级联类型,以便同时在account表中也插入一条数据
- MERGE
修改student表中某条数据,同时account也有修改时,就需要这个级联类型,以便于同时修改account表中的那条数据。
如果你只设置了CascadeType.PERSIST
,那么这个操作就不能完成。所以你需要
@OneToOne(cascade = {CascadeType.MERGE})
- REMOVE
删除student表中某条数据,同时要删除account里对应的那条数据
设置什么要看你的需求,如果你既有插入操作,也有修改操作,还有移除,那么就设置为CascadeType.ALL
。这块设置不对要报错的,如果你只想对student增删改,那么你可以不设置account的级联关系,但是在操作的时候将account属性对象设置为null
。
总结
从上面讲到的关联关系你也可以看出,互联网企业偏爱mybatis是有自己的道理的,他们不允许将外键维护在数据库中,他们要控制精确查询某几个字段,他们要优化sql提高效率,说白了就是他们要自己控制sql的操作,不需要框架自己完成.... 但对于一般规模的应用来说,JPA其实很不错的。
源码
一如既往,你可以在Github上找到源码:jpa-demo
文章评论