应该不止我一个人搞不清楚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 JoinColumn
annotation itself is
* defaulted, a single join column is assumed and the default values
* apply.
*
*
* Example:
*
* @ManyToOne
* @JoinColumn(name="ADDR_ID")
* public Address getAddress() { return address; }
*
*
* Example: unidirectional one-to-many association using a foreign key mapping
*
* // In Customer class
* @OneToMany
* @JoinColumn(name="CUST_ID") // join column is in table for Order
* public Set<Order> getOrders() {return orders;}
*
*
* @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.
*
* - 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.
*
*
* 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.
*
* - When used with entity relationship mappings other
* than the cases described here, the referenced column is in the
* table of the target entity.
*
- When used with a unidirectional OneToMany foreign key
* mapping, the referenced column is in the table of the source
* entity.
*
- When used inside a
JoinTable
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.
* - When used in a
CollectionTable
mapping, the
* referenced column is in the table of the entity containing the
* collection.
*
*
* 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 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.
*/
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.
* 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.
*
* 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.
*
*/
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
注解的时候其实大多数只会用到name
和referencedColumnName
两个属性,有时候还可能会用到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 mappedBy
element of
* the OneToOne
annotation to specify the relationship field or
* property of the owning side.
*
* The OneToOne
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
* mappedBy
element of the OneToOne
* annotation to specify the relationship field or property of the
* embeddable class. The dot (".") notation syntax must be used in the
* mappedBy
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.
*
*
* Example 1: One-to-one association that maps a foreign key column
*
* // On Customer class:
*
* @OneToOne(optional=false)
* @JoinColumn(
* name="CUSTREC_ID", unique=true, nullable=false, updatable=false)
* public CustomerRecord getCustomerRecord() { return customerRecord; }
*
* // On CustomerRecord class:
*
* @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:
*
* @Entity
* public class Employee {
* @Id Integer id;
*
* @OneToOne @MapsId
* EmployeeInfo info;
* ...
* }
*
* // On EmployeeInfo class:
*
* @Entity
* public class EmployeeInfo {
* @Id Integer id;
* ...
* }
*
*
* Example 3: One-to-one association from an embeddable class to another entity.
*
* @Entity
* public class Employee {
* @Id int id;
* @Embedded LocationDetails location;
* ...
* }
*
* @Embeddable
* public class LocationDetails {
* int officeNumber;
* @OneToOne ParkingSpot parkingSpot;
* ...
* }
*
* @Entity
* public class ParkingSpot {
* @Id int id;
* String garage;
* @OneToOne(mappedBy="location.parkingSpot") Employee assignedTo;
* ...
* }
*
*
*
* @since 1.0
*/
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface 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.
*/
Class targetEntity() default void.class;
/**
* (Optional) The operations that must be cascaded to
* the target of the association.
*
* 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;
}
我们来看看注解上面属性的作用吧
(Optional) The entity class that is the target of the association.
Defaults to the type of the field or property that stores the association.
{}
(Optional) The operations that must be cascaded to the target of the association.
By default no operations are cascaded.
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.
true
(Optional) Whether the association is optional. If set to false then a non-null relationship must always exist.
""
(Optional) The field that owns the relationship. This element is only specified on the inverse (non-owning) side of the association.
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
就改变了employee
与employee_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
属性去掉,使用外键约束就可以看到建表语句会加上这么一句外键约束。说明现在employee
的info_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;
}
之后我们保存就能成功了,而且employee
和employee_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
的双向关联设置,我们当然一切以官方要求为准了
mappedBy
element of the @OneToOne
第一个条件说的就是non-owning side(EmployeeInfo
),必须持有owning side(Employee
)的引用,并且需要设置@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-side(Employee
)来维护,也就是外键只是在Employee
表中。所以只有Employee
表中有外键指向EmployeeInfo
表。而我们在EmployeeInfo
设置 @JoinColumn(name = "id", referencedColumnName = "info_id")
也是一样的道理,就是EmployeeInfo
放弃设置外键,EmployeeInfo
主键的值跟Employee
外键的值相等。
@OneToOne
的mappedBy
我们再来深入研究一下这个@OneToOne
的mappedBy
mappedBy
在单向关系不需要设置该属性,双向关系必须设置,避免双方都建立外键字段,设置了mappedBy
的一方不会创建外键字段mappedBy
一定是定义在non-owning-side(被拥有方或者被控方),他指向owning-side(拥有方或者主控方),官方也是这么说的:This element is only specified on the inverse (non-owning) side of the association.
mappedBy
的值是指向另一方的实体里面属性的字段,而不是数据库字段mappedBy
跟JoinColumn
/JoinTable
总是处于互斥的一方,也就是mappedBy
不能跟JoinColumn
/JoinTable
同时使用@JoinColumn
学习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.
保存之后可以发现,虽然Employee
和EmployeeInfo
表中都有数据,但是外键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双向关联最佳实践的设置是怎样的。
@JoinColumn
来指定,否则JPA将采用默认的名称。所以一般@JoinColumn
都是配置在owning-side(拥有方)或者可以理解为配置在mappedBy
的另一方。mappedBy
一定是定义在non-owning-side(被拥有方或者被控方),他指向owning-side(拥有方或者主控方)。mappedBy
的值是指向另一方的实体里面属性的字段@ToString
和@EqualsAndHashCode
,exclude 掉owning-side(拥有方)的字段。@OneToOne(cascade = {CascadeType.ALL})
,至于orphanRemoval = true
根据需求使用下面就是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.
*
* 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
* mappedBy
element must be used to specify the relationship field or
* property of the entity that is the owner of the relationship.
*
*
The OneToMany
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
* mappedBy
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 java.util.Map
, the cascade
* element and the orphanRemoval
element apply to the map value.
*
*
*
* Example 1: One-to-Many association using generics
*
* // In Customer class:
*
* @OneToMany(cascade=ALL, mappedBy="customer")
* public Set<Order> getOrders() { return orders; }
*
* In Order class:
*
* @ManyToOne
* @JoinColumn(name="CUST_ID", nullable=false)
* public Customer getCustomer() { return customer; }
*
*
* Example 2: One-to-Many association without using generics
*
* // In Customer class:
*
* @OneToMany(targetEntity=com.acme.Order.class, cascade=ALL,
* mappedBy="customer")
* public Set getOrders() { return orders; }
*
* // In Order class:
*
* @ManyToOne
* @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:
*
* @OneToMany(orphanRemoval=true)
* @JoinColumn(name="CUST_ID") // join column is in table for Order
* public Set<Order> getOrders() {return orders;}
*
*
*
* @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.
*
* 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.
* Defaults to no operations being cascaded.
*
*
When the target collection is a {@link java.util.Map
* java.util.Map}, the cascade
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
* OneToMany
entity side must used the
* mappedBy
element to specify the relationship field or
* property of the entity that is the owner of the relationship.
*
* The ManyToOne
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 OneToMany
entity side must use the mappedBy
* element of the OneToMany
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 mappedBy
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.
*
*
* Example 1:
*
* @ManyToOne(optional=false)
* @JoinColumn(name="CUST_ID", nullable=false, updatable=false)
* public Customer getCustomer() { return customer; }
*
*
* Example 2:
*
* @Entity
* public class Employee {
* @Id int id;
* @Embedded JobInfo jobInfo;
* ...
* }
*
* @Embeddable
* public class JobInfo {
* String jobDescription;
* @ManyToOne ProgramManager pm; // Bidirectional
* }
*
* @Entity
* public class ProgramManager {
* @Id int id;
* @OneToMany(mappedBy="jobInfo.pm")
* Collection<Employee> manages;
* }
*
*
*
* @since 1.0
*/
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface ManyToOne {
/**
* (Optional) The entity class that is the target of
* the association.
*
* 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.
*
* 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)
可以发现这个时候也是不会创建中间表的,而是在多的一方创建外键,看起来在一对多和多对一双向关联中,@JoinColumn
和mappedBy
在建表语句的生成具有相同的作用。但实际上他们功能并不是一样的。如果你仔细观察你就会发现,在双向关联中,在一的一方加入了@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;
}
我们来看多的一方新增的情况。
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语句去维护多的一方的外键关系。
下面来讲修改的情况,一的一方修改多的一方的数据的时候。
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
最后在来讲一下多方删除的情况
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
的数据,所以如果这种情况下设置了外键约束,则会报错。
我们可以分析一下,当我们从user
的getContactInfos
中移除掉了deletedContact
, 这就意味着user
和deletedContact
的关系断开了,又因为一方需要维护关系,所以这个操作会触发被remove掉的deletedContact
的外键Id被置空。
这种方式存在两个个问题值得我们思考。第一个那就是我们并没有删除掉contact_info
,只是把外键设置为空。所以这种情况是分场景使用的,就是看你到底想不想真正的删除掉多方的数据。如果一方和多方是聚合关系,并且不想真正删除多方数据(多方数据可以和别的一方数据再次关联),那么适用这种方式。但如果是组合关系,那么不存在多方和一方再次关联的情况,是不适用这种方式的。第二个问题就是如果设置外键不能为空,则存在不能更新的问题。
@OneToMany
的orphanRemoval = 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的操作。
言归正传,上面这种方式我们想要彻底删除掉多方数据,但是还是依然会引入一次更新外键为空的操作,这个操作其实很鸡肋,而且如果有外键不能为空的约束的话则会报错。所以当一方不放弃维护关系的时候,不要使用这种方式去删除多方数据。
有的人可能在想,如果我不在多方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掉才行。
综上我们可以总结一下一方不放弃维护关系的结论
所以一方不放弃维护关系的适用场景如下:
多方的外键可以为空。也就是说多方和一方的关系是聚合(可以理解为多方的外键可以为空),允许多方不关联一方。
只想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;
}
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]
从执行结果可以看出来,最后是多方插入了一条数据,并且是带有外键的值,这个时候就是跟我们预期的结果是一样的了。
我们可以分析一下,由于一方放弃维护多方外键,所以新增的时候一方并不会去更新外键。但由于级联新增的设置,所以还是会插入多方数据。所以多方需手动设置外键的关联对象,插入时外键才会有值。
所以上面这种方式才是一方放弃关系维护时,正确的多方插入方式,也就是给插入的多方数据设置关联的一方对象。
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的,因此更新正常。
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]
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)
,删除操作也是可以正常被触发的。
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. 更新多方外键 | 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报错。所以这里主要讨论的最佳实践就是一方放弃维护关系的方式。
@JoinColumn
来指定,否则JPA将采用默认的名称。 @JoinColumn
一般都是配置在在owning-side(拥有方),或者说是在mappedBy
的另一方。mappedBy
一定是定义在non-owning-side(被拥有方或者被控方),他指向owning-side(拥有方或者主控方)。mappedBy
的值是指向另一方的实体里面属性的字段。 在一对多双向关系中, mappedBy
设置在一的一方,指向多方实体里面的属性字段。@ToString
和@EqualsAndHashCode
,exclude 掉non-owning-side(被维护方)的字段。@OneToMany(cascade = CascadeType.ALL)
,至于orphanRemoval = true
根据需求使用可以对比一下跟@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
《Spring Data JPA从入门到精通》