1. 概述
本文将深入讲解如何在 Spring Data JPA 中使用 @EmbeddedId
注解,并结合 JpaRepository
的 findBy
方法实现基于复合主键(Composite Key)实体的查询。
核心要点是:✅ 使用 @Embeddable
和 @EmbeddedId
来定义和映射复合主键实体。
同时,我们还会重点探讨一个实际场景:如何通过复合主键中的部分字段进行查询,比如只根据作者名查找书籍。
整个实现依赖于 Spring Data JPA 提供的强大方法名推导机制,无需手动写 SQL,简单粗暴又高效。
2. 为什么需要 @Embeddable 和 @EmbeddedId
在实际开发中,单字段主键(如自增 ID)虽然常见,但有些业务场景下必须使用多个字段联合唯一标识一条记录——这就是所谓的 复合主键(Composite Primary Key)。
例如:
- 订单项(order_item)表中,用
订单ID + 商品ID
唯一确定一项 - 成绩表中,用
学生ID + 课程ID
作为主键
JPA 提供了标准方式来处理这种结构:
@Embeddable
:标记一个类可以被“嵌入”到实体中,通常用于表示复合主键的结构@EmbeddedId
:在实体类中使用,表示该字段是复合主键
⚠️ 注意:不要混淆 @EmbeddedId
和 @IdClass
,本文聚焦前者,它更直观、类型安全。
3. 实战示例:基于作者和书名的书籍管理
我们以一个典型的 book
表为例。其主键由两个字段组成:author
(作者)和 name
(书名)。用户可能希望仅通过作者或仅通过书名来查询书籍列表。
目标结构如下:
- 定义一个
BookId
类作为复合主键 - 创建
Book
实体类并嵌入该主键 - 利用 Spring Data JPA 的方法命名规则实现按部分主键查询
3.1. 使用 @Embeddable 定义复合主键类
@Embeddable
public class BookId implements Serializable {
private String author;
private String name;
// 构造方法(可选)
public BookId() {}
public BookId(String author, String name) {
this.author = author;
this.name = name;
}
// standard getters and setters
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BookId)) return false;
BookId bookId = (BookId) o;
return Objects.equals(author, bookId.author) &&
Objects.equals(name, bookId.name);
}
@Override
public int hashCode() {
return Objects.hash(author, name);
}
}
📌 关键点说明:
- 必须实现
Serializable
接口(JPA 规范要求) - ✅ 必须重写
equals()
和hashCode()
,否则在集合操作或持久化时会踩坑 - 字段对应数据库中的主键列
3.2. 使用 @Entity 和 @EmbeddedId 构建实体
@Entity
public class Book {
@EmbeddedId
private BookId id;
private String genre; // 类型
private Integer price; // 价格
// 构造方法
public Book() {}
public Book(BookId id, String genre, Integer price) {
this.id = id;
this.genre = genre;
this.price = price;
}
// standard getters and setters
public BookId getId() {
return id;
}
public void setId(BookId id) {
this.id = id;
}
public String getGenre() {
return genre;
}
public void setGenre(String genre) {
this.genre = genre;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
}
📌 注意事项:
- 主键字段类型为
BookId
,并用@EmbeddedId
标记 - 其他非主键字段正常映射即可
- 这种设计让复合主键变得类型安全且易于维护
3.3. JpaRepository 与方法命名规则
接下来是最关键的部分:如何实现“根据作者查书”或“根据书名查书”。
我们只需定义一个继承 JpaRepository
的接口,并利用 Spring Data JPA 的 方法名推导机制:
@Repository
public interface BookRepository extends JpaRepository<Book, BookId> {
List<Book> findByIdName(String name);
List<Book> findByIdAuthor(String author);
}
🔍 方法解析:
findByIdName(String name)
→ 查询条件为id.name = ?
findByIdAuthor(String author)
→ 查询条件为id.author = ?
Spring Data 会自动识别 id
是复合主键对象,其后的 author
或 name
是该对象的属性,从而生成正确的 SQL:
-- findByIdAuthor("Leo Tolstoy")
SELECT * FROM book WHERE author = 'Leo Tolstoy';
-- findByIdName("War and Peace")
SELECT * FROM book WHERE name = 'War and Peace';
✅ 优势:
- 零配置,无需写
@Query
注解 - 类型安全,编译期检查
- 可组合更多条件,如
findByIdAuthorAndGenre
❌ 踩坑提醒:
- 方法名必须严格遵循
findBy[EmbeddedIdFieldName][Property]
格式 - 如果忘记实现
equals/hashCode
,可能导致缓存、集合比较出错
4. 总结
本文通过一个清晰的例子展示了 Spring Data JPA 中复合主键的完整用法:
- ✅ 使用
@Embeddable
定义复合主键类,务必实现Serializable
并重写equals
和hashCode
- ✅ 在实体中使用
@EmbeddedId
引用该主键类 - ✅ 利用
JpaRepository
的findBy
衍生查询方法,支持对复合主键中任意字段的查询 - ✅ 方法命名规则强大且简洁,避免手写 JPQL
这套方案在处理多维度唯一标识的业务场景时非常实用,比如配置管理、权限矩阵、订单明细等。
示例代码已托管至 GitHub:https://github.com/tech-tutorial/spring-data-jpa-composite-key-demo