能否按父ID分组SQL JOIN结果并限制父级数据数量?
1. 能否按Parent.id分组?
先说明白:如果你的目标是避免查询结果里出现重复的Parent实体(毕竟LEFT JOIN符合条件的Child后,每个Child会对应一条Parent记录),那用GROUP BY Parent.id真不是最省心的办法——PostgreSQL要求SELECT里的非聚合字段必须出现在GROUP BY里,要是想同时拿Child数据,还得用array_agg这类聚合函数把Child字段打包成数组,这和Hibernate的实体映射完全不兼容,没法直接转成Parent和Child的实体对象。
更靠谱的方式是用Hibernate的DISTINCT_ROOT_ENTITY(对应Criteria里的query.distinct(true)),它会自动帮你去重Parent实体,同时保留关联的符合条件的Child列表。代码大概长这样:
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Parent> query = cb.createQuery(Parent.class); Root<Parent> parentRoot = query.from(Parent.class); Join<Parent, Child> childJoin = parentRoot.join("children", JoinType.LEFT); // 过滤子级年龄<10 query.where(cb.lessThan(childJoin.get("age"), 10)); // 去重父级实体 query.distinct(true); List<Parent> result = entityManager.createQuery(query).getResultList();
要是你真有特殊需求必须用GROUP BY(比如要做聚合统计),那SQL层面能写,但Hibernate没法直接映射成实体,得用原生SQL或者投影查询,比如:
SELECT p.id, array_agg(c.id) as child_ids, array_agg(c.age) as child_ages FROM parent p LEFT JOIN child c ON p.id = c.parent_id AND c.age < 10 GROUP BY p.id;
但这种方式拿到的是聚合后的数组,不是实体对象,对你的场景来说意义不大。
2. 能否用LIMIT限制返回的父级数量?
完全可以,但绝对不能直接在fetch join的查询后面加LIMIT——因为LEFT JOIN之后,一条Parent会对应多条Child记录,直接加LIMIT会截断JOIN后的结果集,导致返回的Parent数量远低于你预期(比如你想拿10个Parent,结果因为某个Parent有10个符合条件的Child,LIMIT 10就只返回这一个Parent)。
最稳妥的办法是先分页查出符合条件的Parent ID,再根据ID查询Parent及其关联的Child,用CriteriaQuery分两步走:
第一步:查符合条件的Parent ID(加LIMIT)
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Long> idQuery = cb.createQuery(Long.class); Root<Parent> parentRoot = idQuery.from(Parent.class); Join<Parent, Child> childJoin = parentRoot.join("children", JoinType.LEFT); idQuery.select(parentRoot.get("id")) .where(cb.lessThan(childJoin.get("age"), 10)) .distinct(true) // 去重ID,避免同一个Parent多次出现 .orderBy(cb.asc(parentRoot.get("id"))) // 可选,排序保证分页结果稳定 .setMaxResults(10); // 限制返回10个父级ID List<Long> parentIds = entityManager.createQuery(idQuery).getResultList();
第二步:根据ID查Parent和对应的Child
CriteriaQuery<Parent> parentQuery = cb.createQuery(Parent.class); Root<Parent> pRoot = parentQuery.from(Parent.class); Join<Parent, Child> cJoin = pRoot.join("children", JoinType.LEFT); parentQuery.where(pRoot.get("id").in(parentIds), cb.lessThan(cJoin.get("age"), 10)) .distinct(true); List<Parent> finalResult = entityManager.createQuery(parentQuery).getResultList();
另外,你也可以给Parent的children字段加@Fetch(FetchMode.SELECT)注解,这样Hibernate会先分页查Parent,再批量查每个Parent对应的Child,不过要注意N+1问题,配个batch_size参数就能缓解。
或者用子查询在一个CriteriaQuery里实现,代码大概是:
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Parent> query = cb.createQuery(Parent.class); Root<Parent> parentRoot = query.from(Parent.class); // 子查询拿符合条件的Parent ID,加LIMIT Subquery<Long> subquery = query.subquery(Long.class); Root<Parent> subParent = subquery.from(Parent.class); Join<Parent, Child> subChild = subParent.join("children", JoinType.LEFT); subquery.select(subParent.get("id")) .where(cb.lessThan(subChild.get("age"), 10)) .distinct(true) .setMaxResults(10); // 主查询关联子级 Join<Parent, Child> childJoin = parentRoot.join("children", JoinType.LEFT); query.where(parentRoot.get("id").in(subquery), cb.lessThan(childJoin.get("age"), 10)) .distinct(true); List<Parent> result = entityManager.createQuery(query).getResultList();
内容的提问来源于stack exchange,提问作者Marius Manastireanu

