你不会还搞不清楚Spring Data JPA的关联关系注解如何使用吧?

你不会还搞不清楚Spring Data JPA的关联关系注解如何使用吧?

应该不止我一个人搞不清楚Spring Data JPA的关联关系注解吧?就是平时我们是用的@OneToOne,@OneToMany还有@ManyToOne还有相关的@JoinColumn注解。参考:Multiplicity in Entity Relationships

可能平时只是会用,但是具体怎么设置以及注解上每个属性的作用可能还不是了解的特别清楚,以及关联关系怎么去维护等等。所以这篇文章就是带你深入了解Spring Data JPA的关联关系注解的使用。文章主要关注在@OneToOne,@OneToMany还有@ManyToOne还有相关的@JoinColumn注解的使用。@ManyToMany平时工作基本用的比较少(反正在我工作中目前还没有怎么使用过),所以本篇博文就不会关注@ManyToMany的使用。

我们先来简单了解一下@OneToOne,@OneToMany还有@ManyToOne还有相关的@JoinColumn注解。

@JoinColumn 注解的作用:用来指定与所操作实体或实体集合相关联的数据库表中的列字段@JoinColumn主要配合@OneToOne@ManyToOne@OneToMany一起使用,单独使用没有意义。

由于 @OneToOne(一对一)、@OneToMany(一对多)、@ManyToOne(多对一)、@ManyToMany(多对多) 等注解只能确定实体之间几对几的关联关系,它们并不能指定与实体相对应的数据库表中的关联字段,因此,需要与@JoinColumn 注解来配合使用。

我们先来介绍一下@JoinColumn 注解,之后我们再来介绍一下@JoinColumn 注解配合其他@OneToOne(一对一)、@OneToMany(一对多)、@ManyToOne(多对一)的使用

@JoinColumn

package javax.persistence;

import java.lang.annotation.Repeatable;
import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static javax.persistence.ConstraintMode.PROVIDER_DEFAULT;

/**
 * Specifies a column for joining an entity association or element
 * collection.  If the <code>JoinColumn</code> annotation itself is
 * defaulted, a single join column is assumed and the default values
 * apply.
 *
 * <pre>
 *   Example:
 *
 *   &#064;ManyToOne
 *   &#064;JoinColumn(name="ADDR_ID")
 *   public Address getAddress() { return address; }
 *
 *
 *   Example: unidirectional one-to-many association using a foreign key mapping
 * 
 *   // In Customer class
 *   &#064;OneToMany
 *   &#064;JoinColumn(name="CUST_ID") // join column is in table for Order
 *   public Set&#060;Order&#062; getOrders() {return orders;}
 * </pre>
 *
 * @see ManyToOne
 * @see OneToMany
 * @see OneToOne
 * @see JoinTable
 * @see CollectionTable
 * @see ForeignKey
 *
 * @since 1.0
 */
@Repeatable(JoinColumns.class)
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface JoinColumn {

    /** 
     * (Optional) The name of the foreign key column.
     * The table in which it is found depends upon the
     * context. 
     * <ul>
     * <li>If the join is for a OneToOne or ManyToOne
     *  mapping using a foreign key mapping strategy, 
     * the foreign key column is in the table of the
     * source entity or embeddable. 
     * <li> If the join is for a unidirectional OneToMany mapping
     * using a foreign key mapping strategy, the foreign key is in the
     * table of the target entity.  
     * <li> If the join is for a ManyToMany mapping or for a OneToOne
     * or bidirectional ManyToOne/OneToMany mapping using a join
     * table, the foreign key is in a join table.  
     * <li> If the join is for an element collection, the foreign 
     * key is in a collection table.
     *</ul>
     *
     * <p> Default (only applies if a single join column is used):
     * The concatenation of the following: the name of the 
     * referencing relationship property or field of the referencing 
     * entity or embeddable class; "_"; the name of the referenced 
     * primary key column. 
     * If there is no such referencing relationship property or 
     * field in the entity, or if the join is for an element collection,
     * the join column name is formed as the 
     * concatenation of the following: the name of the entity; "_"; 
     * the name of the referenced primary key column.
     */
    String name() default "";

    /**
     * (Optional) The name of the column referenced by this foreign
     * key column. 
     * <ul>
     * <li> When used with entity relationship mappings other
     * than the cases described here, the referenced column is in the
     * table of the target entity. 
     * <li> When used with a unidirectional OneToMany foreign key
     * mapping, the referenced column is in the table of the source
     * entity.  
     * <li> When used inside a <code>JoinTable</code> annotation,
     * the referenced key column is in the entity table of the owning
     * entity, or inverse entity if the join is part of the inverse
     * join definition.  
     * <li> When used in a <code>CollectionTable</code> mapping, the
     * referenced column is in the table of the entity containing the
     * collection.
     * </ul>
     *
     * <p> Default (only applies if single join column is being 
     * used): The same name as the primary key column of the 
     * referenced table.
     */
    String referencedColumnName() default "";

    /**
     * (Optional) Whether the property is a unique key.  This is a
     * shortcut for the <code>UniqueConstraint</code> annotation at
     * the table level and is useful for when the unique key
     * constraint is only a single field. It is not necessary to
     * explicitly specify this for a join column that corresponds to a
     * primary key that is part of a foreign key.
     */
    boolean unique() default false;

    /** (Optional) Whether the foreign key column is nullable. */
    boolean nullable() default true;

    /**
     * (Optional) Whether the column is included in
     * SQL INSERT statements generated by the persistence
     * provider.
     */
    boolean insertable() default true;

    /**
     * (Optional) Whether the column is included in
     * SQL UPDATE statements generated by the persistence
     * provider.
     */
    boolean updatable() default true;

    /**
     * (Optional) The SQL fragment that is used when
     * generating the DDL for the column.
     * <p> Defaults to the generated SQL for the column.
     */
    String columnDefinition() default "";

    /**
     * (Optional) The name of the table that contains
     * the column. If a table is not specified, the column
     * is assumed to be in the primary table of the
     * applicable entity.
     *
     * <p> Default: 
     * <ul>
     * <li> If the join is for a OneToOne or ManyToOne mapping
     * using a foreign key mapping strategy, the name of the table of
     * the source entity or embeddable. 
     * <li> If the join is for a unidirectional OneToMany mapping 
     * using a foreign key mapping strategy, the name of the table of
     * the target entity. 
     * <li> If the join is for a ManyToMany mapping or
     * for a OneToOne or bidirectional ManyToOne/OneToMany mapping
     * using a join table, the name of the join table. 
     * <li> If the join is for an element collection, the name of the collection table.
     * </ul>
     */
    String table() default "";

    /**
     *  (Optional) Used to specify or control the generation of a
     *  foreign key constraint when table generation is in effect.  If
     *  this element is not specified, the persistence provider's
     *  default foreign key strategy will apply.
     *
     *  @since 2.1
     */
    ForeignKey foreignKey() default @ForeignKey(PROVIDER_DEFAULT);
}

我们来看看@JoinColumn注解中的每个属性的作用吧

  • name: 外键列的名称(数据库中的列名称)。外键在哪个表取决于使用@OneToOne或者@ManyToOne还是@OneToMany,具体的我们在后面跟其他注解一起配合讲解。

    • If the join is for a OneToOne or ManyToOne mapping using a foreign key mapping strategy, the foreign key column is in the table of the source entity or embeddable.
    • If the join is for a unidirectional OneToMany mapping using a foreign key mapping strategy, the foreign key is in the table of the target entity.
    • If the join is for a ManyToMany mapping or for a OneToOne or bidirectional ManyToOne/OneToMany mapping using a join table, the foreign key is in a join table.
    • If the join is for an element collection, the foreign key is in a collection table.

    如果我们不配置name属性(默认为空字符串),则会帮我们生成一个外键列的名称(一般推荐自己命名,不使用自动生成),生成的逻辑如下:

    Default (only applies if a single join column is used): The concatenation of the following: the name of the referencing relationship property or field of the referencing entity or embeddable class; "_"; the name of the referenced primary key column. If there is no such referencing relationship property or field in the entity, or if the join is for an element collection, the join column name is formed as the concatenation of the following: the name of the entity; "_"; the name of the referenced primary key column.

  • referencedColumnName: 此外键列引用的列的名称(就是这个外键是关联到哪个表的列,也是数据库中的列名),那如果不设置这个属性(默认为空字符串),则会使用被关联表的主键列名

    Default (only applies if single join column is being used): The same name as the primary key column of the referenced table.

  • unique: 外键列是否为唯一键,默认值为false

    (Optional) Whether the property is a unique key. This is a shortcut for the UniqueConstraint annotation at the table level and is useful for when the unique key constraint is only a single field. It is not necessary to explicitly specify this for a join column that corresponds to a primary key that is part of a foreign key

  • nullable: 外键列是否可以为空值。默认值为true

    (Optional) Whether the foreign key column is nullable.

  • insertable:是否跟随一起新增

    (Optional) Whether the column is included in SQL INSERT statements generated by the persistence provider.

  • updatable:是否跟随一起新增

    (Optional) Whether the column is included in SQL UPDATE statements generated by the persistence provider.

  • columnDefinition:指定SQL片段来创建外键列

    (Optional) The SQL fragment that is used when generating the DDL for the column.

  • table

    (Optional) The name of the table that contains the column. If a table is not specified, the column is assumed to be in the primary table of the applicable entity.
    Default:

    • If the join is for a OneToOne or ManyToOne mapping using a foreign key mapping strategy, the name of the table of the source entity or embeddable.
    • If the join is for a unidirectional OneToMany mapping using a foreign key mapping strategy, the name of the table of the target entity.
    • If the join is for a ManyToMany mapping or for a OneToOne or bidirectional ManyToOne/OneToMany mapping using a join table, the name of the join table.
    • If the join is for an element collection, the name of the collection table.
  • foreignKey : 用于表生成时指定外键约束

    (Optional) Used to specify or control the generation of a foreign key constraint when table generation is in effect. If this element is not specified, the persistence provider’s default foreign key strategy will apply.

一般我们是用@JoinColumn注解的时候其实大多数只会用到namereferencedColumnName两个属性,有时候还可能会用到foreignKey属性,因为我们自动创建表的时候不想使用到外键约束。

参考: https://docs.oracle.com/javaee/7/api/javax/persistence/JoinColumn.html

@OneToOne@JoinColumn

我们先来看看@OneToOne注解

