Hibernate Criteria继承构造DTO时出现关联获取异常问题
问题排查与解决思路
我之前也碰到过类似的Hibernate Criteria查询DTO的坑,咱们来一步步理清问题根源和解决办法:
异常原因分析
这个org.hibernate.QueryException的核心问题在于DTO继承实体类的方式和Hibernate Criteria的投影逻辑不兼容,结合你的场景具体来说:
- 你的
InventoryRequestDTO继承了实体类InventoryRequest,而InventoryRequest又继承了带@MappedSuperclass的AuditEntity。但Hibernate并不会把继承实体的DTO当成“可映射的实体”来处理——当你用Criteria查询返回DTO时,必须明确指定要投影的字段,而不是依赖实体的继承关系自动映射。 - 你的
SpecificationBuilder会处理organization、created_by、modified_by这些属性,其中created_by/modified_by大概率是@ManyToOne这类关联属性。如果Specification里用了fetch join(比如root.fetch("createdBy"))来加载关联实体,Hibernate会要求“关联的所有者(也就是InventoryRequest主实体)必须在选择列表中”,但你现在查询的是DTO,没有把主实体本身加入投影列表,就触发了这个异常。 - 用原
InventoryRequest查询正常的原因是:它是Hibernate管理的实体类,Criteria返回实体时,主实体默认就在选择列表里,Hibernate可以自动处理关联的fetch逻辑,不会有冲突。
具体解决方法
方法1:改用独立DTO+显式投影(最稳妥)
放弃让DTO继承实体类,定义一个独立的InventoryRequestDTO,包含所有需要的字段(包括AuditEntity里的属性),然后在Criteria中手动指定投影字段,用ResultTransformer转换结果:
// 独立DTO示例 public class InventoryRequestDTO { private Long id; private String organization; private User createdBy; // 或者只保留需要的字段,比如String createdByUsername // 其他业务字段... // 全参/无参构造、getter/setter } // Criteria查询代码 CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<InventoryRequestDTO> cq = cb.createQuery(InventoryRequestDTO.class); Root<InventoryRequest> root = cq.from(InventoryRequest.class); // 构建投影列表,映射到DTO的字段 ProjectionList projections = Projections.projectionList() .add(Projections.property("id"), "id") .add(Projections.property("organization"), "organization") .add(Projections.property("createdBy"), "createdBy") // 添加上所有需要的业务字段 .add(Projections.property("requestQuantity"), "requestQuantity"); cq.select(projections); // 应用Specification条件 Specification<InventoryRequest> spec = auditSpecificationBuilder.build(); cq.where(spec.toPredicate(root, cq, cb)); // 设置分页 cq.setFirstResult(pageable.getPageNumber() * pageable.getPageSize()); cq.setMaxResults(pageable.getPageSize()); // 转换结果为DTO List<InventoryRequestDTO> results = entityManager.createQuery(cq) .setResultTransformer(Transformers.aliasToBean(InventoryRequestDTO.class)) .getResultList();
注意:DTO的字段名要和投影的别名完全一致,或者用@Alias注解指定映射关系。
方法2:调整Specification中的关联处理(如果坚持DTO继承实体)
如果一定要让DTO继承实体类,需要修改SpecificationBuilder中的关联逻辑:
- 把
fetch join改成普通join,避免Hibernate自动加载关联实体; - 手动投影关联实体的需要字段,而不是依赖fetch来加载整个关联对象。
比如把原来的:
root.fetch("createdBy", JoinType.LEFT);
改成:
Join<InventoryRequest, User> createdByJoin = root.join("createdBy", JoinType.LEFT); // 投影createdBy的具体字段到DTO对应的属性 projections.add(Projections.property(createdByJoin.get("username")), "createdByUsername");
这样就不会触发“关联所有者不在选择列表”的异常,因为你没有用fetch来强制加载关联实体,而是手动投影需要的字段。
方法3:使用Spring Data JPA Projection接口(最简洁,适用于Spring环境)
如果你的项目用了Spring Data JPA,可以定义一个Projection接口来替代DTO,Spring会自动处理投影逻辑:
// 定义Projection接口,包含所有需要的getter方法 public interface InventoryRequestProjection { Long getId(); String getOrganization(); // 可以直接返回关联对象,或者只取需要的字段 String getCreatedByUsername(); // 假设User实体有getUsername()方法 // 其他业务字段的getter... } // 在Repository中添加方法 public interface InventoryRequestRepository extends JpaRepository<InventoryRequest, Long>, JpaSpecificationExecutor<InventoryRequest> { Page<InventoryRequestProjection> findAll(Specification<InventoryRequest> spec, Pageable pageable); }
这种方式不需要自己写Criteria代码,Spring会自动构建正确的查询,避免手动处理投影的麻烦,也不会出现连接获取的异常。
内容的提问来源于stack exchange,提问作者TillTheDayIDie
相关产品推荐
相关产品推荐

