强制Hibernate子查询抓取集合,解决分页数据缺失问题
问题场景
现有如下Hibernate实体映射:
class Consulta{ @ManyToMany(fetch = FetchType.LAZY, targetEntity = Orden.class) @JoinTable(name = "cons_ords", joinColumns = { @JoinColumn(name = "con_id") }, inverseJoinColumns = { @JoinColumn(name = "ord_id")} ) @Fetch(FetchMode.SUBSELECT) private Set<Orden> ordenes; }
执行分页查询时,代码如下:
Criteria criteria = ...; criteria.createAlias("ordenes", "ordenes", JoinType.LEFT_OUTER_JOIN); criteria.setFetchSize(5); criteria.setMaxResults(5); return criteria.list();
问题现象
尽管已设置FetchType.LAZY和FetchMode.SUBSELECT,但Hibernate仍执行LEFT OUTER JOIN操作,导致查询结果出现重复行,转换为Java实体后因去重,最终返回的实体数量不足5条,分页功能异常。生成的SQL如下:
select this_.id, orden2_.id, ... from Consulta this_ left outer join cons_ords ordcons11_ on this_.ID = ordcons11_.con_id left outer join Orden orden2_ on ordcons11_.ord_id = orden2_.ID where ...
补充测试信息
编辑1:表结构与测试结果
表结构如下:
Consulta cons_ords Orden +----+ +-----------------+ +-----+ | ID | | CON_ID | ORD_ID | | ID | +----+ +-----------------+ +-----+ | 1 | | 1 | 101 | | 101 | | 2 | | 2 | 101 | | 102 | | 3 | | 2 | 102 | | 103 | | 4 | | 3 | 105 | | 104 | | 5 | | 5 | 103 | | 105 | | 6 | +--------+--------+ | 106 | +----+ | 107 | +-----+
移除JoinType.LEFT_OUTER_JOIN后,默认使用内连接,查询返回结果为(1,2,2,3,4),转换为实体后去重仅保留4条数据(5号数据缺失),仍不符合预期。
编辑2:临时方案与疑问
当前临时方案为仅当存在ordenes相关约束时,才创建左外连接:
if (restrictions.stream().anyMatch( c -> c instanceof InExpression && ((InExpression) c).toString().startsWith("ordenes."))) criteria.createAlias("ordenes", "ordenes", JoinType.LEFT_OUTER_JOIN);
该方案可临时解决问题,但存在隐患。核心疑问:无论设置何种连接类型,Hibernate是否应优先遵循LAZY抓取策略?
解决方案
方案1:分页查询仅查主实体,集合懒加载
放弃在分页查询中显式关联ordenes,仅查询Consulta主表,利用@Fetch(FetchMode.SUBSELECT)的批量懒加载特性加载集合。这样主查询仅返回不重复的主实体,分页准确,集合通过后续子查询批量加载。
代码示例:
// 仅查询主表,不关联集合 Criteria criteria = session.createCriteria(Consulta.class); // 添加必要的主表过滤条件 // criteria.add(...); criteria.setMaxResults(5); List<Consulta> consultas = criteria.list(); // 触发集合批量加载(SUBSELECT) for (Consulta consulta : consultas) { consulta.getOrdenes().size(); // 访问集合触发懒加载,Hibernate执行一条子查询加载所有关联Orden }
方案2:使用Distinct根实体去重(适合过滤场景)
若必须基于ordenes添加过滤条件,可通过DISTINCT_ROOT_ENTITY结果转换器在内存中去重,但需注意:数据库仍会返回重复行,setMaxResults是针对数据库返回的行数,因此若重复行数量超过分页大小,去重后仍可能出现实体不足的情况。
代码示例:
Criteria criteria = session.createCriteria(Consulta.class); criteria.createAlias("ordenes", "ordenes", JoinType.LEFT_OUTER_JOIN); // 添加ordenes相关过滤条件 // criteria.add(...); criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY); criteria.setMaxResults(5); return criteria.list();
方案3:分页与查询分离,基于ID子集查询
先查询符合条件的所有Consulta ID(去重后),再分页截取ID子集,最后根据ID查询主实体并加载集合,彻底保证分页的准确性。
代码示例:
// 1. 查询符合条件的所有Consulta ID(去重) Criteria idCriteria = session.createCriteria(Consulta.class); idCriteria.createAlias("ordenes", "ordenes", JoinType.LEFT_OUTER_JOIN); // 添加过滤条件 // idCriteria.add(...); idCriteria.setProjection(Projections.distinct(Projections.id())); List<Long> allIds = idCriteria.list(); // 2. 分页截取ID子集 int pageSize = 5; int pageNum = 1; int startIdx = (pageNum - 1) * pageSize; int endIdx = Math.min(startIdx + pageSize, allIds.size()); List<Long> pageIds = allIds.subList(startIdx, endIdx); // 3. 根据ID查询主实体 Criteria criteria = session.createCriteria(Consulta.class); criteria.add(Restrictions.in("id", pageIds)); List<Consulta> consultas = criteria.list();
关于@Fetch(FetchMode.SUBSELECT)不生效的原因
当你在Criteria中显式调用createAlias关联集合时,Hibernate会优先执行JOIN查询,忽略SUBSELECT抓取策略。SUBSELECT仅作用于懒加载集合的批量加载场景,而非显式关联的查询。只要显式关联了集合,Hibernate就会生成JOIN语句,与LAZY或SUBSELECT设置无关。
内容的提问来源于stack exchange,提问作者Alfredo Morales

