我们都知道数据库表与表之间有如下四种关系
1:1(一对一,相应的注解叫@OneToOne)
1:n(一对多,相应的注解叫@OneToMany)
n:1(多对一,相应的注解叫@ManyToOne)
n:n(多对多,相应的注解叫@ManyToMany)
环境说明:
首先我们在讲解之前,我们先约定几个表关系
然后我在代码中定义了一个抽象的entity父类
AbstractEntity
,所有的entity都继承于它
@Data
@Accessors(chain = true)
@MappedSuperclass
@JsonIgnoreProperties(value = {"handler","hibernateLazyInitializer","fieldHandler"})
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") //防止entity互相引用导致json解析进入死循环
public abstract class AbstractEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 自增主键
Long id;
@Temporal(TemporalType.TIMESTAMP) // 时间格式:YYYY-MM-dd HH:mm:ss
@Column(name = "gmt_create", columnDefinition = "timestamp DEFAULT CURRENT_TIMESTAMP comment '创建时间'")
Date gmtCreate;
@LastModifiedDate
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "gmt_modified", columnDefinition = "timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP comment '最后修改时间'")
Date gmtModified;
}
在讲解这四个注解之前,我们还需要先了解一下@JoinColumn
这个注解
@JoinColumn用于注释表中的字段,与@Column不同的是它要保存表与表之间关系的字段;
- name:是用来标记表中对应的字段的名称。如果不设置name的值,默认情况下,name的取值规则如下:name=关联的表的名称 + "_" + 关联表主键的字段名。
- referencedColumnName:默认情况下,关联的实体的主键一般用来做外键的。如果不想用主键作为外键,则需要设置referencedColumnName属性,如:
@JoinColumn(name="emp_id", referencedColumnName="emp_no")
下面我们分别来对四个注解来进行讲解
一. @OneToOne
表 employees 和 address 是一对一的关系
在jpa中是这样定义的:
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@Entity
public class Address extends AbstractEntity {
// ...其它字段
@OneToOne
@JoinColumn(name = "emp_id")
private Employee employee;
}
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "employees")
public class Employee extends AbstractEntity {
@OneToOne
@JoinColumn(name = "addr_id")
private Address address;
// ...
}
@OneToOne有如下几个可选参数
targetEntity=void.class
,关联的实体类。
cascade={}
,可选值有CascadeType.ALL
,CascadeType.PERSIST
,CascadeType.MERGE
,CascadeType.REMOVE
,CascadeType.REFRESH
,CascadeType.DETACH
。
fetch=FetchType.EAGER
,可选值有FetchType.EAGER
,FetchType.LAZY
。
optional=true
,可选值有true
,false
。
mappedBy=""
。
orphanRemoval=false
,可选值有false
,true
。
我们对以上几个参数做下详细的说明:
optional:表示关联的实体是否能够存在null值。默认为true,表示可以存在null值。如果为false,则要同时配合使用 @JoinColumn 标记。
fetch:在一对一关系中,fetch 默认是 FetchType.EAGER的,也就是立即提取关联的实体,FetchType.LAZY 表示懒加载,只有你在 get 这个关联实体的时候,jpa 才会去数据库执行这个子查询。
targetEntity: 该参数可以不指定,JPA会自动将外键关联到Employee,如果没有@JoinColumn参数指定关联字段,默认生成的外键字段名称为
属性名_主键名
, 如Address中Employee属性名称为employee,Employee表的主键名称为 emp_no,那么生成的外键字段名称就为employee_emp_no
。cascade:级联操作,JPA允许您将状态转换从父实体传播到子实体。为此,JPA javax.persistence.CascadeType定义了各种级联操作类型。对于这个参数,我们来做个实验。
在上面我们创建entity的时候,没有指定由谁管理外键,双方都有对方的外键字段,也就是employee中有外键addr_id,address中也有外键emp_id。
如果此时我们不指定cascade,也就是cascade默认为{}
的时候,我们插入一条数据
Employee employee = new Employee()
.setFirstName("Tom")
.setLastName("Welliam")
.setBirthDate(new Date())
.setHireDate(new Date())
.setGender(Employee.Gender.F)
.setAddress(new Address().setHome("保利国际B1栋4703"));
employeeRepository.save(employee);
我们插入一条员工数据,并且set了员工的地址信息,发现插入报错,如下:
因为address在数据库中还不存在,无法保存员工地址信息。
如果我们在Address的employee字段上加上cascade = CascadeType.PERSIST
会出现什么结果呢?
public class Address extends AbstractEntity {
@OneToOne(cascade = CascadeType.PERSIST)
private Employee employee;
}
同样运行上面的保存员工信息的代码,发现还是报同样的错误,为什么呢?因为我们save的是Employee,在Address 中的 cascade 对save Employee是无效的。 如果要想看到效果,那么我们可以把上面的代码修改一下,改为保存Address:
Address address = new Address().setHome("保利国际B1栋4703")
.setEmployee(new Employee().setFirstName("Tom")
.setLastName("Welliam")
.setBirthDate(new Date())
.setHireDate(new Date())
.setGender(Employee.Gender.F));
addressRepository.save(address);
可以看到,当我们save address的时候,jpa执行了两条SQL语句,先插入employee,然后再插入address。
这里我们先来明确一个概念:父表和字表。父表和子表的概念我们也可以理解为主、副之分,比如此处员工和地址,一般我们认为,某地址是属于某个员工的,那么我们说员工表应该为父表(主表),地址表为子表(副表)。所以我们应该把cascade = CascadeType.PERSIST
作用在父表 Employee 的 address 字段上,表示由employee来管理address。所以员工和地址的一对一关系应该改成下面这样
public class Employee extends AbstractEntity {
@OneToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "addr_id")
private Address address;
}
public class Address extends AbstractEntity {
@OneToOne
@JoinColumn(name = "emp_id")
private Employee employee;
}
至于cascade 的不同取值说明如下:
CascadeType.PERSIST
: 关联持久化(插入)
CascadeType.MERGE
:级联合并(更新)
CascadeType.REMOVE
:关联删除
CascadeType.REFRESH
: 关联刷新(查询)
CascadeType.DETACH
:脱离关联,也就是关闭外键检查,放在父表时,那么就可以单独删除父表数据,而不影响子表数据。放在子表时删除子表数据无效。
CascadeType.ALL
:包含以上所有关联操作逻辑
- mappedBy: 上面讲到父表和子表的区分,在上面我们并没有区分员工和地址谁是父谁是子,父表是有外键的一方,而子表是没有外键的一方。如果我们在双方都没有指定mappedBy参数,那么双方将互为父子关系,双方都有外键字段。上面我们分析过,员工和地址表之间,员工应该为父表,地址应该为子表,通常我们查询员工信息的时候需要带出员工的地址信息,所以员工表中应该存在地址表的外键字段,而地址表中不需要员工的外键字段。如何表示这个关系呢?这就是 mappedBy 的作用了,我们在子表 Address 的 employee 字段上加上
mappedBy="address"
(注意这个address是属性名称,如果你在 Employee 中定义 Address 的属性名称为 addr ,那么这里就要写成mappedBy="addr"
),它表示将Address 交给 Employee 去管理。
public class Address extends AbstractEntity {
@OneToOne(mappedBy = "address")
@JoinColumn(name = "emp_id")
private Employee employee;
}
public class Employee extends AbstractEntity {
@OneToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "addr_id")
private Address address; // mappedBy 要指定的名称
}
- orphanRemoval:我懒得去做测试了,这里有人已经做过测试https://www.oschina.net/question/925076_157346,简单来说,可以将它理解为 CascadeType.REMOVE 的加强, CascadeType.REMOVE 是删除,而 orphanRemoval 可以仅移除关联关系,也就是将外键设置为null,数据依然保留(也许理解不一定正确,如有错误,还请不吝指教)。
二. @OneToMany 和 @ManyToOne
employees 和 salaries 是一对多的关系
在jpa中是这样定义的:
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "Salaries")
public class Salary extends AbstractEntity {
@ManyToOne
private Employee employee;
}
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "employees")
public class Employee extends AbstractEntity {
@OneToMany(mappedBy = "employee")
private Set salaries;
}
@OneToMany 的可选参数
targetEntity=void.class
cascade={}
fetch=FetchType.LAZY
mappedBy=""
orphanRemoval=false
@ManyToOne 只有四个可选参数
targetEntity=void.class
cascade={}
fetch=FetchType.EAGER
optional=true
他们作用在一对一的关系中已经讲的很清楚了,他们只是默认取值不一样而已。
可以看到,在 @OneToMany 中有 mappedBy ,而 @ManyToOne 中没有 mappedBy ,如果你理解了我上面说的 mappedBy 的作用就应该很清楚了,在一对多的关系中,外键只可能存在于多的那一方,所以,在 @ManyToOne 这个注解中不可能存在 mappedBy 这个参数就好理解了。既然如此,那为什么 @OneToMany 中还需要 mappedBy 这个参数呢?默认 mappedBy 为多的一方不久好了吗?那是因为在一对多的关系中,通常我们是不会再需要一个中间表去关联的,只会在多的一方添加一个外键字段即可。这时候我们就需要手动指定 mappedBy 参数,如果不指定它为多的一方,那么默认JPA会帮我们自动生成一个中间表,这可能不是我们想看到的。当然,除此之外,我们也可以用 @JoinColumn 注解达到同样的效果。
特别说明一下在 @OneToMany 和 @ManyToOne 中,mappedBy 参数不能和@JoinTable 以及 @JoinColumn 同时出现,在 @OneToOne 中却是可以的。
三. @ManyToMany
employees 和 departments 是多对多的关系
在jpa中是这样定义的:
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "departments")
public class Department extends AbstractEntity{
@ManyToMany
@JoinTable(name="dept_emp",
joinColumns={@JoinColumn(name="dept_id")},
inverseJoinColumns={@JoinColumn(name="emp_id")})
private Set employees;
}
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@Entity
@Table(name = "employees")
public class Employee extends AbstractEntity {
@ManyToMany
@JoinTable(name="dept_emp",
joinColumns={@JoinColumn(name="emp_id")},
inverseJoinColumns={@JoinColumn(name="dept_id")})
private Set departments;
}
@ManyToMany 也是只四个可选参数
targetEntity=void.class
cascade={}
fetch=FetchType.LAZY
mappedBy=""
在这里我们出现了一个新的 @JoinTable 注解,这个注解定义了中间表。在 @ManyToMany 的关系中,必定会出现一个中间表,如果不用 @JoinTable 注解,默认JPA生成的中间表表名为 employees_departments 或 departments_employees。
至于这个其中谁在前,谁在后由 mappedBy 决定。如果不指定mappedBy,又没有 @JoinTable 注解来指定中间表名称,那么JPA自动给我们生成的表会是这样: