Hibernate问题:@OneToMany关联查询返回重复结果
嘿,我刚看完你的问题,这其实是Hibernate里EAGER关联加载导致的笛卡尔积问题,刚好你提到子分类下有9个产品,和重复的次数完全对应,这就验证了我的判断!
问题原因
你的Category类里,children(子分类)和products两个集合都设置了fetch = FetchType.EAGER。当Hibernate加载父分类时,它会执行一个多表连接查询,把父分类、子分类、子分类的产品全部关联起来。这就会形成笛卡尔积:每个子分类会和它的每一个产品匹配一次,所以子分类就重复出现了等于产品数量的次数——也就是你看到的9次重复。
最佳实践解决方案
我不建议在查询后过滤重复数据,这不仅麻烦,还浪费性能。下面是几种更优雅的解决方式:
1. 优先使用懒加载(LAZY)——最推荐的方案
EAGER加载通常不是最佳实践,除非你确定每次加载Category时都必须用到它的所有子分类和产品。把两个集合的加载方式改成FetchType.LAZY,这样Hibernate只会在你实际调用getChildren()或getProducts()时才去加载对应的关联数据,从根源避免笛卡尔积。
修改Category.java的代码:
@OneToMany(mappedBy = "parentCategory", cascade = { CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY) private List<Category> children; @OneToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY) private List<Product> products;
注意:懒加载需要在事务范围内访问,否则会抛出LazyInitializationException。不过你的getCategory方法已经被@Transactional注解标记了,所以在方法内部访问children和products是完全没问题的。
2. 若必须使用EAGER,用@Fetch(FetchMode.SUBSELECT)避免连接查询
如果你因为业务需求必须用EAGER加载,可以给children集合添加@Fetch(FetchMode.SUBSELECT)注解。这样Hibernate会分两步查询:先查出父分类,再用子查询一次性加载所有关联的子分类,而不是做多表连接,自然就不会产生笛卡尔积了。
代码示例:
@OneToMany(mappedBy = "parentCategory", cascade = { CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.EAGER) @Fetch(FetchMode.SUBSELECT) private List<Category> children;
3. 自定义JPQL查询,指定Fetch Join
你也可以在Repository里写JPQL查询,明确只Fetch你需要的关联集合(注意:一次只能Fetch一个集合,否则还是会出现笛卡尔积)。比如先Fetch子分类,产品用懒加载处理:
修改CategoryRepository.java的getCategory方法:
@Override public Category getCategory(Long id) { if (id == null) { return null; } try { TypedQuery<Category> query = em.createQuery( "SELECT c FROM CATEGORIES c LEFT JOIN FETCH c.children WHERE c.id = :id", Category.class); query.setParameter("id", id); Category category = query.getSingleResult(); if(category != null){ // products是懒加载,这里访问时会触发查询,因为当前在事务内 category.setProducts(this.filterFlaggedOrHiddenUserProducts(category.getProducts())); } return category; } catch (IllegalArgumentException | NoResultException ex){ return null; } }
总结
尽量用懒加载(LAZY)是Hibernate关联查询的最佳实践,它既解决了重复数据的问题,也能提升应用性能——毕竟不需要每次加载分类都把所有子分类和产品一起查出来。如果特殊场景必须用EAGER,再考虑用@Fetch或者自定义JPQL的方式。
内容的提问来源于stack exchange,提问作者Mavelouscello

