You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何创建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_

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 08:17:44