You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JPA实体多表关联咨询:@Embeddable类使用及关联异常排查

解决三张表关联获取Map结构Pizza数据 & Hibernate注解错误问题

嗨,我来帮你一步步理清这个问题!首先你碰到的那个Invocation of init method failed; nested exception is org.hibernate.AnnotationException: Use of @OneToMany or @ManyToMany targeting an unmapped class:错误,本质很明确:你用@OneToMany/@ManyToMany关联的类,没被Hibernate识别为可映射的实体/嵌入类,先解决这个基础问题,再聊三张表关联和@Embeddable的使用场景。


第一步:先搞定那个Hibernate错误

这个错误的常见原因和解决办法:

  • 关联类没加注解:如果你关联的是实体类,必须给它加@Entity;如果是嵌入类(比如用来存复合字段的类),要加@Embeddable
  • 类没被Hibernate扫描到:
    • Spring Boot项目:要么把实体类放在主启动类的包/子包下,要么用@EntityScan注解指定扫描路径
    • 传统Hibernate项目:在persistence.xml里手动添加<class>你的类全路径</class>配置
  • 误用了普通类:如果你的Map值是个没有任何Hibernate注解的普通POJO,Hibernate根本不知道怎么映射它,要么把它改成实体/嵌入类,要么换用支持的类型(比如基本类型、String)

第二步:三张表关联获取Map结构Pizza数据

要不要用@Embeddable,取决于你的关联表有没有额外字段,分两种情况:

情况1:关联表只有外键(纯多对多)

假设你的三张表是:

  • pizza:id、name
  • topping:id、name
  • pizza_topping:pizza_id、topping_id(只有两个外键,无其他字段)

这种场景不需要@Embeddable,直接用@ManyToMany+@MapKey就能实现Map结构:

@Entity
public class Pizza {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    // 关联到topping表,通过pizza_topping中间表
    @ManyToMany
    @JoinTable(
        name = "pizza_topping",
        joinColumns = @JoinColumn(name = "pizza_id"),
        inverseJoinColumns = @JoinColumn(name = "topping_id")
    )
    // 用Topping的name作为Map的键,值是Topping实体
    @MapKey(name = "name")
    private Map<String, Topping> toppings = new HashMap<>();

    // getters & setters
}

@Entity
public class Topping {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    // 可选:反向关联Pizza
    @ManyToMany(mappedBy = "toppings")
    private List<Pizza> pizzas = new ArrayList<>();

    // getters & setters
}

情况2:关联表有额外字段(比如quantity、create_time)

如果你的pizza_topping表除了pizza_id、topping_id,还有比如quantity(配料数量)这种额外字段,纯多对多就满足不了了——这时候需要把中间表变成一个实体类,并且用@Embeddable来定义它的复合主键(因为中间表的主键是pizza_id+topping_id)。

步骤1:创建@Embeddable的复合主键类

这个类要实现Serializable,重写equals()和hashCode(),还要有默认构造函数:

@Embeddable
public class PizzaToppingId implements Serializable {
    @Column(name = "pizza_id")
    private Long pizzaId;
    @Column(name = "topping_id")
    private Long toppingId;

    // 必须要有默认构造函数
    public PizzaToppingId() {}

    public PizzaToppingId(Long pizzaId, Long toppingId) {
        this.pizzaId = pizzaId;
        this.toppingId = toppingId;
    }

    // 复合主键必须重写equals和hashCode
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PizzaToppingId that = (PizzaToppingId) o;
        return Objects.equals(pizzaId, that.pizzaId) && Objects.equals(toppingId, that.toppingId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(pizzaId, toppingId);
    }

    // getters & setters
}

步骤2:创建中间表的实体类PizzaTopping

用@EmbeddedId关联上面的复合主键,再用@MapsId把主键字段和关联的实体绑定:

@Entity
@Table(name = "pizza_topping")
public class PizzaTopping {
    @EmbeddedId
    private PizzaToppingId id;

    // 关联Pizza,@MapsId指定绑定到PizzaToppingId的pizzaId字段
    @ManyToOne
    @MapsId("pizzaId")
    @JoinColumn(name = "pizza_id")
    private Pizza pizza;

    // 关联Topping,@MapsId指定绑定到PizzaToppingId的toppingId字段
    @ManyToOne
    @MapsId("toppingId")
    @JoinColumn(name = "topping_id")
    private Topping topping;

    // 额外字段:配料数量
    private Integer quantity;

    // 默认构造函数
    public PizzaTopping() {}

    // 方便初始化的构造函数
    public PizzaTopping(Pizza pizza, Topping topping, Integer quantity) {
        this.id = new PizzaToppingId(pizza.getId(), topping.getId());
        this.pizza = pizza;
        this.topping = topping;
        this.quantity = quantity;
    }

    // getters & setters
}

步骤3:在Pizza类里定义Map结构

用@OneToMany关联PizzaTopping,再用@MapKey指定Map的键(比如用Topping的name):

@Entity
public class Pizza {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    // 关联PizzaTopping,mappedBy指定反向关联的字段
    @OneToMany(mappedBy = "pizza", cascade = CascadeType.ALL, orphanRemoval = true)
    // 用Topping的name作为Map的键,值是PizzaTopping实体(包含数量信息)
    @MapKey(name = "topping.name")
    private Map<String, PizzaTopping> toppingDetails = new HashMap<>();

    // 方便添加配料的工具方法
    public void addTopping(Topping topping, Integer quantity) {
        PizzaTopping pizzaTopping = new PizzaTopping(this, topping, quantity);
        toppingDetails.put(topping.getName(), pizzaTopping);
        topping.getPizzaToppings().add(pizzaTopping);
    }

    // getters & setters
}

步骤4:Topping类补充反向关联

@Entity
public class Topping {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "topping")
    private List<PizzaTopping> pizzaToppings = new ArrayList<>();

    // getters & setters
}

内容的提问来源于stack exchange,提问作者Arvis M

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 10:13:17