JPA 表关联注解的实验

我们都知道数据库表与表之间有如下四种关系

1:1(一对一,相应的注解叫@OneToOne)
1:n(一对多,相应的注解叫@OneToMany)
n:1(多对一,相应的注解叫@ManyToOne)
n:n(多对多,相应的注解叫@ManyToMany)


环境说明:

首先我们在讲解之前,我们先约定几个表关系

image.png

然后我在代码中定义了一个抽象的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,可选值有falsetrue

我们对以上几个参数做下详细的说明:

  • 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。


表employee的外键

表address的外键

如果此时我们不指定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。


address

employee

这里我们先来明确一个概念:父表和字表。父表和子表的概念我们也可以理解为主、副之分,比如此处员工和地址,一般我们认为,某地址是属于某个员工的,那么我们说员工表应该为父表(主表),地址表为子表(副表)。所以我们应该把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 要指定的名称
}
此时,在Address表中就不会出现emp_id这个外键字段了
  • 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<Salary> 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<Employee> 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<Department> departments;
}
自动生成的表结构

@ManyToMany 也是只四个可选参数

targetEntity=void.class
cascade={}
fetch=FetchType.LAZY
mappedBy=""

在这里我们出现了一个新的 @JoinTable 注解,这个注解定义了中间表。在 @ManyToMany 的关系中,必定会出现一个中间表,如果不用 @JoinTable 注解,默认JPA生成的中间表表名为 employees_departments 或 departments_employees。


image.png

至于这个其中谁在前,谁在后由 mappedBy 决定。如果不指定mappedBy,又没有 @JoinTable 注解来指定中间表名称,那么JPA自动给我们生成的表会是这样:


image.png