1. 简介

在 Hibernate 中,我们通常使用 List 来表示 Java Bean 中的一对多关系。

本篇文章我们来探讨一下如何用 Map 来实现类似功能。与 List 不同,Map 带有键值对结构,因此在持久化时需要额外考虑键的存储方式。

2. MapList 的区别

使用 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 上找到。


原始标题:Persisting Maps with Hibernate | Baeldung