如何创建Criteria Query多列查询以聚合符合条件的学生列表?
JPA Criteria Multiselect 如何聚合关联实体列表到DTO
我有一个包含学生列表的Teacher实体,想通过JPA Criteria API编写multiselect查询,返回TeacherDTO对象——包含教师的姓名、课程,以及该教师名下分数≥15的学生姓名列表。DTO的构造方法如下:
public TeacherDTO(String name, String course, List<String> studentNames) { this.name = name; this.course = course; this.studentNames = studentNames; }
但当前的查询代码会为每个符合分数条件的学生生成一条重复的Teacher数据,而不是每个Teacher对应一个学生姓名列表。现有代码:
final Root<Teacher> root = criteriaQuery.from(Teacher.class); final Join<Teacher, Student> join = root.join(Teacher_.students, JoinType.LEFT); join.on(builder.isGreaterThanOrEquals(join.get(Student_.score), 15)); criteriaQuery.multiselect( root.get(Teacher_.name), root.get(Teacher_.course), join.get(Student_.name));
问题原因
直接通过LEFT JOIN关联Teacher和Student,再select学生姓名,会触发数据库返回笛卡尔积结果——每个符合分数条件的学生都会和所属Teacher组成一条记录,最终得到的是多条重复的Teacher数据,每条对应一个合格学生,而不是聚合后的学生列表。
正确实现方式
要实现聚合学生姓名到列表并映射到DTO,有两种常用思路:
思路1:使用子查询+数据库聚合函数
通过子查询查询当前Teacher下分数≥15的学生姓名,借助数据库聚合函数将姓名合并为列表,再映射到DTO的List<String>参数:
CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery<TeacherDTO> criteriaQuery = builder.createQuery(TeacherDTO.class); Root<Teacher> root = criteriaQuery.from(Teacher.class); // 子查询:聚合当前教师下合格学生的姓名列表 Subquery<List<String>> studentSubquery = criteriaQuery.subquery(List.class); Root<Student> studentRoot = studentSubquery.from(Student.class); studentSubquery.select(builder.function("ARRAY_AGG", List.class, studentRoot.get(Student_.name))) .where(builder.equal(studentRoot.get(Student_.teacher), root), builder.greaterThanOrEqualTo(studentRoot.get(Student_.score), 15)) .groupBy(studentRoot.get(Student_.teacher)); // 构造DTO查询 criteriaQuery.select(builder.construct(TeacherDTO.class, root.get(Teacher_.name), root.get(Teacher_.course), studentSubquery.getSelection())); // 执行查询 List<TeacherDTO> result = entityManager.createQuery(criteriaQuery).getResultList();
注意:ARRAY_AGG是PostgreSQL的聚合函数,不同数据库对应函数不同——比如MySQL用GROUP_CONCAT(返回字符串,需后续分割为列表),Oracle用LISTAGG。如果需要跨数据库兼容,可考虑使用Hibernate自定义函数或ORM框架提供的聚合工具。
思路2:分两次查询手动组装(兼容友好)
如果数据库聚合函数兼容性不好,也可以分两步查询:先获取教师基础信息,再批量获取对应合格学生列表,最后手动组装DTO:
// 1. 查询教师基础信息(可按需添加过滤条件) CriteriaQuery<Tuple> teacherQuery = builder.createQuery(Tuple.class); Root<Teacher> teacherRoot = teacherQuery.from(Teacher.class); teacherQuery.select(builder.tuple( teacherRoot.get(Teacher_.id), teacherRoot.get(Teacher_.name), teacherRoot.get(Teacher_.course))); List<Tuple> teacherTuples = entityManager.createQuery(teacherQuery).getResultList(); // 2. 批量获取所有合格学生,按教师ID分组 List<Long> teacherIds = teacherTuples.stream() .map(t -> (Long) t.get(0)) .collect(Collectors.toList()); CriteriaQuery<Tuple> studentQuery = builder.createQuery(Tuple.class); Root<Student> studentRoot = studentQuery.from(Student.class); studentQuery.select(builder.tuple( studentRoot.get(Student_.teacher).get(Teacher_.id), studentRoot.get(Student_.name))) .where(studentRoot.get(Student_.teacher).get(Teacher_.id).in(teacherIds), builder.greaterThanOrEqualTo(studentRoot.get(Student_.score), 15)); Map<Long, List<String>> teacherStudentMap = entityManager.createQuery(studentQuery).getResultList() .stream() .collect(Collectors.groupingBy( t -> (Long) t.get(0), Collectors.mapping(t -> (String) t.get(1), Collectors.toList()) )); // 3. 组装DTO List<TeacherDTO> result = teacherTuples.stream() .map(t -> { Long teacherId = (Long) t.get(0); String name = (String) t.get(1); String course = (String) t.get(2); List<String> studentNames = teacherStudentMap.getOrDefault(teacherId, Collections.emptyList()); return new TeacherDTO(name, course, studentNames); }) .collect(Collectors.toList());
这种方式虽然多一次查询,但逻辑清晰,无需依赖数据库特定函数,适合跨数据库场景。
额外提示
- 若使用Hibernate 5.2+,也可尝试自定义结果转换器简化聚合操作,但核心思路仍是通过聚合或批量查询避免笛卡尔积。
- 不要直接在原代码基础上GROUP BY教师所有属性,复杂实体场景下极易出错。
内容的提问来源于stack exchange,提问作者Di_
相关产品推荐
相关产品推荐

