复合主键关联场景下Lazy Loading失效问题求助
看起来你遇到的核心问题是懒加载被意外触发,导致Ingredient的完整数据(包括name和循环引用的recipes)被加载出来了。这主要和复合主键的实现方式、序列化行为以及查询语句的写法有关,咱们一步步来解决:
1. 修复复合主键类的实现(关键!)
你当前的RecipeIngredientId直接持有Recipe和Ingredient实体引用,这会导致Hibernate在处理主键时,被迫初始化这些懒加载实体来获取ID值,进而触发非预期的EAGER加载。正确的做法是用基本数据类型的ID字段来定义复合主键:
@NoArgsConstructor @Getter @Setter public class RecipeIngredientId implements Serializable { private static final long serialVersionUID = 1L; // 用Long类型代替实体引用,对应数据库的主键列 private Long recipe; private Long ingredient; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; RecipeIngredientId that = (RecipeIngredientId) o; return Objects.equals(recipe, that.recipe) && Objects.equals(ingredient, that.ingredient); } @Override public int hashCode() { return Objects.hash(recipe, ingredient); } public RecipeIngredientId(Long recipeId, Long ingredientId) { this.recipe = recipeId; this.ingredient = ingredientId; } }
然后修改RecipeIngredient类,给关联字段加上@JoinColumn明确映射数据库列,避免Hibernate自动推断出错:
@Entity @Table(name="recipe_ingredients") @Getter @Setter @NoArgsConstructor @IdClass(RecipeIngredientId.class) public class RecipeIngredient implements Serializable { @Id @ManyToOne(fetch= FetchType.LAZY) @JoinColumn(name = "recipe_id") @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) private Recipe recipe; @Id @ManyToOne(fetch= FetchType.LAZY) @JoinColumn(name = "ingredient_id") @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) private Ingredient ingredient; private int amount; }
这样Hibernate就不需要初始化实体来获取主键,从根源上避免了不必要的懒加载触发。
2. 优化查询语句,精准控制加载内容
你的当前查询只是做了LEFT JOIN,但没有明确控制抓取的关联数据。这里提供两种方案:
方式一:使用DTO投影(推荐,最灵活)
创建一个只包含所需字段的DTO,完全避开实体序列化的问题:
public class RecipeIngredientDTO { private Long recipeId; private Long ingredientId; private int amount; public RecipeIngredientDTO(Long recipeId, Long ingredientId, int amount) { this.recipeId = recipeId; this.ingredientId = ingredientId; this.amount = amount; } // 省略getter方法 }
然后修改查询语句,直接查询需要的字段并转换为DTO:
TypedQuery<Object[]> query = em.createQuery( "SELECT r.id, ri.ingredient.id, ri.amount FROM Recipe r " + "LEFT JOIN r.ingredients ri WHERE r.id= :id", Object[].class ); query.setParameter("id", id); List<Object[]> results = query.getResultList(); // 组装Recipe和DTO集合 Recipe recipe = new Recipe(); recipe.setId(id); Set<RecipeIngredientDTO> ingredientDTOs = results.stream() .map(arr -> new RecipeIngredientDTO((Long)arr[0], (Long)arr[1], (int)arr[2])) .collect(Collectors.toSet()); // 可以给Recipe添加一个DTO集合的字段来存储结果
方式二:序列化层面控制字段输出
如果不想用DTO,可以在Ingredient类中对不需要序列化的字段添加@JsonIgnore,让Jackson忽略这些字段:
@Entity @Table(name="ingredients") @Getter @Setter @NoArgsConstructor @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class Ingredient implements Serializable { private static final long serialVersionUID = 1L; @Id private Long id; @Lob @JsonIgnore // 忽略name字段的序列化 private String name; @JsonIgnore // 忽略循环引用的recipes字段 @OneToMany(mappedBy="ingredient", fetch = FetchType.LAZY) private Set<RecipeIngredient> recipes = new HashSet<RecipeIngredient>(); }
注意这种方式是全局生效的,如果其他业务场景需要加载name字段,就需要调整策略,因此更推荐用DTO投影。
3. 验证懒加载效果
做完以上修改后,可以开启Hibernate的SQL日志,查看是否只有recipes和recipe_ingredients表的查询,没有触发ingredients表的额外查询(除非你主动调用Ingredient的非ID字段)。
内容的提问来源于stack exchange,提问作者Nikki