/**
 * Specifies a single-valued association to another entity that has
 * one-to-one multiplicity. It is not normally necessary to specify
 * the associated target entity explicitly since it can usually be
 * inferred from the type of the object being referenced.  If the relationship is
 * bidirectional, the non-owning side must use the <code>mappedBy</code> element of
 * the <code>OneToOne</code> annotation to specify the relationship field or
 * property of the owning side.
 *
 * <p> The <code>OneToOne</code> annotation may be used within an
 * embeddable class to specify a relationship from the embeddable
 * class to an entity class. If the relationship is bidirectional and
 * the entity containing the embeddable class is on the owning side of
 * the relationship, the non-owning side must use the
 * <code>mappedBy</code> element of the <code>OneToOne</code>
 * annotation to specify the relationship field or property of the
 * embeddable class. The dot (".") notation syntax must be used in the
 * <code>mappedBy</code> element to indicate the relationship attribute within the
 * embedded attribute.  The value of each identifier used with the dot
 * notation is the name of the respective embedded field or property.
 * 
 * <pre>
 *    Example 1: One-to-one association that maps a foreign key column
 *
 *    // On Customer class:
 *
 *    &#064;OneToOne(optional=false)
 *    &#064;JoinColumn(
 *    	name="CUSTREC_ID", unique=true, nullable=false, updatable=false)
 *    public CustomerRecord getCustomerRecord() { return customerRecord; }
 *
 *    // On CustomerRecord class:
 *
 *    &#064;OneToOne(optional=false, mappedBy="customerRecord")
 *    public Customer getCustomer() { return customer; }
 *
 *
 *    Example 2: One-to-one association that assumes both the source and target share the same primary key values. 
 *
 *    // On Employee class:
 *
 *    &#064;Entity
 *    public class Employee {
 *    	&#064;Id Integer id;
 *    
 *    	&#064;OneToOne &#064;MapsId
 *    	EmployeeInfo info;
 *    	...
 *    }
 *
 *    // On EmployeeInfo class:
 *
 *    &#064;Entity
 *    public class EmployeeInfo {
 *    	&#064;Id Integer id;
 *    	...
 *    }
 *
 *
 *    Example 3: One-to-one association from an embeddable class to another entity.
 *
 *    &#064;Entity
 *    public class Employee {
 *       &#064;Id int id;
 *       &#064;Embedded LocationDetails location;
 *       ...
 *    }
 *
 *    &#064;Embeddable
 *    public class LocationDetails {
 *       int officeNumber;
 *       &#064;OneToOne ParkingSpot parkingSpot;
 *       ...
 *    }
 *
 *    &#064;Entity
 *    public class ParkingSpot {
 *       &#064;Id int id;
 *       String garage;
 *       &#064;OneToOne(mappedBy="location.parkingSpot") Employee assignedTo;
 *        ... 
 *    } 
 *
 * </pre>
 *
 * @since 1.0
 */
@Target({METHOD, FIELD}) 
@Retention(RUNTIME)

public @interface OneToOne {

    /** 
     * (Optional) The entity class that is the target of 
     * the association. 
     *
     * <p> Defaults to the type of the field or property 
     * that stores the association. 
     */
    Class targetEntity() default void.class;

    /**
     * (Optional) The operations that must be cascaded to 
     * the target of the association.
     *
     * <p> By default no operations are cascaded.
     */
    CascadeType[] cascade() default {};

    /** 
     * (Optional) Whether the association should be lazily 
     * loaded or must be eagerly fetched. The EAGER 
     * strategy is a requirement on the persistence provider runtime that 
     * the associated entity must be eagerly fetched. The LAZY 
     * strategy is a hint to the persistence provider runtime.
     */
    FetchType fetch() default EAGER;

    /** 
     * (Optional) Whether the association is optional. If set 
     * to false then a non-null relationship must always exist.
     */
    boolean optional() default true;

    /** (Optional) The field that owns the relationship. This 
      * element is only specified on the inverse (non-owning) 
      * side of the association.
     */
    String mappedBy() default "";


    /**
     * (Optional) Whether to apply the remove operation to entities that have
     * been removed from the relationship and to cascade the remove operation to
     * those entities.
     * @since 2.0
     */
    boolean orphanRemoval() default false;
}

我们来看看注解上面属性的作用吧

  • targetEntity:指定关联目标的实体类,一般不使用,因为JPA可以通过使用了@OneToOne的字段引用类型推断出来。

    (Optional) The entity class that is the target of the association.
    Defaults to the type of the field or property that stores the association.

  • cascade:与关联实体的级联方式,默认没有任何级联操作默认值为:{}

    (Optional) The operations that must be cascaded to the target of the association.
    By default no operations are cascaded.

  • fetch:关联实体的加载方式。是立即加载还是使用懒加载,默认为FetchType.EAGER立即加载

    (Optional) Whether the association should be lazily loaded or must be eagerly fetched. The EAGER strategy is a requirement on the persistence provider runtime that the associated entity must be eagerly fetched. The LAZY strategy is a hint to the persistence provider runtime.

  • optional:是否允许为空,默认为true

    (Optional) Whether the association is optional. If set to false then a non-null relationship must always exist.

  • mappedBy:关联关系被谁维护,默认为""

    (Optional) The field that owns the relationship. This element is only specified on the inverse (non-owning) side of the association.

  • orphanRemoval: 是否级联删除,默认值为false

    (Optional) Whether to apply the remove operation to entities that have been removed from the relationship and to cascade the remove operation to those entities.

一般我们是用@OneToOne最常使用的就是cascade,fetch(不过在一对一的情况下,一般都是用FetchType.EAGER立即加载),还有mappedBy,orphanRemoval

参考:https://docs.oracle.com/javaee/7/api/javax/persistence/OneToOne.html

现在我们来从代码来看看,创建了一个一对一的关系,employee和employee info是一对一的关系。

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employee")
public class Employee {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @OneToOne
  private EmployeeInfo employeeInfo;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employee_info")
public class EmployeeInfo {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String departmentName;
}

这个时候我们自动建表的语句如下:

Hibernate: 
    
    create table employee (
       id bigint not null auto_increment,
        name varchar(255),
        employee_info_id bigint,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table employee_info (
       id bigint not null auto_increment,
        department_name varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    alter table employee 
       add constraint FKmn4awjrpongc0bglj2co4ji1x 
       foreign key (employee_info_id) 
       references employee_info (id)

可以看出来,当我们在employee实体中加入@OneToOne关联employee info的时候,就会在employee表中加入一个column employee_info_id来关联employee_info 表。并且会生成外键约束。

前面说到了@JoinColumn不能单独使用,要跟其他关联注解一起使用。这个时候我们就可以通过@JoinColumn注解和@OneToOne来设置这些。前面说到了@JoinColumn注解的name属性是来设置外键列的名称的。在employee表中会有一个指向employee_info表主键的字段employee_info_id,所以主控方或者叫做owning side.(指能够主动改变关联关系的一方)一定是employee,因为只要改变employee表的employee_info_id就改变了employeeemployee_info之间的关联关系,所以@JoinColumn要写在员工实体类Employee上,自然而然地,EmployeeInfo就是被控方或者叫做non-owning side

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employee")
public class Employee {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @OneToOne
  @JoinColumn(name = "info_id") //加上 @JoinColumn指定外键列的字段名
  private EmployeeInfo employeeInfo;
}

从创建语句可以看出,生成的外键字段名称就从默认的employee_info_id(字段默认的命名规则:被控方类名_被控方主键,参考上面的@JoinColumn的name属性的default规则)变成了info_id

    create table employee (
       id bigint not null auto_increment,
        name varchar(255),
        info_id bigint,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table employee_info (
       id bigint not null auto_increment,
        department_name varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    alter table employee 
       add constraint FK8yw6mmam5rw2fphbh1g985guh 
       foreign key (info_id) 
       references employee_info (id)

还可以使用referencedColumnName属性来指定外键是关联到哪个表的列,不设置默认是关联表的主键。同时我们还可以使用foreignKey属性来控制外键约束,比如不设置外键约束,因为我们比较少用外键约束,一般从代码层面控制。

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employee")
public class Employee {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @OneToOne
  @JoinColumn(name = "info_id",referencedColumnName = "employee_info_id",foreignKey = @ForeignKey(NO_CONSTRAINT))
  private EmployeeInfo employeeInfo;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employee_info")
public class EmployeeInfo {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String departmentName;

  @Column(name = "employee_info_id")
  private Long employeeInfoId;

}

建表语句如下:

Hibernate: 
    
    create table employee (
       id bigint not null auto_increment,
        name varchar(255),
        info_id bigint,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table employee_info (
       id bigint not null auto_increment,
        department_name varchar(255),
        employee_info_id bigint,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    alter table employee_info 
       add constraint UK_f9aet7061wab7k5b1s3wr1t8n unique (employee_info_id)

如果你把foreignKey属性去掉,使用外键约束就可以看到建表语句会加上这么一句外键约束。说明现在employeeinfo_id字段关联的是employee_info表的employee_info_id字段。

alter table employee 
       add constraint FK8yw6mmam5rw2fphbh1g985guh 
       foreign key (info_id) 
       references employee_info (employee_info_id)

@OneToOne的级联操作

接下来来看看@OneToOne的级联保存,首先我们来看看实体类的内容如下:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employee")
public class Employee {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @OneToOne
  @JoinColumn(name = "info_id")
  private EmployeeInfo employeeInfo;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employee_info")
public class EmployeeInfo {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String departmentName;


}

然后我们看看级联保存的操作如下:

EmployeeInfo department = EmployeeInfo.builder()
        .departmentName("test department").build();
Employee employee = Employee.builder()
    .name("test employee")
    .employeeInfo(department).build();
employeeRepository.save(employee);

结果发现保存的时候报错如下:

org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : org.example.entity.Employee.employeeInfo -> org.example.entity.EmployeeInfo; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : org.example.entity.Employee.employeeInfo -> org.example.entity.EmployeeInfo

这个原因是因为级联保存没有开启。我们只需要加上@OneToOne(cascade = {CascadeType.PERSIST})

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employee")
public class Employee {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @OneToOne(cascade = {CascadeType.PERSIST})
  @JoinColumn(name = "info_id")
  private EmployeeInfo employeeInfo;
}

之后我们保存就能成功了,而且employeeemployee_info表中都保存了数据,并且外键info_id也指定好了

接下来试试级联更新,首先是插入了数据,然后我们把两个实体的数据都一并修改了。

Employee employee = employeeService.saveEmployee(); // 跟上面保存一样插入了两个表的数据
employee.setName("test employee 2");
employee.getEmployeeInfo().setDepartmentName("test department 2");
employeeRepository.save(employee);

结果保存完发现,只是更新了Employee的数据,但是EmployeeInfo的数据并没有改变,这个原因就是因为没有开启级联更新

  @OneToOne(cascade = {CascadeType.PERSIST,CascadeType.MERGE})
  @JoinColumn(name = "info_id")
  private EmployeeInfo employeeInfo;

当我们开启了CascadeType.MERGE就可以发现能够级联更新了。

接下来看看级联删除

Employee employee = employeeService.saveEmployee();
employeeRepository.deleteById(1L);

运行之后发现只有Employee被删除了,但是EmployeeInfo还保留着,如果你想在Employee删除的同事也想把EmployeeInfo删除,就需要开启级联删除CascadeType.REMOVE,开启之后你就能够发现能够正常级联删除了。

如果使用orphanRemoval = true属性也可以实现级联删除,但是它跟CascadeType.REMOVE还是有区别的。

建议可以动手试试,如果去掉cascade = {CascadeType.REMOVE},加上orphanRemoval = true之后,在执行如下代码的时候会发现也能够实现级联删除

Employee employee = employeeService.saveEmployee();
employeeRepository.deleteById(1L);

但是如果我们执行如下代码就会发现,当外键关系解绑之后,EmployeeInfo也会被删除

Employee employee = employeeService.saveEmployee();
employee.setEmployeeInfo(null);
employeeRepository.save(employee);

所以区别就是orphanRemoval = true是外键关系解绑之后就会删掉关联的数据,当然如果你删掉主表的数据,外键关系自然解绑了,所以也会删掉关联表的数据。而CascadeType.REMOVE是在删除主控数据之后就会删除掉关联表数据。

所以一般我们在设置级联操作的时候,一般是在主控方设置,也就是在owning side中设置cascade = CascadeType.ALL,而orphanRemoval = true根据业务逻辑使用,记住要慎用。

@OneToOne(cascade = CascadeType.ALL,orphanRemoval = true)
@JoinColumn(name = "info_id")
private EmployeeInfo employeeInfo;

参考:Cascade Operations and Relationships

Orphan Removal in Relationships

@OneToOne的双向关联

接下来我们来看看@OneToOne的双向关联,有的人看到双向关联总是很疑惑,什么才叫做双向关联呢?官方给出的定义:Bidirectional Relationships

Bidirectional Relationships
In a bidirectional relationship, each entity has a relationship field or property that refers to the other entity. Through the relationship field or property, an entity class’s code can access its related object. If an entity has a related field, the entity is said to “know” about its related object. For example, if Order knows what LineItem instances it has and if LineItem knows what Order it belongs to, they have a bidirectional relationship.

Bidirectional relationships must follow these rules.

  • The inverse side of a bidirectional relationship must refer to its owning side by using the mappedBy element of the @OneToOne, @OneToMany, or @ManyToMany annotation. The mappedBy element designates the property or field in the entity that is the owner of the relationship.

  • The many side of many-to-one bidirectional relationships must not define the mappedBy element. The many side is always the owning side of the relationship.

  • For one-to-one bidirectional relationships, the owning side corresponds to the side that contains the corresponding foreign key.

  • For many-to-many bidirectional relationships, either side may be the owning side.

下面展示一个错误的双向关联。

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employee")
public class Employee {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @OneToOne(cascade = {CascadeType.ALL},orphanRemoval = true)
  @JoinColumn(name = "info_id")
  private EmployeeInfo employeeInfo;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employee_info")
@ToString(exclude = "employee")
public class EmployeeInfo {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String departmentName;


  @OneToOne
  private Employee employee;

}

这种设置方式,虽然看起来名义上双向关联的,但是还是不符合官方的一些规则。

这种错误的双向关联关系,使得两个表中都会创建了外键字段指向了另一个表,所以使得保存的时候需要额外的互相指定

Employee employee = employeeService.saveEmployee();
//错误的双向关联关系,使得两个表中都创建了一个字段指向了另一个表,所以使得保存的时候需要额外的互相指定
employee.getEmployeeInfo().setEmployee(employee);
employeeRepository.save(employee);
// 额外的互相指定之后才能够正确的保存,并相互的关联查询
System.out.println(employeeInfoRepository.findAll().get(0).getEmployee());

我们来看官方要求的正确@OneToOne的双向关联设置,我们当然一切以官方要求为准了

  1. The inverse side of a bidirectional relationship must refer to its owning side by using the mappedBy element of the @OneToOne
  2. For one-to-one bidirectional relationships, the owning side corresponds to the side that contains the corresponding foreign key.

第一个条件说的就是non-owning sideEmployeeInfo),必须持有owning sideEmployee)的引用,并且需要设置@OneToOne注解,并带有 mappedBy的属性。而第二个条件说的就是对于一对一双向关联来说,owning side一方需要包含外键,也就是外键需要在Employee这一方。

所以一个正确的one-to-one的双向关联关系应该像下面这样设置

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employee")
public class Employee {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @OneToOne(cascade = {CascadeType.ALL},orphanRemoval = true)
  @JoinColumn(name = "info_id")
  private EmployeeInfo employeeInfo;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employee_info")
@ToString(exclude = "employee")
public class EmployeeInfo {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String departmentName;


  @OneToOne(mappedBy = "employeeInfo")
  private Employee employee;

}

当我们去保存数据的时候

EmployeeInfo department = EmployeeInfo.builder()
    .departmentName("test department").build();
Employee employee = Employee.builder()
    .name("test employee")
    .employeeInfo(department).build();
employeeRepository.save(employee);
System.out.println(employeeInfoRepository.findAll().get(0).getEmployee());

我们可以看到最终打印出来的SQL如下

  Hibernate: 
    insert 
    into
        employee_info
        (department_name) 
    values
        (?)
2022-08-02 09:34:39.165 TRACE 18288 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [test department]
Hibernate: 
    insert 
    into
        employee
        (info_id, name) 
    values
        (?, ?)
2022-08-02 09:34:39.182 TRACE 18288 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-08-02 09:34:39.183 TRACE 18288 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [test employee]

当然除了使用mappedBy,还有一种不推荐的写法也是等价的

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employee")
public class Employee {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @OneToOne(cascade = {CascadeType.ALL},orphanRemoval = true)
  @JoinColumn(name = "info_id")
  private EmployeeInfo employeeInfo;
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employee_info")
@ToString(exclude = "employee")
public class EmployeeInfo {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String departmentName;


  @OneToOne
  @JoinColumn(name = "id", referencedColumnName = "info_id") //等价于mappedBy
  private Employee employee;

}

因为当我们使用mappedBy的时候相当于放弃了关系维护,所以维护关系是owning-sideEmployee)来维护,也就是外键只是在Employee表中。所以只有Employee表中有外键指向EmployeeInfo表。而我们在EmployeeInfo设置 @JoinColumn(name = "id", referencedColumnName = "info_id")也是一样的道理,就是EmployeeInfo放弃设置外键,EmployeeInfo主键的值跟Employee外键的值相等。

@OneToOnemappedBy

我们再来深入研究一下这个@OneToOnemappedBy

  1. mappedBy在单向关系不需要设置该属性,双向关系必须设置,避免双方都建立外键字段,设置了mappedBy的一方不会创建外键字段
  2. mappedBy一定是定义在non-owning-side(被拥有方或者被控方),他指向owning-side(拥有方或者主控方),官方也是这么说的:This element is only specified on the inverse (non-owning) side of the association.
  3. mappedBy的值是指向另一方的实体里面属性的字段,而不是数据库字段
  4. mappedByJoinColumn/JoinTable总是处于互斥的一方,也就是mappedBy不能跟JoinColumn/JoinTable同时使用
  5. 关系的拥有方负责关系的维护,在拥有方建立外键。所以用到@JoinColumn
  6. 只有关系维护方才能操作两者的关系,被维护方即使设置了维护方属性进行存储也不会更新外键关联。

学习JPA就是要多动手,多动手才能更熟练使用。前面1-5这几个点我们前面都讲过了,下面主要来讲一下第6点。下面动手试试吧.

我们现在就是使用被维护方(EmployeeInfo)来进行保存,

Employee employee = Employee.builder()
    .name("test employee").build();

EmployeeInfo department = EmployeeInfo.builder()
    .departmentName("test department")
    .employee(employee).build();
//对了别忘记加上级联操作,不然会报错:org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : org.example.entity.EmployeeInfo.employee -> org.example.entity.Employee; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : org.example.entity.EmployeeInfo.employee -> org.example.entity.Employee
// @OneToOne(mappedBy = "employeeInfo",cascade = CascadeType.ALL)
employeeInfoRepository.save(department);

执行结果

Hibernate: 
    insert 
    into
        employee_info
        (department_name) 
    values
        (?)
2022-08-02 09:31:04.003 TRACE 21080 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [test department]
Hibernate: 
    insert 
    into
        employee
        (info_id, name) 
    values
        (?, ?)
2022-08-02 09:31:04.021 TRACE 21080 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [null]
2022-08-02 09:31:04.022 TRACE 21080 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [test employee]
2022-08-02 09:31:04.085  INFO 21080 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2022-08-02 09:31:04.087  INFO 21080 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2022-08-02 09:31:04.125  INFO 21080 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

保存之后可以发现,虽然EmployeeEmployeeInfo表中都有数据,但是外键info_id是没有值的。一样的如果不是用mappedBy而是使用@JoinColumn(name = "id", referencedColumnName = "info_id")也是一样的效果,外键info_id是不会更新的,也就是第6点说的只有关系维护方才能操作两者的关系,被维护方即使设置了维护方属性进行存储也不会更新外键关联。(多动手试试,才能学的更快)

所以只有关系维护方才能操作两者的关系,也就是说owning-side(维护方)需要设置non-owning-side(被维护方)的属性并进行保存才可以更新外键关联。

Employee employee = Employee.builder()
    .name("test employee").build();

EmployeeInfo department = EmployeeInfo.builder()
    .departmentName("test department")
    .employee(employee).build();
employee.setEmployeeInfo(department); // 需要维护方去设置被维护方的属性才能更新外键关联
//对了别忘记加上级联操作,不然会报错:org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : org.example.entity.EmployeeInfo.employee -> org.example.entity.Employee; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : org.example.entity.EmployeeInfo.employee -> org.example.entity.Employee
// @OneToOne(mappedBy = "employeeInfo",cascade = CascadeType.ALL)
employeeInfoRepository.save(department);

这个时候保存的执行结果如下

Hibernate: 
    insert 
    into
        employee_info
        (department_name) 
    values
        (?)
2022-08-02 09:41:34.999 TRACE 20908 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [test department]
Hibernate: 
    insert 
    into
        employee
        (info_id, name) 
    values
        (?, ?)
2022-08-02 09:41:35.023 TRACE 20908 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-08-02 09:41:35.023 TRACE 20908 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [test employee]

从执行结果看,这种保存方式才能够更新外键

@OneToOne双向关联最佳实践

所以到这里可以总结一下我个人推荐one-to-one双向关联最佳实践的设置是怎样的。

  1. 外键设置在owning-side(拥有方),如果需要自定义指定外键列的名称,则使用@JoinColumn来指定,否则JPA将采用默认的名称。所以一般@JoinColumn都是配置在owning-side(拥有方)或者可以理解为配置在mappedBy的另一方。
  2. mappedBy一定是定义在non-owning-side(被拥有方或者被控方),他指向owning-side(拥有方或者主控方)。mappedBy的值是指向另一方的实体里面属性的字段
  3. 为了避免双向关联嵌套死循环,我们应该在non-owning-side(被维护方)中设置@ToString@EqualsAndHashCode,exclude 掉owning-side(拥有方)的字段。
  4. owning-side(拥有方)来操作两者的关系。
  5. owning-side(拥有方)中需要设置级联操作@OneToOne(cascade = {CascadeType.ALL}),至于orphanRemoval = true根据需求使用
  6. 根据个人需要判断是否需要外键约束,我们这边一般都不使用外键约束。

下面就是one-to-one双向关联最佳实践的设置代码

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employee")
public class Employee {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @OneToOne(cascade = {CascadeType.ALL}) //设置级联操作
  @JoinColumn(name = "info_id",foreignKey = @ForeignKey(NO_CONSTRAINT)) //指定外键列名称,取消外键约束
  private EmployeeInfo employeeInfo;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employee_info")
@ToString(exclude = "employee")
@EqualsAndHashCode(exclude = "employee")
public class EmployeeInfo {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String departmentName;


  @OneToOne(mappedBy = "employeeInfo") //设置mappedBy放弃关系维护,不设置外键,变成non-owning-side(被维护方)
  private Employee employee;

}

然后级联的更新保存操作都由在owning-side(拥有方)发起,也就是owning-side(拥有方)需要设置non-owning-side(被维护方)的属性

@OneToMany单向

/**
 * Specifies a many-valued association with one-to-many multiplicity.
 * 
 * <p> If the collection is defined using generics to specify the 
 * element type, the associated target entity type need not be 
 * specified; otherwise the target entity class must be specified.
 * If the relationship is bidirectional, the
 * <code> mappedBy</code> element must be used to specify the relationship field or
 * property of the entity that is the owner of the relationship.
 *
 * <p> The <code>OneToMany</code> annotation may be used within an embeddable class
 * contained within an entity class to specify a relationship to a
 * collection of entities. If the relationship is bidirectional, the
 * <code> mappedBy</code> element must be used to specify the relationship field or
 * property of the entity that is the owner of the relationship.
 *
 * When the collection is a <code>java.util.Map</code>, the <code>cascade</code> 
 * element and the <code>orphanRemoval</code> element apply to the map value.
 *
 * <pre>
 *
 *    Example 1: One-to-Many association using generics
 *
 *    // In Customer class:
 *
 *    &#064;OneToMany(cascade=ALL, mappedBy="customer")
 *    public Set&#060;Order&#062; getOrders() { return orders; }
 *
 *    In Order class:
 *
 *    &#064;ManyToOne
 *    &#064;JoinColumn(name="CUST_ID", nullable=false)
 *    public Customer getCustomer() { return customer; }
 *
 *
 *    Example 2: One-to-Many association without using generics
 *
 *    // In Customer class:
 *
 *    &#064;OneToMany(targetEntity=com.acme.Order.class, cascade=ALL,
 *                mappedBy="customer")
 *    public Set getOrders() { return orders; }
 *
 *    // In Order class:
 *
 *    &#064;ManyToOne
 *    &#064;JoinColumn(name="CUST_ID", nullable=false)
 *    public Customer getCustomer() { return customer; }
 *
 *
 *    Example 3: Unidirectional One-to-Many association using a foreign key mapping
 *
 *    // In Customer class:
 *
 *    &#064;OneToMany(orphanRemoval=true)
 *    &#064;JoinColumn(name="CUST_ID") // join column is in table for Order
 *    public Set&#060;Order&#062; getOrders() {return orders;}
 *    
 * </pre>
 *
 * @since 1.0
 */
@Target({METHOD, FIELD}) 
@Retention(RUNTIME)

public @interface OneToMany {

    /**
     * (Optional) The entity class that is the target
     * of the association. Optional only if the collection
     * property is defined using Java generics.
     * Must be specified otherwise.
     *
     * <p> Defaults to the parameterized type of
     * the collection when defined using generics.
     */
    Class targetEntity() default void.class;

    /** 
     * (Optional) The operations that must be cascaded to 
     * the target of the association.
     * <p> Defaults to no operations being cascaded.
     *
     * <p> When the target collection is a {@link java.util.Map
     * java.util.Map}, the <code>cascade</code> element applies to the
     * map value.
     */
    CascadeType[] cascade() default {};

    /** (Optional) Whether the association should be lazily loaded or
     * must be eagerly fetched. The EAGER strategy is a requirement on
     * the persistence provider runtime that the associated entities
     * must be eagerly fetched.  The LAZY strategy is a hint to the
     * persistence provider runtime.
     */
    FetchType fetch() default LAZY;

    /** 
     * The field that owns the relationship. Required unless 
     * the relationship is unidirectional.
     */
    String mappedBy() default "";

    /**
     * (Optional) Whether to apply the remove operation to entities that have
     * been removed from the relationship and to cascade the remove operation to
     * those entities.
     * @since 2.0
     */
    boolean orphanRemoval() default false;
}

一样的我们先来看看@OneToMany注解上的每一个属性的作用

  • targetEntity: 指定目标关联的实体类型,一般都不需要指定

    (Optional) The entity class that is the target of the association. Optional only if the collection property is defined using Java generics. Must be specified otherwise.
    Defaults to the parameterized type of the collection when defined using generics.

  • cascade:设置级联操作方式,默认没有任何级联操作,默认值为:{}

    (Optional) The operations that must be cascaded to the target of the association.
    Defaults to no operations being cascaded.

    When the target collection is a java.util.Map, the cascade element applies to the map value.

  • fetch: 关联实体的加载方式。是立即加载还是使用懒加载,默认为FetchType.EAGER立即加载

    (Optional) Whether the association should be lazily loaded or must be eagerly fetched. The EAGER strategy is a requirement on the persistence provider runtime that the associated entities must be eagerly fetched. The LAZY strategy is a hint to the persistence provider runtime.

  • mappedBy: 关联关系被谁维护,如果是双向关联则是必须的,默认值为""

    The field that owns the relationship. Required unless the relationship is unidirectional.

  • orphanRemoval: 是否级联删除,默认值为false

    (Optional) Whether to apply the remove operation to entities that have been removed from the relationship and to cascade the remove operation to those entities.

参考:https://docs.oracle.com/javaee/7/api/javax/persistence/OneToMany.html

@OneToMany@JoinColumn

下面我会以User为一方,ContactInfo为多方。每个User有多个ContactInfo为多方来做例子

下面这个例子就是一对多单向关联。

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "user")
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @Builder.Default
  @OneToMany
  private List<ContactInfo> contactInfo = new ArrayList<>();

}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "contact_info")
public class ContactInfo {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String phoneNumber;

  private String address;
}

运行之后发现建表语句如下:

Hibernate: 
    
    create table contact_info (
       id bigint not null auto_increment,
        address varchar(255),
        phone_number varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table user (
       id bigint not null auto_increment,
        name varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table user_contact_info (
       user_id bigint not null,
        contact_info_id bigint not null
    ) engine=InnoDB
Hibernate: 
    
    alter table user_contact_info 
       add constraint UK_181ov8vty4ti5tuel4ui9o190 unique (contact_info_id)
Hibernate: 
    
    alter table user_contact_info 
       add constraint FKamckxw8lpdddfpq7wcg9odsnc 
       foreign key (contact_info_id) 
       references contact_info (id)
Hibernate: 
    
    alter table user_contact_info 
       add constraint FKrx8f6go6ut4syfuogfcs2tgv5 
       foreign key (user_id) 
       references user (id)

可以看出来默认使用@OneToMany的话,在建表的时候会给我们创建一个中间表来关联一对多。默认创建的中间表就是用下划线来拼接两个表名,比如user_contact_info。通常并不推荐Hibernate自动去自动生成中间表,而是使用@JoinTable注解来指定中间表,或者不使用中间表,我个人推荐是不要使用中间表,毕竟多了一个新的表。

所以我们需要使用到@JoinColumn来避免创建中间表.

这个时候我们需要在一的那一方加上@JoinColumn的注解

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "user")
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @Builder.Default
  @OneToMany
  @JoinColumn(name="contact_info_id") //在一对多单向关系中,多的一方(没有注解,一的一方有注解,如果一的一方不加@JoinColumn指定外键字段的话,Hibernate会自动生成一张中间表来进行绑定。

  private List<ContactInfo> contactInfo = new ArrayList<>();

}
Hibernate: 
    
    create table contact_info (
       id bigint not null auto_increment,
        address varchar(255),
        phone_number varchar(255),
        contact_info_id bigint,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table user (
       id bigint not null auto_increment,
        name varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    alter table contact_info 
       add constraint FK6x7vketg3c3yg2f48p1wfx2dl 
       foreign key (contact_info_id) 
       references user (id)

可以发现,在一对多单向关系中,如果在一的一方加上@JoinColumn,则会在多的一方的表中加入一个外键关联一的一方,外键列的名称就是@JoinColumn设置的属性name的值。这个在官网对于@JoinColumn的使用描述也可以印证这一点。

If the join is for a unidirectional OneToMany mapping using a foreign key mapping strategy, the foreign key is in the table of the target entity.

在使用@OneToMany单向关联的时候,外键是在target entity的一方,也就是设置了@OneToMany设置target属性的一方,其实也就是多的一方会拥有外键。

@OneToMany单向级联操作

 List<ContactInfo> contactInfos = List.of(ContactInfo.builder()
        .address("test address").phoneNumber("1234").build());
    User user = User.builder()
        .name("kevin").contactInfo(contactInfos).build();
    userRepository.save(user);

如果不加上级联操作CascadeType.PERSIST,上面的代码会报如下的错误

org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: org.example.entity.ContactInfo; nested exception is java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: org.example.entity.ContactInfo

加上级联操作CascadeType.PERSIST之后

@Builder.Default
@OneToMany(cascade = {CascadeType.PERSIST})
@JoinColumn(name="contact_info_id")
private List<ContactInfo> contactInfo = new ArrayList<>();

运行结果如下,可以看到两个实体都保存下来了,并且给contact_info表的外键contact_info_id也设置了值

Hibernate: 
    insert 
    into
        user
        (name) 
    values
        (?)
2022-07-22 10:10:07.382 TRACE 14020 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [kevin]
Hibernate: 
    insert 
    into
        contact_info
        (address, phone_number) 
    values
        (?, ?)
2022-07-22 10:10:07.488 TRACE 14020 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [test address]
2022-07-22 10:10:07.488 TRACE 14020 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [1234]
Hibernate: 
    update
        contact_info 
    set
        contact_info_id=? 
    where
        id=?
2022-07-22 10:10:07.533 TRACE 14020 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-07-22 10:10:07.535 TRACE 14020 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
2022-07-22 10:10:07.883  INFO 14020 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2022-07-22 10:10:07.885  INFO 14020 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2022-07-22 10:10:07.950  INFO 14020 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

一样的,对于级联一起update的情况,也是需要加上CascadeType.MERGE, 不然下面的代码只会update User表的数据.

List<ContactInfo> contactInfos = List.of(ContactInfo.builder()
        .address("test address").phoneNumber("1234").build());
    User user = User.builder()
        .name("kevin").contactInfo(contactInfos).build();
    return userRepository.save(user);

下面是级联删除的情况,如果不设置级联删除操作,在删除User的时候,只会删除掉User表的数据,然后ContactInfo表中的外键设置为空,并不会级联删除掉ContactInfo表。所以我们需要加上CascadeType.REMOVE

User user = userService.save();
userRepository.delete(user);

级联删除还可以使用orphanRemoval = true,不过要注意区别

//去掉设置CascadeType.REMOVE,加入orphanRemoval = true
//效果跟设置CascadeType.REMOVE一样
User user = userService.save();
userRepository.delete(user);

但是另一种情况,不删除掉User,而是解除跟ContactInfo表的关系

//不使用orphanRemoval = true
User user = userService.save();
user.getContactInfo().clear();
userRepository.save(user);

假如我们不设置orphanRemoval = true,上面的代码运行结果就是,会把外键设置为空,但不会删除掉数据。

Hibernate: 
    update
        contact_info 
    set
        contact_info_id=? 
    where
        id=?
2022-07-22 11:16:24.332 TRACE 8016 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-07-22 11:16:24.332 TRACE 8016 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]

或者你不是使用clear(),而是直接设置为null

//不使用orphanRemoval = true
User user = userService.save();
user.setContactInfo(null);
userRepository.save(user);

结果也是一样的。但是强烈建议不要这么做!!,建议还是使用clear()来做,因为如果你使用了orphanRemoval = true,设置为null这种方式会报错

Hibernate: 
    update
        contact_info 
    set
        contact_info_id=? 
    where
        id=?
2022-07-22 11:18:01.782 TRACE 24588 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-07-22 11:18:01.783 TRACE 24588 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]

我们可以动手试试,加入orphanRemoval = true

//去掉设置CascadeType.REMOVE,加入orphanRemoval = true
User user = userService.save();
user.setContactInfo(null);
userRepository.save(user);

上面的代码就会报如下错误:

org.springframework.orm.jpa.JpaSystemException: A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance: org.example.entity.User.contactInfo; nested exception is org.hibernate.HibernateException: A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance: org.example.entity.User.contactInfo

所以应该使用clear()

//去掉设置CascadeType.REMOVE,加入orphanRemoval = true
User user = userService.save();
user.getContactInfo().clear();
userRepository.save(user);

这个时候你会发现,除了把外键设置为空之后还会把这个记录删除掉。

Hibernate: 
    update
        contact_info 
    set
        contact_info_id=null 
    where
        contact_info_id=?
2022-07-22 11:22:55.677 TRACE 23784 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
Hibernate: 
    delete 
    from
        contact_info 
    where
        id=?
2022-07-22 11:22:55.682 TRACE 23784 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]

@ManyToOne单向

@ManyToOne注解中的属性跟@OneToMany的属性还是有一丢丢区别的,注意区分。

/**
 * Specifies a single-valued association to another entity class that
 * has many-to-one multiplicity. It is not normally necessary to
 * specify the target entity explicitly since it can usually be
 * inferred from the type of the object being referenced.  If the
 * relationship is bidirectional, the non-owning
 * <code>OneToMany</code> entity side must used the
 * <code>mappedBy</code> element to specify the relationship field or
 * property of the entity that is the owner of the relationship.
 *
 * <p> The <code>ManyToOne</code> annotation may be used within an
 * embeddable class to specify a relationship from the embeddable
 * class to an entity class. If the relationship is bidirectional, the
 * non-owning <code>OneToMany</code> entity side must use the <code>mappedBy</code>
 * element of the <code>OneToMany</code> annotation to specify the
 * relationship field or property of the embeddable field or property
 * on the owning side of the relationship. The dot (".") notation
 * syntax must be used in the <code>mappedBy</code> element to indicate the
 * relationship attribute within the embedded attribute.  The value of
 * each identifier used with the dot notation is the name of the
 * respective embedded field or property.
 * <pre>
 *
 *     Example 1:
 *
 *     &#064;ManyToOne(optional=false) 
 *     &#064;JoinColumn(name="CUST_ID", nullable=false, updatable=false)
 *     public Customer getCustomer() { return customer; }
 *
 *
 *     Example 2:
 * 
 *     &#064;Entity
 *        public class Employee {
 *        &#064;Id int id;
 *        &#064;Embedded JobInfo jobInfo;
 *        ...
 *     }
 *
 *     &#064;Embeddable
 *        public class JobInfo {
 *        String jobDescription; 
 *        &#064;ManyToOne ProgramManager pm; // Bidirectional
 *     }
 *
 *     &#064;Entity
 *        public class ProgramManager {
 *        &#064;Id int id;
 *        &#064;OneToMany(mappedBy="jobInfo.pm")
 *        Collection&#060;Employee&#062; manages;
 *     }
 *
 * </pre>
 *
 * @since 1.0
 */
@Target({METHOD, FIELD}) 
@Retention(RUNTIME)

public @interface ManyToOne {

    /** 
     * (Optional) The entity class that is the target of 
     * the association. 
     *
     * <p> Defaults to the type of the field or property 
     * that stores the association. 
     */
    Class targetEntity() default void.class;

    /**
     * (Optional) The operations that must be cascaded to 
     * the target of the association.
     *
     * <p> By default no operations are cascaded.
     */
    CascadeType[] cascade() default {};

    /** 
     * (Optional) Whether the association should be lazily 
     * loaded or must be eagerly fetched. The EAGER
     * strategy is a requirement on the persistence provider runtime that 
     * the associated entity must be eagerly fetched. The LAZY 
     * strategy is a hint to the persistence provider runtime.
     */
    FetchType fetch() default EAGER;

    /** 
     * (Optional) Whether the association is optional. If set 
     * to false then a non-null relationship must always exist.
     */
    boolean optional() default true;
}

接下来我们也是看一看@ManyToOne的属性。

  • targetEntity: 关联的目标实体,一般不需要配,JPA会根据类型推断出来

    (Optional) The entity class that is the target of the association.
    Defaults to the type of the field or property that stores the association.

  • cascade : 级联操作,默认没有任何级联操作

    (Optional) The operations that must be cascaded to the target of the association.
    By default no operations are cascaded.

  • fetch: 关联实体的加载方式。是立即加载还是使用懒加载,默认为FetchType.EAGER立即加载

    (Optional) Whether the association should be lazily loaded or must be eagerly fetched. The EAGER strategy is a requirement on the persistence provider runtime that the associated entity must be eagerly fetched. The LAZY strategy is a hint to the persistence provider runtime.

  • optional: 关联是否可选,默认为true

    (Optional) Whether the association is optional. If set to false then a non-null relationship must always exist.

参考:https://docs.oracle.com/javaee/7/api/javax/persistence/ManyToOne.html

接下来我们写一个多对一单向关联,还是用上面的例子

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "user")
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;


}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "contact_info")
public class ContactInfo {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String phoneNumber;

  private String address;

  @ManyToOne
  private User user;
}

运行之后发现建表语句如下:

Hibernate: 
    
    create table contact_info (
       id bigint not null auto_increment,
        address varchar(255),
        phone_number varchar(255),
        user_id bigint,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table user (
       id bigint not null auto_increment,
        name varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    alter table contact_info 
       add constraint FK1v8a72hlm21xufkxjnh76jcn7 
       foreign key (user_id) 
       references user (id)

可以看到当多方使用了@ManyToOne,而一的一方没有使用任何注解,默认是不会创建中间表的,而是会在多的一方创建一个外键,关联到一的那一方。外键的名称是JPA默认设置。

一样的,我们也是能用@JoinColumn来指定外键列名称,并且外键就在当前的类的表上,官网中也是这么介绍的。如果使用在了@ManyToOne上,那么外键就在source entity一方,也就是在当前类实体这一方。

If the join is for a OneToOne or ManyToOne mapping using a foreign key mapping strategy, the foreign key column is in the table of the source entity or embeddable.

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "contact_info")
public class ContactInfo {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String phoneNumber;

  private String address;

  @ManyToOne
  @JoinColumn(name = "uid") //指定外键列名称
  private User user;
}

执行的建表语句如下, 可以看到外键列的名称使用的是我们刚刚指定的了。

Hibernate: 
    
    create table contact_info (
       id bigint not null auto_increment,
        address varchar(255),
        phone_number varchar(255),
        uid bigint,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table user (
       id bigint not null auto_increment,
        name varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    alter table contact_info 
       add constraint FKr76qkq93fb87i0tkquhx1a8fj 
       foreign key (uid) 
       references user (id)

@ManyToOne单向级联操作

User user = User.builder().name("kevin").build();
List<ContactInfo> contactInfos = new ArrayList<>();
contactInfos.add(ContactInfo.builder().address("test address").user(user).build());
contactInfos.add(ContactInfo.builder().address("test address2").user(user).build());
contactInfoRepository.saveAll(contactInfos);

如果不使用级联操作,上面的代码会报如下的错误:

org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : org.example.entity.ContactInfo.user -> org.example.entity.User; nested exception is java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : org.example.entity.ContactInfo.user -> org.example.entity.User

所以我们需要加上@ManyToOne(cascade = {CascadeType.PERSIST})

运行后的结果如下:

Hibernate: 
    insert 
    into
        user
        (name) 
    values
        (?)
2022-07-22 11:39:57.899 TRACE 23276 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [kevin]
Hibernate: 
    insert 
    into
        contact_info
        (address, phone_number, uid) 
    values
        (?, ?, ?)
2022-07-22 11:39:57.919 TRACE 23276 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [test address]
2022-07-22 11:39:57.919 TRACE 23276 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [null]
2022-07-22 11:39:57.920 TRACE 23276 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]
Hibernate: 
    insert 
    into
        contact_info
        (address, phone_number, uid) 
    values
        (?, ?, ?)
2022-07-22 11:39:57.924 TRACE 23276 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [test address2]
2022-07-22 11:39:57.925 TRACE 23276 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [null]
2022-07-22 11:39:57.925 TRACE 23276 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]
2022-07-22 11:39:58.008  INFO 23276 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2022-07-22 11:39:58.010  INFO 23276 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2022-07-22 11:39:58.109  INFO 23276 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

可以看到插入了一条user的数据,和两条contact_info的数据。但值得注意的是,你可以发现外键的设置是直接通过insert数据同时设置的。跟一对多单向级联保存的时候是不一样的,读者可以往上翻一翻运行的结果对比下,一对多单向级联保存是insert一个外键为空的数据,然后在执行一个update的操作把外键设置为正确的值。注意一下区别。

接下来是级联更新,这次我们一开始只保存一条contact_info和一条user数据,然后再对他们进行更新操作。

List<ContactInfo> contactInfos = contactInfoService.save();
contactInfos.get(0).setAddress("test address2");
contactInfos.get(0).getUser().setName("test kevin2");
contactInfoRepository.saveAll(contactInfos);

一样的,只有加上级联更新操作CascadeType.MERGE,才能在更新ContactInfo的同时更新User.

最后就是级联删除了。

List<ContactInfo> contactInfos = contactInfoService.save();
contactInfoRepository.deleteAll(contactInfos);

如果不设置级联删除,上面的代码运行结果如下:

Hibernate: 
    delete 
    from
        contact_info 
    where
        id=?
2022-07-22 13:24:49.287 TRACE 25324 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-07-22 13:24:49.335  INFO 25324 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2022-07-22 13:24:49.337  INFO 25324 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2022-07-22 13:24:49.379  INFO 25324 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

设置了级联删除CascadeType.REMOVE之后,运行结果如下:

Hibernate: 
    delete 
    from
        contact_info 
    where
        id=?
2022-07-22 13:27:07.730 TRACE 10088 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
Hibernate: 
    delete 
    from
        user 
    where
        id=?
2022-07-22 13:27:07.738 TRACE 10088 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-07-22 13:27:07.829  INFO 10088 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2022-07-22 13:27:07.831  INFO 10088 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2022-07-22 13:27:07.893  INFO 10088 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

@OneToMany@ManyToOne双向关联

接下来就是重头戏了,也是我们平时使用比较多的,就是@OneToMany@ManyToOne双向关联了。

怎么设置双向关联,肯定是要参考官方给的建议了,参考:Bidirectional Relationships,虽然在@OneToOne双向关联中已经提到了,但是为了方便读者阅读,我这里再贴一份。

In a bidirectional relationship, each entity has a relationship field or property that refers to the other entity. Through the relationship field or property, an entity class’s code can access its related object. If an entity has a related field, the entity is said to “know” about its related object. For example, if Order knows what LineItem instances it has and if LineItem knows what Order it belongs to, they have a bidirectional relationship.

Bidirectional relationships must follow these rules.

  • The inverse side of a bidirectional relationship must refer to its owning side by using the mappedBy element of the @OneToOne, @OneToMany, or @ManyToMany annotation. The mappedBy element designates the property or field in the entity that is the owner of the relationship.

  • The many side of many-to-one bidirectional relationships must not define the mappedBy element. The many side is always the owning side of the relationship.

  • For one-to-one bidirectional relationships, the owning side corresponds to the side that contains the corresponding foreign key.

  • For many-to-many bidirectional relationships, either side may be the owning side.

首先从规则看,双向的前提条件就是双方都持有对方的引用啦,然后并且使用@OneToMany@ManyToOne。第二点,对于many-to-one 双向关联关系来说,多的一方不能设置mappedBy属性,这一点我们在对比@OneToMany@ManyToOne属性之间的区别就可以看到,@ManyToOne是没有mappedBy属性的,所以已经限制了我们在多一方去配置mappedBy属性了。

我们先不设置mappedBy属性,也不设置@JoinColumn来看看是什么效果。

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "contact_info")
public class ContactInfo {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String phoneNumber;

  private String address;

  @ManyToOne
  private User user;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "user")
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @Builder.Default
  @OneToMany
  private List<ContactInfo> contactInfos = new ArrayList<>();

}

这个时候会发现建表语句是会创建中间表的

Hibernate: 
    
    create table contact_info (
       id bigint not null auto_increment,
        address varchar(255),
        phone_number varchar(255),
        user_id bigint,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table user (
       id bigint not null auto_increment,
        name varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table user_contact_infos (
       user_id bigint not null,
        contact_infos_id bigint not null
    ) engine=InnoDB
Hibernate: 
    
    alter table user_contact_infos 
       add constraint UK_k670ptwfqidip71rqucx7ehdg unique (contact_infos_id)
Hibernate: 
    
    alter table contact_info 
       add constraint FK1v8a72hlm21xufkxjnh76jcn7 
       foreign key (user_id) 
       references user (id)
Hibernate: 
    
    alter table user_contact_infos 
       add constraint FKp6fw6v5usm6lt3dos7s20oa9r 
       foreign key (contact_infos_id) 
       references contact_info (id)
Hibernate: 
    
    alter table user_contact_infos 
       add constraint FKfnktbao5v8kufdonpv0biruvv 
       foreign key (user_id) 
       references user (id)

但是如果我们加了mappedBy属性,就会发现建表语句不会创建中间表,而是会在contact_info中创建外键。

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "user")
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @Builder.Default
  @OneToMany(mappedBy = "user")
  private List<ContactInfo> contactInfos = new ArrayList<>();

}

建表语句如下:

Hibernate: 
    
    create table contact_info (
       id bigint not null auto_increment,
        address varchar(255),
        phone_number varchar(255),
        user_id bigint,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table user (
       id bigint not null auto_increment,
        name varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    alter table contact_info 
       add constraint FK1v8a72hlm21xufkxjnh76jcn7 
       foreign key (user_id) 
       references user (id)

记得我们之前在一对多单向的时候,默认也是会创建中间表,然后我们会使用@JoinColumn使得在多的一方创建外键。那在双向关联中是不是也是这样呢,我们可以动手试试,去掉mappedBy,加入@JoinColumn

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "user")
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @Builder.Default
  @OneToMany
  @JoinColumn(name = "uid") //不会创建中间表,而是在多的一方设置外键,并指定外键列名称
  private List<ContactInfo> contactInfos = new ArrayList<>();

}

建表语句如下:

Hibernate: 
    
    create table contact_info (
       id bigint not null auto_increment,
        address varchar(255),
        phone_number varchar(255),
        user_id bigint,
        uid bigint,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table user (
       id bigint not null auto_increment,
        name varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    alter table contact_info 
       add constraint FK1v8a72hlm21xufkxjnh76jcn7 
       foreign key (user_id) 
       references user (id)
Hibernate: 
    
    alter table contact_info 
       add constraint FKr76qkq93fb87i0tkquhx1a8fj 
       foreign key (uid) 
       references user (id)

可以发现这个时候也是不会创建中间表的,而是在多的一方创建外键,看起来在一对多和多对一双向关联中,@JoinColumnmappedBy在建表语句的生成具有相同的作用。但实际上他们功能并不是一样的。如果你仔细观察你就会发现,在双向关联中,在一的一方加入了@JoinColumn之后,虽然在多的一方创建了外键,但是你可以发现JPA默认的外键也会创建。也就是有两个外键user_id还有uid。如果是一对多单向关联得到话,在一的一方加入了@JoinColumn之后不会创建中间表,并且只会在多的一方创建一个外键,并且外键列的名称是我们自己手动指定的(可以翻看之前的例子,不过@JoinColumn的name属性最好设置成不是默认的名称,设置一个特殊一点的名称来观察),这里我重新改了一下一对多单向时候设置的@JoinColumn的name的值为info_id,可以看到建表语句如下:

    create table contact_info (
       id bigint not null auto_increment,
        address varchar(255),
        phone_number varchar(255),
        info_id bigint,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table user (
       id bigint not null auto_increment,
        name varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    alter table contact_info 
       add constraint FKawjq96arapgrcy3qpnt45b3ym 
       foreign key (info_id) 
       references user (id)

所以注意一下单向和双向设置@JoinColumn的区别。

那如果我们在不设置mappedBy,只是设置@JoinColumn如何做到不创建中间表,并且制定的外键列名称是对的,并且只创建一个外键呢?

那就是在多的一方设置@JoinColumn,在多的一方也设置@JoinColmn

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "contact_info")
public class ContactInfo {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String phoneNumber;

  private String address;

  @ManyToOne
  @JoinColumn(name = "uid")
  private User user;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "user")
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @Builder.Default
  @OneToMany
  @JoinColumn(name = "uid") //不会创建中间表,而是在多的一方设置外键,并指定外键列名称
  private List<ContactInfo> contactInfos = new ArrayList<>();

}

这样创建出来的表语句就是对的了

Hibernate: 
    
    create table contact_info (
       id bigint not null auto_increment,
        address varchar(255),
        phone_number varchar(255),
        uid bigint,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table user (
       id bigint not null auto_increment,
        name varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    alter table contact_info 
       add constraint FKr76qkq93fb87i0tkquhx1a8fj 
       foreign key (uid) 
       references user (id)

题外话,可能有人想问双向关联如果只在多的一方设置@JoinColumn,不在一的一方设置呢?会起效果吗?懂事的同学已经去尝试了,这里我可以告诉你们答案,答案就是只在多的一方设置@JoinColumn是没有效果的,是会默认创建中间表的。

还有的同学可能想问,如果我同时在@OneToMany上设置了在mappedBy@JoinColumn会怎样呢?这一点我之前在讲@OneToOne的时候mappedBy属性的时候就讲到了,mappedBy@JoinColumn是互斥的。可能有的同学不信,那你们可以去试试,我这里已经帮你们试出来了,答案就是JPA会报错,因为这种设置就是不允许的。报错如下:

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is org.hibernate.AnnotationException: Associations marked as mappedBy must not define database mappings like @JoinTable or @JoinColumn: org.example.entity.User.contactInfos

那又衍生出来一个问题,如果我用了mappedBy又想要指定外键的列名称咋做呢?答案就是在多的一方设置@JoinColumn

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "user")
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @Builder.Default
  @OneToMany(mappedBy = "user")
  private List<ContactInfo> contactInfos = new ArrayList<>();

}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "contact_info")
public class ContactInfo {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String phoneNumber;

  private String address;

  @ManyToOne
  @JoinColumn(name = "uid") // 一的一方使用了mappedBy,多的一方使用 @JoinColumn
  private User user;
}

建表语句如下:

Hibernate: 
    
    create table contact_info (
       id bigint not null auto_increment,
        address varchar(255),
        phone_number varchar(255),
        uid bigint,
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    create table user (
       id bigint not null auto_increment,
        name varchar(255),
        primary key (id)
    ) engine=InnoDB
Hibernate: 
    
    alter table contact_info 
       add constraint FKr76qkq93fb87i0tkquhx1a8fj 
       foreign key (uid) 
       references user (id)

到这里有的小伙伴可能对mappedBy属性开始有点好奇了,那我设置和不设置mappedBy到底有啥区别,我只用@JoinColumn分别设置在一和多的一方,和我设置了mappedBy之间的区别是啥。那我下面就带你研究。

@OneToMany双向关联mappedBy的使用

mappedBy这个属性,主要表示谁来维护关联关系,使用了mappedBy的一方会放弃关联关系的维护。@OneToMany上面的mappedBy属性默认为空,说明一的一方需要维护关系。而如果设置了mappedBy = "user",代表一方放弃维护关系。可能有点抽象,什么放弃维护关系,什么维护关系,听得有点难懂,简单的来说维护关系就是维护外键,如果还不太明白,接下来我就通过例子分别来看看设置和不设置mappedBy分别有啥效果。

@OneToMany双向关联不设置mappedBy

首先来看不设置mappedBy是怎样的?

@OneToMany双向关联不设置mappedBy,也就是说双方都会维护关系,也就是会维护外键。

首先我们需要把一的一方的级联操作加上才能开始我们的测试

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "user")
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @Builder.Default
  @OneToMany(cascade = CascadeType.ALL)
  @JoinColumn(name = "uid") //不会创建中间表,而是在多的一方设置外键,并指定外键列名称
  private List<ContactInfo> contactInfos = new ArrayList<>();

}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "contact_info")
public class ContactInfo {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String phoneNumber;

