Jpa中的一对一关系处理

实体类的关系

我们已经了解了如何定义实体类和Repository:
•实体类中的标注,就是描述实体类到数据库Schema的映射,对应于DDL操作
•Repository接口中的方法,则对应于DML操作,即对数据库的增删改查
此前看到的标注都是关于Schema自身字段的信息,Schema之间还存在关系,比如外键关联。相应地,JPA@OneToOne、@ManyToOne、OneToMany和@ManyToMany几个标注提供用以定义实体类之间的关系。
首先来了解一下@OneToOne,相关概念梳理清楚了,后面几个标注也很容易理解。

基础使用

一个博客站点一般会有博客的名称、简介和背景图片等信息,这里通过BlogMetaInfo类来表示。关于(About)页面通常也需要使用这些信息,将其保存在数据库中可以进行动态配置。
那么User类和BlogMetaInfo类显然是一对一的关系,实体类的定义中需要使用@OneToOne标注。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Entity
class BlogMetaInfo {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private long id;

private String title;
private String intro;
private String picUrl;

@OneToOne
private User user;

}

@Entity
public class User {

@Id
@GeneratedValue(strategy=GenerationType.AUTO)

// ...
}

这里BlogMetaInfo包含了一个User类型的属性,并且通过@OneToOne标注。

@OneToOne的属性

  • targetEntity属性表示默认关联的实体类型,默认为当前标注的实体类,绝大数据情况下不需要进行设置。
    -cascade属性表示级联操作策略:
    1. 不定义,则对关系表不会产生任何影响
    2. CascadeType.PERSIST:级联新建
    3. CascadeType.REMOVE:级联删除
    4. CascadeType.REFRESH: 级联刷新,即重新同步到数据库中状态,会覆盖掉已经修改但是还没保存的实体类属性
    5. CascadeType.MERGE: 级联更新
    6. CascadeType.ALL:表示选择全部四项
    • fetch属性表示实体的加载方式,有LAZY和EAGER两种取值,默认值为EAGER
    • ptional属性表示关联的实体是否能够存在null值,默认为true,表示可以存在null值
      关于fetch属性,所有@XXXToOne,默认值为EAGER,所有@XXXToMany,默认值为LAZY。

@JoinColumn 的使用

前一节示例代码中的@OneToOne标注,对应于BlogMetaInfo表中的User_Id字段,自动映射的字段名按照如下方式命名:
关联表的名称 + "" + 关联表主键的字段名这就是User_Id的由来。如果对应的字段名是其他名字,则可以通过@JoinColumn来定义外键关联的字段名称。比如如果字段名为blogUser_Id的话,可以这样定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
public class User {

@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private long id;
private String name;
private String email;

@OneToOne
@JoinColumn(name = "blogUser_Id")
private BlogMetaInfo blogMetaInfo;

}

默认情况下,关联实体(User)的主键一般用来做外键。如果不使用主键作为外键,则需要设置referencedColumnName属性,如:

@JoinColumn(name=”blogUser_Id”, referencedColumnName=”other_column_for_fk”)

@JoinColumn的其它属性与@Column是类似的,可以参考前一节的内容。

unique属性

注意在我们的代码中,由于是基于实体类定义自动生成Schema,需要增加@JoinColumn(unique = true)才会在BlogMetaInfo将user_id字段设置为unique。实际开发中,如果事先定义好了数据库并且设置了user_id字段的unique属性,则@JoinColumn(unique = true)添加与否不会产生任何影响。

1
2
3
4
5
6
7
8
@Entity
class BlogMetaInfo {

@OneToOne
@JoinColumn(unique = true)
private User user;

}

双向@OneToOne关联

如果希望能够从User直接引用到BlogMataInfo,则可以给User增加一个类型为BlogMetaInfo的属性,这样两个实体类就是一种双向关联关系了。

1
2
3
4
5
6
7
8
9
10
11
@Entity
public class User {

@Id
@GeneratedValue(strategy=GenerationType.AUTO)

@OneToOne(mappedBy = "user")
private BlogMetaInfo blogMetaInfo;

// ...
}

注意上面的代码中,不仅用@OneToOne标注了blogMetaInfo,同时还设置mappedBy属性。
关于mappedBy属性,需要理解一下两点:

  1. mappedBy = "user"表示,当前类(User)是通过BlogMetaInfo中user属性与之建立关联的。

  2. 设置了mappedBy属性的关系标注(各种@XXXToXX),表明当前类是关系的被维护方,而另外一个类则是关系维护方,你可以这样理解:

  • 关系维护方(BlogMetaInfo类)对应的是定义外键约束的数据库表

  • 关系被维护方(User类)对应于外键所在的数据库表

最后需要注意的一个问题是,关系维护方才能够操作两者的关系。反过来说,User对象即使设置BlogMetaInfo属性,如果对该对象进行存储,并不会去更新外键关联,因为User是关系被维护方。

双向关联与cascade示例

为了理解以上知识点,这里通过代码示例来进一步。
现在有如下实体类定义:

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
class BlogMetaInfo {
@OneToOne(cascade = CASCADETYPE.ALL)
@JoinColumn(unique = true)
private User user;
}

@Entity
public class User {
@OneToOne(cascade = CASCADETYPE.ALL, mappedBy = "user")
private BlogMetaInfo blogMetaInfo;
}

我们给两个实体的@OneToOne都设置cascade属性,表示可以进行级联操作。

1
2
3
4
5
6
7
BlogMetaInfo blogMetaInfo = new BlogMetaInfo();
// initialize blogMetaInfo
// ...

User user = new User();
// initialize blogMetaInfo
// ...

假设有blogMetaInfoRepository和userRepository两个接口的实例,我们分别进行两种操作:

1
2
3
// 操作1:保存User
user.setBlogMetaInfo(blogMetaInfo);
userRepository.save(user);

1
2
3
// 操作2:保存BlogMetaInfo
blogMetaInfo.setUser(user);
blogMetaInfoRepository.save(blogMetaInfo);