JPA调用findAll()触发大量子查询性能低下,如何优化?
问题本质
你遇到的是JPA默认懒加载机制引发的N+1查询问题:你定义的@OneToMany关联默认使用FetchType.LAZY加载策略,主查询仅返回Post字段,后续访问postTags、items、subItems等关联属性时,才会触发额外的单独查询,嵌套层级越多,额外查询量越大。同时你使用分页查询时触发的count查询也是额外开销来源。
优化方案
1. 使用Fetch Join一次性抓取全量关联
不管是用JPQL还是你现在用的Criteria API,都可以通过fetch指令主动抓取需要的关联属性,一次性将所有需要的数据从数据库查出,避免后续懒加载:
JPQL示例:
@Query("SELECT DISTINCT p FROM Post p " + "INNER JOIN FETCH p.postTags pt " + "INNER JOIN FETCH pt.tag t " + "LEFT JOIN FETCH pt.items i " + "LEFT JOIN FETCH i.subItems si " + "WHERE t.tagName IN :tags AND pt.somefield BETWEEN :start AND :end " + "ORDER BY pt.somefield DESC, pt.pubDate DESC") Page<Post> findAllWithAllAssociations(@Param("tags") List<String> tags, @Param("start") Date start, @Param("end") Date end, Pageable pageable);
Criteria API示例:
CriteriaQuery<Post> cq = cb.createQuery(Post.class); Root<Post> postRoot = cq.from(Post.class); // 主动fetch所有需要的关联 Fetch<Post, PostTag> postTagFetch = postRoot.fetch("postTags", JoinType.INNER); postTagFetch.fetch("tag", JoinType.INNER); Fetch<PostTag, Items> itemsFetch = postTagFetch.fetch("items", JoinType.LEFT); itemsFetch.fetch("subItems", JoinType.LEFT); // 后续加查询条件、排序即可
注意添加DISTINCT关键字,避免多表关联产生的笛卡尔积导致返回重复的Post对象,Hibernate会自动完成内存去重,不会在SQL中加无用的distinct。
2. 优化分页count查询
你日志中的第一条额外查询是分页时自动生成的count查询,优化方式:
- 如果你不需要精确的总条数、总页数,可将返回值从
Page改为Slice,Spring Data JPA就不会触发count查询,仅判断是否有下一页 - 如果必须要精确总条数,可自定义count查询,避免自动生成的count查询冗余关联
3. 使用Entity Graph简化关联抓取配置
如果你多个查询都需要抓取相同的关联,可使用JPA提供的@NamedEntityGraph统一配置:
@Entity @NamedEntityGraph( name = "Post.withAllAssociations", attributeNodes = { @NamedAttributeNode(value = "postTags", subgraph = "postTagGraph") }, subgraphs = { @NamedSubgraph(name = "postTagGraph", attributeNodes = { @NamedAttributeNode("tag"), @NamedAttributeNode(value = "items", subgraph = "itemsGraph") }), @NamedSubgraph(name = "itemsGraph", attributeNodes = { @NamedAttributeNode("subItems") }) } ) public class Post { // 原有属性不变 }
然后在Repository方法上添加注解即可生效:
@EntityGraph(value = "Post.withAllAssociations", type = EntityGraph.EntityGraphType.LOAD) Page<Post> findByPostTags_Tag_TagNameInAndPostTags_SomefieldBetween(List<String> tags, Date start, Date end, Pageable pageable);
4. 直接查询DTO避免实体加载开销
如果你最终只需要返回DTO,不需要操作实体的持久态,可直接用Projection或者原生SQL一次性查询需要的字段映射到DTO,完全跳过实体懒加载的问题,性能是所有方案中最优的。
特殊场景优化:避免过大笛卡尔积
如果你的关联集合数据量极大,多表join产生的笛卡尔积导致单次查询返回数据量过大,可使用@BatchSize注解做批量懒加载:
@OneToMany(mappedBy = "Post", cascade = CascadeType.ALL) @BatchSize(size = 50) // 批量加载50个Post的postTags,将N次查询合并为1次 private Collection<PostTag> postTags = new HashSet<PostTag>();
同理可给PostTag.items、Items.subItems都加上@BatchSize,虽然不能做到单次查询,但可以把N+1降低为(N/批量大小)+1,也能大幅提升性能,同时避免大笛卡尔积的风险。
内容的提问来源于stack exchange,提问作者Satscreate