  private String address;

  @ManyToOne
  @JoinColumn(name = "uid") //不会创建中间表,而是在多的一方设置外键,并指定外键列名称
  private User user;
}

我们来看多的一方新增的情况。

  1. 多方新增的情况
List<ContactInfo> contactInfos = new ArrayList<>(List.of(ContactInfo.builder()
        .address("test address").phoneNumber("1234").build()));
    User user = User.builder()
        .name("kevin").contactInfos(contactInfos).build();
    userRepository.save(user);

可以看到新增的情况下,在插入多方(有外键的一方)的情况下,并不是一开始就把外键一并插入进去,而是插入了一条外键为空的数据,然后在update外键

Hibernate: 
    insert 
    into
        user
        (name) 
    values
        (?)
2022-07-22 15:26:51.428 TRACE 14032 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [kevin]
Hibernate: 
    insert 
    into
        contact_info
        (address, phone_number, uid) 
    values
        (?, ?, ?)
2022-07-22 15:26:51.446 TRACE 14032 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [test address]
2022-07-22 15:26:51.446 TRACE 14032 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [1234]
2022-07-22 15:26:51.447 TRACE 14032 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [null]
Hibernate: 
    update
        contact_info 
    set
        uid=? 
    where
        id=?
2022-07-22 15:26:51.474 TRACE 14032 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-07-22 15:26:51.475 TRACE 14032 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]

