一、复合主键概述
在 JPA 中,当实体需要多个字段联合作为主键时,需要使用复合主键。JPA 提供了两种实现方式:
- @EmbeddedId:嵌入式主键
- @IdClass:主键类
二、@EmbeddedId 实现方式
实现步骤
- 创建主键类 (使用
@Embeddable
注解)
java
@Embeddable
public class EmployeeId implements Serializable {
private static final long serialVersionUID = 1L;
private String departmentId;
private String employeeCode;
// 必须有无参构造器
public EmployeeId() {}
// Getter/Setter 省略
// 必须重写 equals() 和 hashCode()
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
}
- 实体类使用
@EmbeddedId
java
@Entity
public class Employee {
@EmbeddedId
private EmployeeId id; // 嵌入式复合主键
private String name;
// 其他字段...
}
特点分析
优点 | 缺点 |
---|---|
面向对象设计,语义清晰 | 访问主键字段需通过中间对象 |
JPQL 查询可直接导航(e.id.departmentId ) |
主键类需要额外定义 |
主键逻辑集中,可复用 | |
无字段冗余 |
三、@IdClass 实现方式
实现步骤
- 创建主键类(普通 POJO,无注解)
java
public class EmployeeId implements Serializable {
private static final long serialVersionUID = 1L;
private String departmentId;
private String employeeCode;
public EmployeeId() {}
// Getter/Setter 省略
// 必须重写 equals() 和 hashCode()
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
}
- 实体类标注
@IdClass
java
@Entity
@IdClass(EmployeeId.class) // 指定主键类
public class Employee {
@Id
private String departmentId; // 主键字段1
@Id
private String employeeCode; // 主键字段2
private String name;
// 其他字段...
}
特点分析
优点 | 缺点 |
---|---|
直接访问主键字段 | 主键类与实体字段重复 |
兼容传统开发模式 | JPQL 需单独引用字段 |
适合简单主键场景 | 面向对象程度较低 |
四、核心区别对比
特性 | @EmbeddedId | @IdClass |
---|---|---|
主键定义方式 | 封装为独立对象 | 分散在实体类中 |
主键类注解 | @Embeddable |
无注解(普通 POJO) |
实体类注解 | @EmbeddedId |
@IdClass + 多个 @Id |
访问主键字段 | employee.getId().getDepartmentId() |
employee.getDepartmentId() |
JPQL 查询 | WHERE e.id.departmentId = 'dept-01' |
WHERE e.departmentId = 'dept-01' |
代码冗余 | 无冗余 | 主键类字段与实体类重复 |
面向对象程度 | 高 | 低 |
序列化要求 | 主键类必须实现 Serializable | 主键类必须实现 Serializable |
五、为什么必须实现 Serializable 接口
1. 根本原因
- JPA 规范强制要求 :所有复合主键类必须实现
Serializable
接口 - 技术必要性:JPA 实现(如 Hibernate)内部机制依赖序列化能力
2. 具体应用场景
- 分布式环境中主键传输(缓存、集群)
- 主键作为 Map 的键使用(如 EntityManager 缓存)
- 持久化上下文处理主键对象
3. 实现要点
java
public class EmployeeId implements Serializable {
// 强烈建议显式声明 serialVersionUID
private static final long serialVersionUID = 1L;
// 主键字段...
}
4. 常见误区
误区 | 正解 |
---|---|
实体类需要实现 Serializable | 只有主键类需要实现 |
必须定义 serialVersionUID | 规范不强制但强烈建议 |
所有字段都需要序列化 | 只需主键类实现接口 |
六、如何选择实现方式
选择建议
-
优先使用 @EmbeddedId:
- 需要面向对象设计
- 主键逻辑独立且可能复用
- 希望避免字段冗余
-
选择 @IdClass:
- 需要直接访问主键字段
- 维护旧系统兼容性
- 简单主键场景
最佳实践
-
主键类规范:
- 实现
Serializable
- 显式声明
serialVersionUID
- 提供无参构造器
- 重写
equals()
和hashCode()
- 实现
-
性能考量:
- 两种方式性能差异可忽略
- 选择应基于代码结构和可维护性
七、总结
最终建议 :在新项目中优先使用 @EmbeddedId
,它提供更清晰的面向对象设计,同时满足 JPA 规范的所有要求。无论选择哪种方式,都要确保主键类正确实现 Serializable
接口并遵循主键类的基本规范。