使用Criteria API multiselect映射集合至视图的报错及解决方案咨询
问题描述
实体定义
@Entity public class Parent { @Id private Long id; @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY) private List<Child> children; @OneToOne private Foo foo; // getters and setters } @Entity public class Child { @Id private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") private Parent parent; // getters and setters } @Entity public class Foo { @Id private Long id; // .... }
目标视图类
public class ParentView { private Long parentId; private Long fooId; private List<Child> children; }
尝试的Criteria API代码
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<ParentView> query = cb.createQuery(ParentView.class); Root<Parent> parentRoot = query.from(Parent.class); // 注:此处代码存在错误,应为关联foo而非children Join<Parent, Foo> fooJoin = parentRoot.join("children", JoinType.LEFT); parentRoot.fetch("children"); query.multiselect( parentRoot.get("id"), fooJoin.get("id"), parentRoot.get("children") );
错误信息
query specified join fetching, but the owner of the fetched association was not present in the select list
即使将parentRoot加入multiselect作为第一个参数,错误依然存在。尝试过子查询、不使用fetch的普通关联等方案,均未解决问题。想了解Hibernate中是否有办法将查询结果映射至视图的集合字段,或存在可行的变通方案。
解决方案
方案一:构造函数映射 + 普通join(修正错误并避免笛卡尔积)
首先修正原代码中的关联错误:parentRoot.join("children")应改为parentRoot.join("foo"),因为要关联的是Foo实体而非children集合。
- 给
ParentView添加匹配的构造函数:
public ParentView(Long parentId, Long fooId, List<Child> children) { this.parentId = parentId; this.fooId = fooId; this.children = children; }
- 修改后的Criteria代码:
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<ParentView> query = cb.createQuery(ParentView.class); Root<Parent> parentRoot = query.from(Parent.class); // 关联Foo实体 Join<Parent, Foo> fooJoin = parentRoot.join("foo", JoinType.LEFT); // 启用去重,避免Parent-Child-Foo关联产生笛卡尔积导致重复结果 query.distinct(true); query.multiselect( parentRoot.get("id"), fooJoin.get("id"), parentRoot.get("children") ); List<ParentView> result = entityManager.createQuery(query).getResultList();
方案二:手动结果转换(适配旧版Hibernate)
如果使用Hibernate 5.2以下版本,可通过ResultTransformer或手动遍历结果集完成映射:
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Object[]> query = cb.createQuery(Object[].class); Root<Parent> parentRoot = query.from(Parent.class); Join<Parent, Foo> fooJoin = parentRoot.join("foo", JoinType.LEFT); // 使用fetch加载children集合 parentRoot.fetch("children", JoinType.LEFT); query.multiselect(parentRoot, fooJoin.get("id")); query.distinct(true); List<Object[]> results = entityManager.createQuery(query).getResultList(); // 手动转换为ParentView,避免重复 Map<Long, ParentView> viewMap = new HashMap<>(); for (Object[] row : results) { Parent parent = (Parent) row[0]; Long fooId = (Long) row[1]; ParentView view = viewMap.get(parent.getId()); if (view == null) { view = new ParentView(); view.setParentId(parent.getId()); view.setFooId(fooId); view.setChildren(parent.getChildren()); viewMap.put(parent.getId(), view); } } List<ParentView> finalResult = new ArrayList<>(viewMap.values());
方案三:子查询获取集合(避免笛卡尔积)
通过子查询单独获取每个Parent的children集合,无需处理重复结果:
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<ParentView> query = cb.createQuery(ParentView.class); Root<Parent> parentRoot = query.from(Parent.class); Join<Parent, Foo> fooJoin = parentRoot.join("foo", JoinType.LEFT); // 子查询获取当前Parent的所有children Subquery<List<Child>> childSubquery = query.subquery(List.class); Root<Child> childRoot = childSubquery.from(Child.class); childSubquery.select(cb.list(childRoot)) .where(cb.equal(childRoot.get("parent"), parentRoot)); query.multiselect( parentRoot.get("id"), fooJoin.get("id"), childSubquery ); List<ParentView> result = entityManager.createQuery(query).getResultList();
方案四:Spring Data JPA投影(若使用Spring生态)
如果项目基于Spring Data JPA,可直接通过JPQL构造函数投影简化实现:
- 确保
ParentView存在对应构造函数,然后在Repository中定义方法:
public interface ParentRepository extends JpaRepository<Parent, Long> { @Query("SELECT DISTINCT new com.yourpackage.ParentView(p.id, f.id, p.children) FROM Parent p LEFT JOIN p.foo f") List<ParentView> findAllParentViews(); }
关键注意点
- 原代码中关联对象错误是触发问题的直接原因之一,需确保关联的是正确的实体。
- 使用
fetch时,Hibernate要求关联的所有者(如Parent)必须作为顶层对象出现在select列表中,与multiselect的部分字段查询冲突,因此要么放弃fetch改用普通join,要么select整个所有者实体后再转换。 - 关联集合字段时,笛卡尔积是常见问题,需通过
distinct或子查询方式避免重复结果。
内容的提问来源于stack exchange,提问作者Alexey Usharovski
相关产品推荐
相关产品推荐

