1. 简介
在 Hibernate 中,我们通常使用 List
来表示 Java Bean 中的一对多关系。
本篇文章我们来探讨一下如何用 Map
来实现类似功能。与 List
不同,Map
带有键值对结构,因此在持久化时需要额外考虑键的存储方式。
2. Map
与 List
的区别
使用 Map
表示一对多关系时,最大的不同在于引入了 键(Key),这就构成了一个三元关联:父实体、键、值。
因此,使用 Map
时,Hibernate 通常会引入 一张中间表(Join Table) 来存储以下信息:
- 父实体的外键
- Map 的 Key
- Map 的 Value
⚠️ 注意:这张中间表的主键通常不是两个外键的组合,而是父实体外键 + key 列的组合。
根据 Map 中 value 的类型,可以分为两种情况:
- 值类型(Value Type):如 String、Double、Integer 等,或可嵌入对象(@Embeddable)
- 实体类型(Entity Type):如另一个实体类
接下来我们分别来看这两种情况的处理方式。
3. 使用 @MapKeyColumn
(值类型)
假设我们有一个 Order
实体,想记录订单中每项商品的名称和价格,可以用一个 Map<String, Double>
表示:
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue
@Column(name = "id")
private int id;
@ElementCollection
@CollectionTable(name = "order_item_mapping",
joinColumns = @JoinColumn(name = "order_id", referencedColumnName = "id"))
@MapKeyColumn(name = "item_name")
@Column(name = "price")
private Map<String, Double> itemPriceMap;
// getters and setters
}
✅ 关键注解说明:
@ElementCollection
:表示这是一个值类型集合,不是实体引用@CollectionTable
:指定中间表名及关联字段@MapKeyColumn
:指定 Map 的 key 存储在哪一列@Column
:指定 Map 的 value 存储在哪一列
⚠️ 注意:itemPriceMap
是值类型,因此必须使用 @ElementCollection
注解。
4. 使用 @MapKey
(实体类型)
如果我们的 Map value 是一个实体类(比如 Item
),就需要用 @OneToMany
或 @ManyToMany
来声明关系,并配合 @MapKey
使用。
比如我们先定义一个 Item
实体:
@Entity
@Table(name = "item")
public class Item {
@Id
@GeneratedValue
@Column(name = "id")
private int id;
@Column(name = "name")
private String itemName;
@Column(name = "price")
private double itemPrice;
// 其他字段和getter/setter
}
然后修改 Order
实体,将 Map<String, Double>
改为 Map<String, Item>
:
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue
@Column(name = "id")
private int id;
@OneToMany(cascade = CascadeType.ALL)
@JoinTable(name = "order_item_mapping",
joinColumns = @JoinColumn(name = "order_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "item_id", referencedColumnName = "id"))
@MapKey(name = "itemName")
private Map<String, Item> itemMap;
// getters and setters
}
✅ 关键点:
@MapKey(name = "itemName")
:表示使用Item
实体中的itemName
字段作为 Map 的 key- 中间表
order_item_mapping
中不再显式存储 key 字段,而是通过关联Item
表的itemName
字段来获取 key
⚠️ 注意:@MapKey
和 @MapKeyColumn
不能同时使用。前者用于实体字段作为 key,后者用于中间表中单独存储 key 字段。
5. 使用 @MapKeyEnumerated
和 @MapKeyTemporal
当 Map 的 key 是枚举类型或时间类型时,我们可以分别使用:
@MapKeyEnumerated(EnumType.STRING)
:用于枚举类型的 key@MapKeyTemporal(TemporalType.TIMESTAMP)
:用于时间类型的 key
这两个注解行为分别类似于 @Enumerated
和 @Temporal
,它们会将 key 存储在中间表的一列中。
示例:
@MapKeyEnumerated(EnumType.STRING)
@MapKeyColumn(name = "item_type")
如果希望复用实体中已有的字段作为 key,而不是中间表新增列,可以用 @MapKey
替代。
6. 使用 @MapKeyJoinColumn
(复杂键)
假设我们有一个 Seller
实体,并希望将 Order
中的 Item
按照 Seller
分组,即使用 Map<Seller, Item>
:
@Entity
@Table(name = "seller")
public class Seller {
@Id
@GeneratedValue
@Column(name = "id")
private int id;
@Column(name = "name")
private String sellerName;
// getters and setters
}
在 Item
中添加 seller 关联:
@ManyToOne
@JoinColumn(name = "seller_id")
private Seller seller;
然后在 Order
中使用:
@OneToMany(cascade = CascadeType.ALL)
@JoinTable(name = "order_item_mapping",
joinColumns = @JoinColumn(name = "order_id"),
inverseJoinColumns = @JoinColumn(name = "item_id"))
@MapKeyJoinColumn(name = "seller_id")
private Map<Seller, Item> sellerItemMap;
✅ 说明:
@MapKeyJoinColumn(name = "seller_id")
:表示中间表中使用seller_id
作为 key- 这样我们就可以通过
seller_id
进行分组查询
7. 总结
Hibernate 提供了多种方式来持久化 Map 类型,具体使用哪种方式取决于你的业务需求:
Map Key 类型 | 注解方式 | 是否需要中间表 | 是否需要额外 key 列 |
---|---|---|---|
值类型(String、Double) | @MapKeyColumn + @ElementCollection |
✅ | ✅ |
实体字段(如 itemName) | @MapKey(name = "xxx") |
✅ | ❌ |
枚举/时间类型 | @MapKeyEnumerated / @MapKeyTemporal |
✅ | ✅ |
实体对象(如 Seller) | @MapKeyJoinColumn |
✅ | ✅ |
📌 踩坑提醒:
- 不要同时使用
@MapKey
和@MapKeyColumn
- 使用
@ElementCollection
时,value 必须是值类型或@Embeddable
- 如果 Map 的 key 是实体字段,确保该字段在目标实体中唯一,否则查询结果可能不准确
完整示例代码可在 GitHub 上找到。