1. 概述

本文将带你了解在 JPA 中实现一对一关系(One-to-One Relationship)的几种常见方式。

阅读本文前,建议你对 Hibernate 框架有一定的了解。如果你还不太熟悉,可以先参考我们的 Hibernate 5 与 Spring 整合指南

2. 场景描述

假设我们要开发一个用户管理系统,老板要求为每个用户存储一个邮寄地址。每个用户只对应一个地址,每个地址也只属于一个用户。

这就是典型的一对一关系,发生在 UserAddress 两个实体之间。

接下来,我们将看看如何在 JPA 中实现这种关系。

3. 使用外键实现一对一关系

3.1. 外键建模方式

来看一下这个 ER 图,它展示了通过外键实现的一对一关系:

通过 address_id 外键将 Users 与 Addresses 关联

在这个例子中,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 表的主键

Users 与 Addresses 共享主键

这种方式节省了存储空间,充分利用了一对一关系的特性。

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,而是通过 @MapsIdUser 复制主键值。

5. 使用连接表实现一对一关系

前面两种方式都是一对一的“强制绑定”关系。但在某些场景中,这种关系可能是可选的

5.1. 连接表建模方式

比如员工和工位之间的关系:一个员工对应一个工位,但并不是每个员工都有工位,也不是每个工位都有员工。

之前的方式需要使用 NULL 来表示“无关联”,而使用连接表(Join Table)可以避免这种情况:

通过连接表关联 Employees 与 Workstations

只要存在关联,就在 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 找到。


原始标题:One-to-One Relationship in JPA