如何使用Hibernate Criteria投影至含List字段的POJO
解决Hibernate Criteria获取包含集合字段的BillEntity问题
你现在的问题是用Hibernate Criteria的投影(Projection)来获取包含paymentIds列表的BillEntity,但默认的投影只处理单值属性,集合字段需要特殊处理。下面给你几种可行的方案:
方案1:放弃投影,直接通过Fetch加载完整实体
如果你的主要目的是获取完整的BillEntity(包括集合字段),其实没必要用投影——只要你的实体映射配置正确(比如@ElementCollection或@OneToMany关联paymentIds),可以直接通过设置Fetch模式来加载集合:
DetachedCriteria criteria = DetachedCriteria.forClass(BillEntity.class); // 假设paymentIds是集合属性,设置LEFT JOIN来加载集合,避免N+1查询 criteria.setFetchMode("paymentIds", FetchMode.JOIN); // 如果有其他查询条件,在这里添加 // criteria.add(Restrictions.eq("xxx", value)); // 执行查询,直接得到包含paymentIds的BillEntity列表 List<BillEntity> bills = criteria.getExecutableCriteria(session).list();
这种方式最简单,Hibernate会自动帮你处理集合的加载,不需要手动处理投影和结果转换。
方案2:用SQL聚合函数+自定义ResultTransformer处理投影
如果你因为业务需求必须使用投影(比如有复杂的Join或字段筛选),可以通过数据库的聚合函数(比如MySQL的GROUP_CONCAT、PostgreSQL的STRING_AGG)把集合字段拼接成字符串,再通过自定义ResultTransformer把字符串转回List<Integer>:
示例代码(MySQL环境):
DetachedCriteria criteria = DetachedCriteria.forClass(BillEntity.class); // 左连接到paymentIds对应的关联表,别名设为payments criteria.createAlias("paymentIds", "payments", JoinType.LEFT_OUTER_JOIN); // 构建投影列表,注意要按Bill的id分组 ProjectionList projectionList = Projections.projectionList() .add(Projections.groupProperty("id"), "id") // 按id分组,确保每个Bill只返回一行 .add(Projections.groupProperty("amount"), "amount") .add(Projections.sqlProjection( "GROUP_CONCAT(DISTINCT payments.id SEPARATOR ',') as paymentIds", // 聚合paymentId为字符串 new String[]{"paymentIds"}, new Type[]{StandardBasicTypes.STRING} ), "paymentIds"); criteria.setProjection(projectionList); // 自定义ResultTransformer,把拼接的字符串转成List<Integer> criteria.setResultTransformer(new AliasToBeanResultTransformer(BillEntity.class) { @Override public Object transformTuple(Object[] tuple, String[] aliases) { BillEntity bill = (BillEntity) super.transformTuple(tuple, aliases); String paymentIdsStr = (String) tuple[2]; // 对应投影中paymentIds的位置 if (paymentIdsStr != null && !paymentIdsStr.isEmpty()) { // 分割字符串并转成Integer列表 List<Integer> paymentIds = Arrays.stream(paymentIdsStr.split(",")) .map(Integer::parseInt) .collect(Collectors.toList()); bill.setPaymentIds(paymentIds); } else { bill.setPaymentIds(new ArrayList<>()); // 空集合处理 } return bill; } }); // 执行查询得到结果 List<BillEntity> bills = criteria.getExecutableCriteria(session).list();
注意事项:
- 如果是PostgreSQL,把
GROUP_CONCAT换成STRING_AGG(payments.id, ',') - 必须用
groupProperty按Bill的主键分组,否则会返回重复的Bill行 - 要处理
paymentIds为空的情况,避免空指针异常
方案3:先投影单值,再手动合并集合结果
如果不想用SQL聚合函数,也可以先投影出每个Bill和对应的单个paymentId,再通过代码手动把同一个Bill的paymentId合并成列表:
步骤1:创建临时DTO类
public class BillDTO { private int id; private double amount; private Integer paymentId; // 构造函数、getter、setter省略 }
步骤2:投影并转换为DTO,再合并成BillEntity
DetachedCriteria criteria = DetachedCriteria.forClass(BillEntity.class); criteria.createAlias("paymentIds", "payments", JoinType.LEFT_OUTER_JOIN); ProjectionList projectionList = Projections.projectionList() .add(Projections.property("id"), "id") .add(Projections.property("amount"), "amount") .add(Projections.property("payments.id"), "paymentId"); criteria.setProjection(projectionList); // 用Transformers把结果转成BillDTO criteria.setResultTransformer(Transformers.aliasToBean(BillDTO.class)); List<BillDTO> dtoList = criteria.getExecutableCriteria(session).list(); // 手动合并DTO为BillEntity Map<Integer, BillEntity> billMap = new HashMap<>(); for (BillDTO dto : dtoList) { BillEntity bill = billMap.get(dto.getId()); if (bill == null) { bill = new BillEntity(); bill.setId(dto.getId()); bill.setAmount(dto.getAmount()); bill.setPaymentIds(new ArrayList<>()); billMap.put(dto.getId(), bill); } if (dto.getPaymentId() != null) { bill.getPaymentIds().add(dto.getPaymentId()); } } // 最终得到包含完整paymentIds的BillEntity列表 List<BillEntity> bills = new ArrayList<>(billMap.values());
这种方式不需要依赖数据库的聚合函数,兼容性更好,但需要额外写代码处理合并逻辑。
内容的提问来源于stack exchange,提问作者Vihar
相关产品推荐
相关产品推荐

