Spring JPA Hibernate逐条按ID查询问题原因及优化咨询
这是Hibernate一对一共享主键关联场景下非常典型的N+1查询问题,触发逻辑非常明确:
- 你写的原生查询SQL只查询了
identifier_pool_definition(别名ipd)的全量字段,没有查询关联的identifier_definition表的字段,Hibernate在组装IdentifierPoolDefinitionEntity实体的时候,拿不到关联的IdentifierDefinitionEntity的数据。 - 你在
IdentifierPoolDefinitionEntity中配置的@OneToOne关联虽然显式设置了fetch = FetchType.LAZY,但这个配置对共享主键的一对一关联默认不生效:你没有给这个关联加optional = true配置,Hibernate会默认这个关联是必然存在、不能为空的,因此不会为关联对象生成可懒加载的代理类,必须在主查询结果组装阶段就拿到关联对象的完整数据,否则会抛出关联实体不存在的异常。 - 因为主查询没返回关联表数据,Hibernate只能针对每一条主查询返回的记录,单独发起一次按主键查询
identifier_definition的请求,最终形成1条主查询+N条关联查询的现象。
根据你的业务场景,可以选下面任意一种方案解决这个隐式调用问题:
方案1:修正关联配置,让懒加载真正生效
如果你大部分场景下查询IdentifierPoolDefinitionEntity都不需要用到关联的IdentifierDefinitionEntity数据,直接修改实体类的关联注解,强制Hibernate为关联对象生成懒加载代理:@JsonIgnore // 加上optional = true,告诉Hibernate这个关联可能为空 @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = true) // 显式指定使用代理实现懒加载 @LazyToOne(LazyToOneOption.PROXY) @PrimaryKeyJoinColumn private IdentifierDefinitionEntity identifierDefinitionEntity;配置完成后,只要业务代码不主动调用
getIdentifierDefinitionEntity()方法访问关联对象属性,Hibernate就不会发起额外的单条查询。注意这个方案需要引入org.hibernate.annotations.LazyToOne注解,是Hibernate的原生扩展注解。方案2:修改原生SQL,一次性查出两个表的全量字段
你当前的原生SQL本来就已经和identifier_definition做了关联,不需要额外加表关联,只需要把查询列从ipd.*改成两个表的字段全查即可,Hibernate拿到两个表的全量字段后会直接组装好两个实体,不会再发额外查询:@Query(value = "SELECT ipd.*, id.* FROM identifier_pool_definition ipd, identifier_definition id WHERE\n" + "ipd.definition_id = id.definition_id AND id.acquirer_id = :acquirerId AND" + " id.domain = :domain AND id.definition_type = 'pool' AND id.status IN :statuses AND id.type = :poolType AND id.is_deleted = false", nativeQuery = true) List<IdentifierPoolDefinitionEntity> findAllWithPoolTypeAndStatuses(@Param("acquirerId") String processorId, @Param("domain") String domain, @Param("poolType") String poolType, @Param("statuses") Collection<String> statuses);注意两个表存在
definition_id、created、updated、created_by、updated_by这类重名字段,如果出现字段映射错误,可以给重名字段加自定义别名,配合@ResultMap做显式映射即可。方案3:替换原生查询为JPQL,使用fetch join一次性加载关联
如果你没有必须使用原生SQL的强需求,这是最省心的方案:直接用JPQL的JOIN FETCH语法,Hibernate会自动生成连表查询语句,一次性加载主实体和关联实体的数据,完全规避N+1问题,不需要修改实体配置:@Query("SELECT ipd FROM IdentifierPoolDefinitionEntity ipd " + "JOIN FETCH ipd.identifierDefinitionEntity id " + "WHERE id.acquirerId = :acquirerId " + "AND id.domain = :domain " + "AND id.definitionType = 'pool' " + "AND id.status IN :statuses " + "AND id.type = :poolType " + "AND id.isDeleted = false") List<IdentifierPoolDefinitionEntity> findAllWithPoolTypeAndStatuses(@Param("acquirerId") String processorId, @Param("domain") String domain, @Param("poolType") String poolType, @Param("statuses") Collection<String> statuses);方案4:使用投影查询,不返回完整实体
如果这个查询场景下你压根不需要用到关联的IdentifierDefinitionEntity数据,直接放弃返回实体类,使用接口投影或者类DTO只接收你需要的identifier_pool_definition表字段即可,Hibernate组装返回结果的时候不会处理关联字段,自然不会触发额外查询。
比如定义投影接口:public interface PoolDefinitionProjection { UUID getDefinitionId(); String getPrefix(); String getSuffix(); String getFormatter(); Long getLowerBound(); Long getUpperBound(); String getSeparator(); // 只保留你业务需要的ipd表字段,不要添加关联对象的get方法 }将Repository方法的返回值修改为
List<PoolDefinitionProjection>即可,这种方案的查询性能是所有方案里最高的。
内容的提问来源于stack exchange,提问作者Nilesh Barai

