Spring JPA分页查询关联集合时Hibernate内存分页问题咨询
实体定义
@Entity @NamedEntityGraph( name = "with-keys", attributeNodes = { @NamedAttributeNode(Substitution_.KEYS), } ) public class Substitution { @ManyToMany private Set<Key> keys; private boolean disabled; }
问题场景
调用仓库方法findAll(Pageable pageable)查询50条数据并由Jackson序列化时,Hibernate会为每个Substitution单独查询keys,产生1+50条SQL语句,出现N+1查询问题。
给findAll添加@EntityGraph(value = "with-keys")注解后,Hibernate抛出警告HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory,此时仍会把所有实体加载到内存中,哪怕已经通过关联查询获取了keys。
疑问
- 为什么要将所有实体加载到内存?这会造成巨大性能损耗,数据量过大时甚至无法正常运行;
- 为什么Spring JPA仓库不能先查询分页后的
Substitution,再通过如下SQL批量查询所有关联的keys并按ID映射?
select skr.subst_id, key.* from subst_key_relation skr join keys key on key.id = skr.key_id where skr.subst_id in :substitutionIdsInPage
关于“为何要将所有实体加载到内存”
这是JPA规范的限制导致的:当使用fetch join(包括实体图触发的集合关联抓取)配合分页参数(firstResult/maxResults)时,底层SQL会返回所有匹配的关联结果行——比如一条Substitution对应3个Key,就会返回3条记录。如果直接在SQL层面做分页,截断的是关联后的结果行,而非实际的Substitution实体数量,会导致最终返回的实体数量不符合分页要求(比如要50个实体,SQL分页可能只返回了30个实体的关联行)。
因此Hibernate必须先把所有关联结果行加载到内存,合并成完整的实体集合后,再在内存中执行分页逻辑,这就导致了所有实体被加载到内存的情况。
关于“为何Spring JPA仓库不采用先分页再批量查关联的方式”
Spring Data JPA是基于JPA规范实现的,而JPA规范本身并没有定义这种“先分页查主实体,再批量查关联”的默认行为。这种方式属于特定场景下的优化手段,需要额外的执行步骤:
- 先执行分页查询,获取当前页
Substitution的ID列表; - 通过ID列表批量查询对应的所有
Key关联数据; - 手动将
Key映射到对应的Substitution实体上。
这种优化需要明确的业务场景判断,JPA作为通用持久化规范不会默认介入这类特定逻辑。不过你可以通过自定义Repository方法来实现该优化:
- 先分页查询
Substitution的ID和基础字段; - 再批量查询关联的
Key; - 最后在代码中完成两者的关联。另外也可以使用Hibernate的
@BatchSize注解来优化N+1查询(该注解是Hibernate扩展,不属于JPA标准)。
内容的提问来源于stack exchange,提问作者Lorenz