由此我们可以分析出,第一次insert into contact_info 一条外键为空的数据,是因为一的一方级联操作,级联插入了多方的数据,第二次update外键的语句是因为一的一方需要维护外键,所以会对多方新增的数据update外键。

所以这种情况如果设置了外键不能为空,上面就会报错,所以这种情况我们需要在多的一方设置一下一的一方,如下。

List<ContactInfo> contactInfos = new ArrayList<>(List.of(ContactInfo.builder()
        .address("test address").phoneNumber("1234").build()));
    User user = User.builder()
        .name("kevin").contactInfos(contactInfos).build();
    contactInfos.get(0).setUser(user);
    userRepository.save(user);

运行结果如下,可以看到第一次insert into contact_info的时候虽然已经把外键设置进去了,但是后面还是一样执行了update语句,这是因为一的一方需要维护关系(维护外键),所以还是执行了update语句。

Hibernate: 
    insert 
    into
        user
        (name) 
    values
        (?)
2022-07-22 15:35:34.893 TRACE 14768 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [kevin]
Hibernate: 
    insert 
    into
        contact_info
        (address, phone_number, uid) 
    values
        (?, ?, ?)
2022-07-22 15:35:34.915 TRACE 14768 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [test address]
2022-07-22 15:35:34.915 TRACE 14768 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [1234]
2022-07-22 15:35:34.915 TRACE 14768 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]
Hibernate: 
    update
        contact_info 
    set
        uid=? 
    where
        id=?
