1. 概述
本文将带你了解在 JPA 中实现一对一关系(One-to-One Relationship)的几种常见方式。
阅读本文前,建议你对 Hibernate 框架有一定的了解。如果你还不太熟悉,可以先参考我们的 Hibernate 5 与 Spring 整合指南。
2. 场景描述
假设我们要开发一个用户管理系统,老板要求为每个用户存储一个邮寄地址。每个用户只对应一个地址,每个地址也只属于一个用户。
这就是典型的一对一关系,发生在 User 和 Address 两个实体之间。
接下来,我们将看看如何在 JPA 中实现这种关系。
3. 使用外键实现一对一关系
3.1. 外键建模方式
来看一下这个 ER 图,它展示了通过外键实现的一对一关系:
在这个例子中,users 表中的 address_id 列是外键,指向 address 表的主键。
3.2. JPA 中的外键实现方式
我们先定义 User
实体类:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Long id;
//...
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "address_id", referencedColumnName = "id")
private Address address;
// ... getters and setters
}
✅ 注意以下几点:
@OneToOne
注解要加在关联的实体字段上(这里是Address
)。@JoinColumn
用来指定外键列名。如果不指定,Hibernate 会按规则自动生成列名。@JoinColumn
只需要加在关系的拥有方(owning side)上,也就是外键所在的那一方。
再来看 Address
实体:
@Entity
@Table(name = "address")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Long id;
//...
@OneToOne(mappedBy = "address")
private User user;
//... getters and setters
}
这里我们使用了 mappedBy
,表示这是双向关系中的“非拥有方”(non-owning side)。
4. 使用共享主键实现一对一关系
4.1. 共享主键建模方式
这种方式不新增外键列,而是让 address
表的主键(user_id)同时作为外键指向 users
表的主键:
这种方式节省了存储空间,充分利用了一对一关系的特性。
4.2. JPA 中的共享主键实现方式
实体类定义稍有不同:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Long id;
//...
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
@PrimaryKeyJoinColumn
private Address address;
//... getters and setters
}
@Entity
@Table(name = "address")
public class Address {
@Id
@Column(name = "user_id")
private Long id;
//...
@OneToOne
@MapsId
@JoinColumn(name = "user_id")
private User user;
//... getters and setters
}
📌 关键点说明:
mappedBy
现在放在User
类上,因为外键在address
表。@PrimaryKeyJoinColumn
表示使用主键作为外键。Address
中的id
不再使用@GeneratedValue
,而是通过@MapsId
从User
复制主键值。
5. 使用连接表实现一对一关系
前面两种方式都是一对一的“强制绑定”关系。但在某些场景中,这种关系可能是可选的。
5.1. 连接表建模方式
比如员工和工位之间的关系:一个员工对应一个工位,但并不是每个员工都有工位,也不是每个工位都有员工。
之前的方式需要使用 NULL
来表示“无关联”,而使用连接表(Join Table)可以避免这种情况:
只要存在关联,就在 emp_workstation
表中插入一条记录,完全避免 NULL
。
5.2. JPA 中的连接表实现方式
这次我们不再使用 @JoinColumn
,而是使用 @JoinTable
:
@Entity
@Table(name = "employee")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Long id;
//...
@OneToOne(cascade = CascadeType.ALL)
@JoinTable(name = "emp_workstation",
joinColumns =
{ @JoinColumn(name = "employee_id", referencedColumnName = "id") },
inverseJoinColumns =
{ @JoinColumn(name = "workstation_id", referencedColumnName = "id") })
private WorkStation workStation;
//... getters and setters
}
@Entity
@Table(name = "workstation")
public class WorkStation {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Long id;
//...
@OneToOne(mappedBy = "workStation")
private Employee employee;
//... getters and setters
}
📌 说明:
@JoinTable
告诉 Hibernate 使用连接表来维护关系。Employee
是关系的拥有方,因为我们在它上面配置了@JoinTable
。
6. 总结
本文介绍了在 JPA 和 Hibernate 中实现一对一关系的三种常见方式:
方式 | 特点 | 适用场景 |
---|---|---|
外键方式 | 最常用,简单直观 | 强制一对一关系 |
共享主键 | 节省空间 | 强制一对一关系 |
连接表 | 支持可选关系,避免 NULL | 可选一对一关系 |
你可以根据业务需求选择最适合的实现方式。
本文的完整代码可以在 GitHub 找到。