JPA实体多表关联咨询:@Embeddable类使用及关联异常排查
嗨,我来帮你一步步理清这个问题!首先你碰到的那个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>配置
- Spring Boot项目:要么把实体类放在主启动类的包/子包下,要么用
- 误用了普通类:如果你的Map值是个没有任何Hibernate注解的普通POJO,Hibernate根本不知道怎么映射它,要么把它改成实体/嵌入类,要么换用支持的类型(比如基本类型、String)
第二步:三张表关联获取Map结构Pizza数据
要不要用@Embeddable,取决于你的关联表有没有额外字段,分两种情况:
情况1:关联表只有外键(纯多对多)
假设你的三张表是:
pizza:id、nametopping:id、namepizza_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

