1. 概述

JPA 让 Java 应用操作关系型数据库变得更轻松。当我们把每张表都对应到一个独立的实体类时,事情很简单。

但实际开发中,我们有时需要更灵活的映射方式:

  • 想对字段做逻辑分组时,可以用 @Embedded 把多个类合并映射到一张表 ✅
  • 涉及继承时,可以将类继承结构映射到数据库表结构 ✅
  • 当相关字段分散在多张表中,但我们希望用一个 Java 类来统一建模

本文重点解决最后这一类场景。

2. 数据模型

假设我们经营一家餐厅,需要记录每道菜品的信息:

  • 名称
  • 描述
  • 价格
  • 包含哪些过敏原(如花生、芹菜、芝麻等)

由于过敏原种类多,我们将其单独建表,形成如下结构:

meals

接下来,我们就看看如何用 JPA 注解把这两张表映射到实体类中。

3. 创建多个实体类

最直观的做法是为每张表创建一个实体类。

先定义主表对应的 MealWithMultipleEntities

@Entity
@Table(name = "meal")
public class MealWithMultipleEntities {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    Long id;

    @Column(name = "name")
    String name;

    @Column(name = "description")
    String description;

    @Column(name = "price")
    BigDecimal price;

    @OneToOne(mappedBy = "meal")
    AllergensAsEntity allergens;

    // standard getters and setters
}

再定义过敏原表对应的 AllergensAsEntity

@Entity
@Table(name = "allergens")
class AllergensAsEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "meal_id")
    Long mealId;

    @OneToOne
    @PrimaryKeyJoinColumn(name = "meal_id")
    Meal meal;

    @Column(name = "peanuts")
    boolean peanuts;

    @Column(name = "celery")
    boolean celery;

    @Column(name = "sesame_seeds")
    boolean sesameSeeds;

    // standard getters and setters
}

注意这里 meal_id 既是主键又是外键,因此要用 @PrimaryKeyJoinColumn 显式声明关联关系。

但这种方式有两个痛点:

  • 无法强制保证每道菜都有对应的过敏原记录 —— 即使加了 @NotNull,也只是运行时校验,不够彻底
  • 业务上本应属于同一逻辑对象的数据被拆到了两个类里,使用起来不够直观

虽然可以通过级联保存缓解问题,但代码复杂度上升,且仍存在数据不一致风险。

4. 使用 @SecondaryTable 创建单一实体

更好的方案是使用 @SecondaryTable 注解,让一个实体类映射多张表:

@Entity
@Table(name = "meal")
@SecondaryTable(name = "allergens", pkJoinColumns = @PrimaryKeyJoinColumn(name = "meal_id"))
class MealAsSingleEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    Long id;

    @Column(name = "name")
    String name;

    @Column(name = "description")
    String description;

    @Column(name = "price")
    BigDecimal price;

    @Column(name = "peanuts", table = "allergens")
    boolean peanuts;

    @Column(name = "celery", table = "allergens")
    boolean celery;

    @Column(name = "sesame_seeds", table = "allergens")
    boolean sesameSeeds;

    // standard getters and setters

}

✅ JPA 会在底层自动做主表和副表的 JOIN,填充所有字段。
✅ 所有属性都在同一个类中,使用更直观。
✅ 保存时会自动同步两张表,避免出现“有菜无过敏信息”的脏数据。

⚠️ 注意要点:

  • 凡是来自副表的字段,必须在 @Column 中显式指定 table 属性
  • 主表字段可省略 table,JPA 默认查找主表
  • 支持多个副表:可通过 @SecondaryTables 包裹多个 @SecondaryTable,或直接重复使用 @SecondaryTable(Java 8+ 支持重复注解)

5. 结合 @SecondaryTable@Embedded

我们还可以进一步优化:把副表字段封装成一个嵌入式对象。

@Entity
@Table(name = "meal")
@SecondaryTable(name = "allergens", pkJoinColumns = @PrimaryKeyJoinColumn(name = "meal_id"))
class MealWithEmbeddedAllergens {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    Long id;

    @Column(name = "name")
    String name;

    @Column(name = "description")
    String description;

    @Column(name = "price")
    BigDecimal price;

    @Embedded
    AllergensAsEmbeddable allergens;

    // standard getters and setters

}

@Embeddable
class AllergensAsEmbeddable {

    @Column(name = "peanuts", table = "allergens")
    boolean peanuts;

    @Column(name = "celery", table = "allergens")
    boolean celery;

    @Column(name = "sesame_seeds", table = "allergens")
    boolean sesameSeeds;

    // standard getters and setters

}

这种组合方案的优势很明显:

  • ✅ 数据一致性更强:JPA 自动确保主副表数据成对存在
  • ✅ 代码更简洁:无需手动维护 @OneToOne 关系
  • ✅ 逻辑更清晰:通过 @Embedded 实现了字段的逻辑分组

⚠️ 但要注意:这种“类一对一”关系成立的前提是两张表的主键完全一致(即共享主键)。

💡 小技巧:如果 AllergensAsEmbeddable 被多个实体复用,建议在使用处通过 @AttributeOverride 重定义列名和表名,提升灵活性。

6. 总结

本文演示了如何用 @SecondaryTable 将单个 JPA 实体映射到多张物理表:

  • ✅ 避免了多实体间繁琐的关联配置
  • ✅ 保证了跨表数据的一致性
  • ✅ 结合 @Embedded 可实现更优雅的代码组织

实际项目中,当你遇到“主数据 + 扩展属性分表”或“大宽表拆分”的场景时,这个技巧非常实用,建议集合备用。

示例代码已上传至 GitHub:https://github.com/baeldung/tutorials/tree/master/persistence-modules/java-jpa-2


原始标题:Mapping a Single Entity to Multiple Tables in JPA | Baeldung