2022-07-22 15:35:34.934 TRACE 14768 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-07-22 15:35:34.935 TRACE 14768 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
2022-07-22 15:35:35.127  INFO 14768 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2022-07-22 15:35:35.129  INFO 14768 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2022-07-22 15:35:35.193  INFO 14768 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

所以到这里应该明白维护关系(维护外键)是什么意思了吧。因为一的一方,外键不在自身身上,外键在多的一方,所以当你用一的一方去触发级联保存操作的时候,除了保存双方数据之外还需要把多的一方的外键维护好,所以一定会多一条update外键的语句确保外键维护好。而同样的情况下你使用多的一方去级联保存的时候,虽然也需要维护外键,但是外键就在多的一方自身身上,所以不需要额外去update,直接在insert的时候就能够处理好了。

这里我们可以试一试,前提当然还是多的一方还需要设置级联操作

// 需要设置@ManyToOne(cascade = CascadeType.ALL)
    User user = User.builder()
        .name("kevin").build();
    List<ContactInfo> contactInfos = new ArrayList<>(List.of(ContactInfo.builder()
        .address("test address").phoneNumber("1234").user(user).build()));
    contactInfoRepository.saveAll(contactInfos);

然后保存的语句如下

Hibernate: 
    insert 
    into
        user
        (name) 
    values
        (?)
