1. 概述

本文将深入讲解如何在 Spring Data JPA 中使用 @EmbeddedId 注解,并结合 JpaRepositoryfindBy 方法实现基于复合主键(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 是复合主键对象,其后的 authorname 是该对象的属性,从而生成正确的 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 中复合主键的完整用法:

  1. ✅ 使用 @Embeddable 定义复合主键类,务必实现 Serializable 并重写 equalshashCode
  2. ✅ 在实体中使用 @EmbeddedId 引用该主键类
  3. ✅ 利用 JpaRepositoryfindBy 衍生查询方法,支持对复合主键中任意字段的查询
  4. ✅ 方法命名规则强大且简洁,避免手写 JPQL

这套方案在处理多维度唯一标识的业务场景时非常实用,比如配置管理、权限矩阵、订单明细等。

示例代码已托管至 GitHub:https://github.com/tech-tutorial/spring-data-jpa-composite-key-demo


原始标题:Spring JPA @Embedded and @EmbeddedId