浅析Spring Data JPA的owning side及foreign key的关系-上

背景

Spring Data JPA提供的关联关系(即@OneToOne, @OneToMany)以及级联操作提升了开发者的编程效率,但如果没有理解其中owning side和foreign key的关系,则会非常容易踩到坑,下面就来浅析一下Spring Data JPA中owning side及foreign key的关系。

1.1 OneToOne mapping的owning side

1.1.1 Entity Info

错误代码示例

  • BucketEntity Info
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Entity
@Table(name = "bucket")
public class BucketEntity {

@Id
@Column(name = "bucket_id")
private String id;

@Column(name = "bucket_number")
private String number;

@Column(name = "bucket_description")
private String description;

private String status;

@Column(name = "updated_timestamp")
private Instant timestamp;

@OneToOne(cascade = CascadeType.ALL)
@PrimaryKeyJoinColumn
private BucketDimensionEntity bucketDimensionEntity;
}
  • BucketDimensionEntity info
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
@Table(name = "bucket_dimension")
public class BucketDimensionEntity {

@Id
@Column(name = "bucket_id")
private String id;

@Column(name = "weight")
private String weight;

@Column(name = "volume")
private String volume;
}

1.2 建表语句

1
2
3
4
5
6
7
8
9
CREATE TABLE IF NOT EXISTS public.bucket
(
bucket_id text COLLATE pg_catalog."default" NOT NULL,
bucket_number text COLLATE pg_catalog."default" NOT NULL,
bucket_description text COLLATE pg_catalog."default",
status text COLLATE pg_catalog."default" NOT NULL,
updated_timestamp timestamp with time zone NOT NULL,
CONSTRAINT bucket_pkey PRIMARY KEY (bucket_id)
)
1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE IF NOT EXISTS public.bucket_dimension
(
bucket_id text COLLATE pg_catalog."default" NOT NULL,
weight text COLLATE pg_catalog."default",
volume text COLLATE pg_catalog."default",
CONSTRAINT bucket_dimension_pkey PRIMARY KEY (bucket_id),
CONSTRAINT fk_bucket_dimension FOREIGN KEY (bucket_id)
REFERENCES public.bucket (bucket_id) MATCH SIMPLE
ON UPDATE RESTRICT
ON DELETE CASCADE
)

1.3 运行报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bucketRepository.save(bucketEntity);

------------------------------------------

Hibernate:
insert
into
bucket_dimension
(weight, volume, bucket_id)
values
(?, ?, ?)

org.hibernate.exception.ConstraintViolationException: could not execute statement] with root cause
org.postgresql.util.PSQLException: ERROR: insert or update on table "bucket_dimension" violates foreign key constraint "fk_bucket_dimension"
Detail: Key (bucket_id)=(123456) is not present in table "bucket".
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2675)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2365)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:355)
at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:490)
at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:408)

1.4 踩坑分析

1.4.1 Owning side是什么?

此处报错的原因是对于Hibernate关联关系中的owning side认知不够明确。(Spring Data JPA底层依赖Hibernate)

Hibernate关联关系中的owning side指的是什么?

The owning side of the entity was determined by referencing both entities in the data model and identifying the entity containing the foreign key

简单来说就是关联关系中设置了foreign key的一方即为owning side

1.4.2 @OneToOne和@PrimaryKeyJoinColumn浅析

  1. OneToOne annotation 浅析

此处源码描述可以看出,当我们只用bidirectional @OneToOne时,对于non-owning side的一方,需要在@OneToOne注解中加上mappedBy标识,以达到识别owning side的目的。

  1. PrimaryKeyJoinColumn annotation 浅析

此处源码描述可以看出,在OneToOne关联关系中,引用(使用)@PrimaryKeyJoinColumn注解的Entity的primary key将作为foreign key,此Entity将成为关联关系中的owning side

1.4.3 报错原因分析

回过头来,我们再来看一下BucketEntity中的关联关系设置,不难发现我们将@PrimaryKeyColumn放在了BucketEntity,即告知Hibernate BucketEntity才是关联关系中的owning side (即Hibernate会默认foreign key建在bucket表中)。

从运行结果中的sql语句可以看出,在save方法执行时,就会先insert bucket_dimension table

但实际上foregin key设置在了BucketDimension中,此次insert操作违反了实际数据表中的foreign key constraint (需要保证reference table的数据先被insert),也就抛出了运行结果中出现的exception。

1
2
3
4
5
Hibernate: 
insert into bucket_dimension (......) values (......)

org.postgresql.util.PSQLException: ERROR: insert or update on table "bucket\_dimension" violates foreign key constraint "fk\_bucket\_dimension"
Detail: Key (bucket\_id)=(123456) is not present in table "bucket".

1.5 解决方案

1.5.1 方案说明

解决如上问题,需要将owning side设置到正确的位置上。对于Demo中OneToOne关联关系,我们需将owning side设置在foreign key所在的entity中,即设置在BucketDimensionEntity中。

1.5.2 代码细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
@Table(name = "bucket")
public class BucketEntity {

@Id
@Column(name = "bucket_id")
private String id;

@Column(name = "bucket_number")
private String number;

@Column(name = "bucket_description")
private String description;

private String status;

@Column(name = "updated_timestamp")
private Instant timestamp;

@OneToOne(mappedBy = "bucketObject", cascade = CascadeType.ALL)
private bucketDimensionEntity bucketDimension;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
@Table(name = "bucket_dimension")
public class bucketDimensionEntity {

@Id
@Column(name = "bucket_id")
private String id;

@Column(name = "weight")
private String weight;

@Column(name = "volume")
private String volume;

@OneToOne
@PrimaryKeyJoinColumn
private bucketEntity bucketObject;
}

1.5.3 改进结果

这次我们再调用BucketRepository的save方法进行persist操作就可以成功保存信息到数据库中了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Hibernate: 
insert
into
bucket
(bucket_number, bucket_description, status, updated_timestamp, bucket_id)
values
(?, ?, ?, ?, ?)

Hibernate:
insert
into
bucket_dimension
(weight, volume, bucket_id)
values
(?, ?, ?)

可以看到改动之后Hibernate会按照设定的owning side来决定先insert那张表的数据,改动后owning side设置在了BucketDimensionEntity (foreign key设置在bucket_dimension表)中,那么就会先insert bucket表的数据,再insert bucket_dimension表的数据。

这样的insert顺序就遵循了foreign key constraint,最终整个流程就可以成功完成了。