2022-07-22 15:41:42.691 TRACE 10860 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [kevin]
Hibernate: 
    insert 
    into
        contact_info
        (address, phone_number, uid) 
    values
        (?, ?, ?)
2022-07-22 15:41:42.722 TRACE 10860 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [test address]
2022-07-22 15:41:42.722 TRACE 10860 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [1234]
2022-07-22 15:41:42.723 TRACE 10860 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]

可能到这里还有的小伙伴不是特别理解,那我们还是来看看官方怎么说的:

The many side of many-to-one bidirectional relationships must not define the mappedBy element. The many side is always the owning side of the relationship.

官方的意思就是在多对一中是不设置mappedBy的,因为多的一方总是the owning side,也就是说多的一方总是拥有方(主控方),而一的那一方是non-owning-side(被拥有方或者被控方),你也可以理解为多的一方是有外键的那方。当多的一方一旦把外键的指向改变了,自然就改变了他引用的一的一方。所以多的一方总是the owning side,所以他根本不存在要不要放弃维护关系(或者说维护外键),因为他是一定要维护关系(维护外键的),所以只有一的那一方才有可能存在是否要放弃维护关系(或者说维护外键)。所以当一的一方需要维护关系的时候,在新增多的一方并使用一的一方级联保存的时候,就需要维护多的一方的外键了。大概就是这么一个意思,可能需要多点时间仔细品一品。

上面讲的就是一的一方在维护关系的时候,新增多的一方会多一条update语句去维护多的一方的外键关系。

下面来讲修改的情况,一的一方修改多的一方的数据的时候。

  1. 多方更新的情况
User user = userRepository.findById(1L).get();
user.getContactInfos().get(0).setAddress("test test");
userRepository.save(user);
Hibernate: 
    update
        contact_info 
    set
        address=?,
        phone_number=?,
        uid=? 
    where
        id=?
2022-07-27 14:23:59.670 TRACE 19624 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [test test]
2022-07-27 14:23:59.671 TRACE 19624 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [1234]
2022-07-27 14:23:59.671 TRACE 19624 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [null]
2022-07-27 14:23:59.671 TRACE 19624 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [1]

可以分析一下,由于更新前,先进行了查询,并且配置了双向关联,所以被更新的contactInfo数据是有关联user的,因此更新正常。所以save User的时候会update多方ContactInfo

最后在来讲一下多方删除的情况

  1. 多方删除的情况
  • 仅从一方的list中remove
User user = userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(0);
user.getContactInfos().remove(deletedContact);
userRepository.save(user);

执行结果如下:

Hibernate: 
    update
        contact_info 
    set
        uid=null 
    where
        uid=?
2022-08-01 14:43:47.630 TRACE 17912 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]

从执行结果来看,可以得知只是把contact_info的外键设置为null,并没有物理删除掉contact_info的数据,所以如果这种情况下设置了外键约束,则会报错。

我们可以分析一下,当我们从usergetContactInfos中移除掉了deletedContact, 这就意味着userdeletedContact的关系断开了,又因为一方需要维护关系,所以这个操作会触发被remove掉的deletedContact的外键Id被置空。

这种方式存在两个个问题值得我们思考。第一个那就是我们并没有删除掉contact_info,只是把外键设置为空。所以这种情况是分场景使用的,就是看你到底想不想真正的删除掉多方的数据。如果一方和多方是聚合关系,并且不想真正删除多方数据(多方数据可以和别的一方数据再次关联),那么适用这种方式。但如果是组合关系,那么不存在多方和一方再次关联的情况,是不适用这种方式的。第二个问题就是如果设置外键不能为空,则存在不能更新的问题。

  • 一方list中remove,并且多方显式delete
    那如果我们想删掉多方的数据,该怎么办,很显然我们会想到显式的调用多方的delete,这里不讨论设置@OneToManyorphanRemoval = true,因为如果设置了orphanRemoval = true,不需要显式delete,当直接save也会删除到多方的数据。
User user = userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(0);
user.getContactInfos().remove(deletedContact);
contactInfoRepository.delete(deletedContact);
userRepository.save(user);

执行结果如下

Hibernate: 
    update
        contact_info 
    set
        uid=null 
    where
        uid=?
2022-08-01 14:56:31.683 TRACE 10848 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
Hibernate: 
    delete 
    from
        contact_info 
    where
        id=?
2022-08-01 14:56:31.686 TRACE 10848 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]

可以看到我们最后是执行了delete语句,把数据物理删除掉了,但是我们可以看到在delete之前,我们还是update了设置了外键为空,所以如果存在外键约束不能为空,这个时候依然会存在问题。

额外提一点,如果上面的代码去掉save的操作,结果也是一样的。可以思考一下为啥,这个跟JPA底层机制有关。

User user = userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(0);
user.getContactInfos().remove(deletedContact);
contactInfoRepository.delete(deletedContact); // 此时持久化操作从多方delete发出,但是外键维护关系一方未放弃,还是会执行update的操作。

言归正传,上面这种方式我们想要彻底删除掉多方数据,但是还是依然会引入一次更新外键为空的操作,这个操作其实很鸡肋,而且如果有外键不能为空的约束的话则会报错。所以当一方不放弃维护关系的时候,不要使用这种方式去删除多方数据。

  • 只在多方delete

有的人可能在想,如果我不在多方remove,直接显式删除多方是不是就不会有这个update语句的产生

User user = userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(0);
contactInfoRepository.delete(deletedContact);
userRepository.save(user);

让人失望的是,上面这段代码什么都不会发生。由于先进行了查询,所以jpa认为被删除的contactInfo数据和user的关系还在(跟JPA底层的机制有关,以后有机会在专门讲一下)。直接删除contactInfo无效。必须先从一方持有的list中remove掉才行。

综上我们可以总结一下一方不放弃维护关系的结论

  1. 由于双方都维护外键关系,一方维护关系体现在对多方外键的更新上。
  2. 而remove操作,只是断开关联。但不会删除多方数据。remove之后,多方显式调用delete操作,多方才会被删除。
  3. 在这种配置下,插入和删除,都会多执行一条update多方外键的sql,很多情况下是完全没必要的。而且如果数据库外键如果不能为空会报错。

所以一方不放弃维护关系的适用场景如下:

  1. 多方的外键可以为空。也就是说多方和一方的关系是聚合(可以理解为多方的外键可以为空),允许多方不关联一方。

  2. 只想update多方外键为空,而不想彻底删除多方数据。

@OneToMany双向关联设置mappedBy

