1. 概述
JPA 让 Java 应用操作关系型数据库变得更轻松。当我们把每张表都对应到一个独立的实体类时,事情很简单。
但实际开发中,我们有时需要更灵活的映射方式:
- 想对字段做逻辑分组时,可以用
@Embedded
把多个类合并映射到一张表 ✅ - 涉及继承时,可以将类继承结构映射到数据库表结构 ✅
- 当相关字段分散在多张表中,但我们希望用一个 Java 类来统一建模 ❗
本文重点解决最后这一类场景。
2. 数据模型
假设我们经营一家餐厅,需要记录每道菜品的信息:
- 名称
- 描述
- 价格
- 包含哪些过敏原(如花生、芹菜、芝麻等)
由于过敏原种类多,我们将其单独建表,形成如下结构:
接下来,我们就看看如何用 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