一的一方设置了mappedBy则说明一方不维护关系了,放弃了关系的维护。也就是放弃了外键的维护。

一样的我们也是用多方的新增,更新和删除来实践一下。

但首先我们还是先把我们的实体类设置好

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "user")
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @Builder.Default
  @OneToMany(cascade = CascadeType.ALL,mappedBy = "user")
  private List<ContactInfo> contactInfos = new ArrayList<>();

}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "contact_info")
public class ContactInfo {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String phoneNumber;

  private String address;

  @ManyToOne
  @JoinColumn(name = "uid")
  private User user;
}
  1. 多方新增的情况
List<ContactInfo> contactInfos = new ArrayList<>(List.of(ContactInfo.builder()
    .address("test address").phoneNumber("1234").build()));
User user = User.builder()
    .name("kevin").contactInfos(contactInfos).build();
userRepository.save(user);

执行结果如下:

Hibernate: 
    insert 
    into
        user
        (name) 
    values
        (?)
2022-08-01 15:22:06.257 TRACE 11024 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [kevin]
Hibernate: 
    insert 
    into
        contact_info
        (address, phone_number, uid) 
    values
        (?, ?, ?)
2022-08-01 15:22:06.270 TRACE 11024 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [test address]
2022-08-01 15:22:06.270 TRACE 11024 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [1234]
2022-08-01 15:22:06.270 TRACE 11024 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [null]

从执行结果可以看出来,多方最后只是新增了一条外键为空的数据。

我们可以分析一下,由于一方放弃维护关系,那么不会有update外键的操作。而由于设置了级联persist,所以多方数据会级联插入。但是导致插入的多方数据没有外键。如果有外键约束不能为空则会报错。所以上面这种方式是一种错误的新增方式,因为新增的多方数据是没有外键的,跟我们的预期效果是不一样的。

正确的新增方式是在我们在保存的时候,多方还需要设置一下一方的对象。

List<ContactInfo> contactInfos = new ArrayList<>(List.of(ContactInfo.builder()
    .address("test address").phoneNumber("1234").build()));
User user = User.builder()
    .name("kevin").contactInfos(contactInfos).build();
contactInfos.get(0).setUser(user);
userRepository.save(user);

执行结果如下:

Hibernate: 
    insert 
    into
        user
        (name) 
    values
        (?)
2022-08-01 15:31:48.118 TRACE 20276 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [kevin]
Hibernate: 
    insert 
    into
        contact_info
        (address, phone_number, uid) 
    values
        (?, ?, ?)
2022-08-01 15:31:48.132 TRACE 20276 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [test address]
2022-08-01 15:31:48.133 TRACE 20276 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [1234]
2022-08-01 15:31:48.133 TRACE 20276 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]

从执行结果可以看出来,最后是多方插入了一条数据,并且是带有外键的值,这个时候就是跟我们预期的结果是一样的了。

我们可以分析一下,由于一方放弃维护多方外键,所以新增的时候一方并不会去更新外键。但由于级联新增的设置,所以还是会插入多方数据。所以多方需手动设置外键的关联对象,插入时外键才会有值。

所以上面这种方式才是一方放弃关系维护时,正确的多方插入方式,也就是给插入的多方数据设置关联的一方对象。

  1. 多方更新的情况
User user = userRepository.findById(1L).get();
user.getContactInfos().get(0).setAddress("test test");
userRepository.save(user);

执行结果

Hibernate: 
    update
        contact_info 
    set
        address=?,
        phone_number=?,
        uid=? 
    where
        id=?
2022-08-01 16:12:57.366 TRACE 19960 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [test test]
2022-08-01 16:12:57.366 TRACE 19960 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [1234]
2022-08-01 16:12:57.366 TRACE 19960 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]
2022-08-01 16:12:57.366 TRACE 19960 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [1]

结果和一方未放弃维护关系时是一致的

可以分析一下,由于更新前,先进行了查询,并且配置了双向关联,所以被更新的contactInfo数据是有关联user的,因此更新正常。

  1. 多方删除的情况
  • 仅从一方的list中remove(这里不讨论设置了orphanRemoval = true的情况)
User user = userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(0);
user.getContactInfos().remove(deletedContact);
userRepository.save(user);

执行结果是没有任何更新删除的操作

我们可以分析一下,remove操作只是使关系断开。但由于一方放弃外键关系维护,所以不会更新多方外键。而由于没有显式delete多方,所以也不会删除contactInfo数据。这种删除方式显然是错误的。

如果设置了orphanRemoval = true的情况,上述的代码是能删除数据的,执行的sql如下

Hibernate: 
    delete 
    from
        contact_info 
    where
        id=?
2022-08-01 16:48:11.989 TRACE 9136 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]

  • 从一方的list中remove,并且多方显式执行delete
User user = userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(0);
user.getContactInfos().remove(deletedContact);
contactInfoRepository.delete(deletedContact);
userRepository.save(user);

执行结果如下:

Hibernate: 
    delete 
    from
        contact_info 
    where
        id=?
2022-08-01 16:24:56.182 TRACE 20468 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]

从执行结果来看,并没有更新外键为空的操作,而是直接删除掉了数据

我们可以分析一下,由于一方放弃了外键关系所以维护,所以remove的时候,一方不会去更新多方外键为null。在remove后关系断开,多方显式调用delete,可以删除掉contactInfo。

所以这是一方放弃关系维护时,正确的多方删除的方式,先要在一方维护的多方list中remove掉删除数据,然后多方显式调用delete。

另外,去掉userRepository.save(user),删除操作也是可以正常被触发的。

  • 仅在多方delete
User user = userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(0);
contactInfoRepository.delete(deletedContact);
userRepository.save(user);

这种方式会报错,如下:

org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.ObjectDeletedException: deleted instance passed to merge: [org.example.entity.ContactInfo#<null>]; nested exception is java.lang.IllegalArgumentException: org.hibernate.ObjectDeletedException: deleted instance passed to merge: [org.example.entity.ContactInfo#<null>]

需要改成如下方式

User user = userRepository.findById(1L).get();
ContactInfo deletedContact = user.getContactInfos().get(0);
//需要调用下:理解为清除对ContactInfo 表数据的引用,不然会报错关闭session或者deleted instance passed to merge:
user.getContactInfos().clear();
contactInfoRepository.delete(deletedContact);
userRepository.save(user);

最后的结果如下

Hibernate: 
    delete 
    from
        contact_info 
    where
        id=?
2022-08-01 16:32:56.768 TRACE 2128 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]

很明显这种删除方式也是不合理的。

综上我们可以总结一下一方放弃维护关系的结论

  1. 由于一方放弃维护外键关系,一方放弃维护关系体现在不会对多方外键进行更新。
  2. remove操作只是使关系断开。但由于一方放弃外键关系维护,所以不会更新多方外键。而由于没有显式delete多方,所以也不会删除contactInfo数据,所以需要显式的delete多方。
  3. 在这种配置下,插入和删除,都不会有一条update多方外键的sql

总结

最后总结一下一方维护关系和一方放弃维护关系设置下,应该怎么进行新增插入删除操作

一方不放弃维护关系的表象一方放弃维护关系的表象一方不放弃维护关系时的正确操作一方放弃维护关系时的正确操作结论
多方新增1. 新增多方数据 2. 更新多方外键1. 新增多方数据1. 如果设置了外键不为空的约束,需要在对方设置好一方对象1. 多方需要设置一方对象 2. 一方进行save操作建议采用一方放弃维护关系,避免插入和删除执行两条sql
多方更新1. 直接更新多方数据1. 直接更新多方数据1. 一方进行save操作1. 一方进行save操作在更新下没有区别
多方删除1. 更新多方外键为空 2. 删除多方数据1. 直接删除多方数据1. 从一方的list中remove多方 2. 并显式删除多方1. 一方list中remove掉多方 2. 并显式 删除多方需要彻底删除多方数据时,建议采用一方放弃关系的方式。如果不想删除多方,只是设置外键为空,这种情况下只能采用一方不放弃的方式

@OneToMany@ManyToOne双向关联最佳实践

所以到这里可以总结一下我个人推荐one-to-Many双向关联最佳实践的设置是怎样的。从上面总结可以看出,绝大多数场景下,应该采取一方放弃维护关系的方式。这避免了插入和删除时执行两条sql的问题,而且也不会因为数据库设置了外键字段不能为空,导致update的sql报错。所以这里主要讨论的最佳实践就是一方放弃维护关系的方式。

  1. 外键设置在owning-side(拥有方),在一对多双向关系中,多的一方就是owning-side(拥有方,所以外键设置在多的一方。如果需要自定义指定外键列的名称,则使用@JoinColumn来指定,否则JPA将采用默认的名称。 @JoinColumn一般都是配置在在owning-side(拥有方),或者说是在mappedBy的另一方。
  2. mappedBy一定是定义在non-owning-side(被拥有方或者被控方),他指向owning-side(拥有方或者主控方)。mappedBy的值是指向另一方的实体里面属性的字段。 在一对多双向关系中, mappedBy设置在一的一方,指向多方实体里面的属性字段。
  3. 为了避免双向关联嵌套死循环,我们应该在owning-side(拥有方)中设置@ToString@EqualsAndHashCode,exclude 掉non-owning-side(被维护方)的字段。
  4. non-owning-side(被维护方)的一方,也就是在一的一方中需要设置级联操作@OneToMany(cascade = CascadeType.ALL),至于orphanRemoval = true根据需求使用
  5. 根据个人需要判断是否需要外键约束,我们这边一般都不使用外键约束。
  6. 一方放弃维护关系时的正确操作,需要参考上面总结的表格。

可以对比一下跟@OneToOne双向关联最佳实践的区别。

最后提供 @OneToMany@ManyToOne双向关联最佳实践的实体配置

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "user")
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  @Builder.Default
  @OneToMany(cascade = CascadeType.ALL,mappedBy = "user")
  private List<ContactInfo> contactInfos = new ArrayList<>();

}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "contact_info")
@EqualsAndHashCode(exclude = "user")
@ToString(exclude = "user")
public class ContactInfo {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String phoneNumber;

  private String address;

  @ManyToOne
  @JoinColumn(name = "uid",foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
  private User user;
}

至于新增,更新,删除操作需要参考上面总结的表格中的一方放弃维护关系时的正确操作。

到此Spring Data JPA的关联关系注解的使用就介绍到这里,文章也是通过网上的资料还有自己操作实践写的,如果有什么不对或者疑惑的地方,欢迎指出~ 谢谢。

参考

Multiplicity in Entity Relationships

@JoinColumn 详解

SpringBoot重点详解–@JoinColumn注解

@JoinColumn 详解

Bidirectional Relationships

《Spring Data JPA从入门到精通》

Spring Data JPA中的mappedBy

JPA:Spring Data JPA @OneToMany级联,多方删除修改新增总结

JPA: Spring Data JPA @OneToMany 注解参数 orphanRemoval,一对多删除详解

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值