在带联接的分页JPA查询之后出现的冗余请求

后端开发 2026-07-09

在测试通过 OrderRepository 获取 Order 实体数据的各种方法时,我发现对带分页的JPA查询存在冗余请求。

@Component(OrderRepository.BEAN_NAME)
public interface OrderRepository extends JpaRepository<Order, Long> {
// public static final BEAN_NAME...

@Query(value = "select os from Order os left join fetch os.items osit left join fetch os.issues osis where os.user.id =:userId",countQuery = "select count(os) from Order os where os.user.id =:userId")
Page<Order> findByUserIdWithPagination(
        @Param("userId") Long userId,
        Pageable pageable);
}

生成的统计数据显示,对一个与测试中的仓库无关、附属于 User 实体的 Inquiry 实体,进行了N 次直接向数据库的请求,其中N 是与给定的 User 相关的查询数量。

@Entity
public class User {

    @Id
    Long id;

    String name;

    @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true)
    Set<Order> orders = new HashSet<>();

    @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true)
    Set<Inquiry> inquiries = new HashSet<>();

}

有没有办法防止这种情况?使用不带分页的JPA语法的标准方法不会产生这种荒诞的现象。

@Entity
public class Order {

    @Id
    Long id;

    String name;

    @ManyToOne
    @JoinColumn(name = "USER_ID")
    User user;

    @OneToMany(mappedBy = "order", cascade = CascadeType.REMOVE, orphanRemoval = true)
    @BatchSize(size = 10)
    Set<Item> items = new HashSet<>();

    @OneToMany(mappedBy = "order", cascade = CascadeType.REMOVE, orphanRemoval = true)
    Set<Issue> issues;
}

@Entity
public class Inquiry {

    @Id
    Long id;

    String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "USER_ID")
    User user;

    @OneToMany(mappedBy = "inquiry", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY)
    Set<Item> items = new HashSet<>();

    @OneToMany(mappedBy = "inquiry", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.LAZY)
    Set<Issue> issues = new HashSet<>();
}

在把一个 User 实体附加到每个 Order 实体时,会添加一个 User 实体,并以EAGER模式检索到的11个 Inquiry 实体。

2026-05-29 08:37:15.382 DEBUG 21276 --- [           main] org.hibernate.SQL                        : select order0_.id as...

2026-05-29 08:37:15.392 DEBUG 21276 --- [           main] org.hibernate.SQL                        : select user0_.id as ...

026-05-29 08:37:15.393 DEBUG 21276 --- [           main] org.hibernate.SQL                        : select inquiry0_.id as ........ (x 11)

2026-05-29 08:05:52.778  INFO 20540 --- [           main] i.StatisticalLoggingSessionEventListener : Session Metrics {
    344513 nanoseconds spent acquiring 1 JDBC connections;
    0 nanoseconds spent releasing 0 JDBC connections;
    1316376 nanoseconds spent preparing 13 JDBC statements;
    3183940 nanoseconds spent executing 13 JDBC statements;
    0 nanoseconds spent executing 0 JDBC batches;
    0 nanoseconds spent performing 0 L2C puts;
    0 nanoseconds spent performing 0 L2C hits;
    0 nanoseconds spent performing 0 L2C misses;
    0 nanoseconds spent executing 0 flushes (flushing a total of 0 entities and 0 collections);
    20064 nanoseconds spent executing 1 partial-flushes (flushing a total of 0 entities and 0 collections)
}

我担心在带有联接的自定义查询中,如果不使用标准的JPA方法语法,就应该避免分页。

解决方案

The redundant query behavior you are experiencing is caused by combining `JOIN FETCH` with `Pageable` execution when returning a `Page<T>` in Spring Data JPA. 

To fulfill the `Page` contract, Spring Data JPA automatically generates and runs a secondary count query to calculate the total number of records. However, when Hibernate tries to translate your custom JPQL query into a count query, it often duplicates or mismanages the joins (especially collection fetches like `os.items` and related `User` / `Inquiry` associations), resulting in unnecessary extra queries or executing Cartesian products in memory.

Here are the two best approaches to eliminate these redundant requests:

### Solution 1: Provide a Custom Optimized `countQuery`
By explicitly separating the count logic, you force Spring Data JPA to evaluate total elements using a clean query that completely bypasses the heavy relationship fetches.

    @Query(
        value = "select os from Order os left join fetch os.items where os.user.id = :userId",
        countQuery = "select count(os) from Order os where os.user.id = :userId"
    )
    Page<Order> findByUserIdWithPagination(@Param("userId") Long userId, Pageable pageable);

### Solution 2: Switch the Return Type to `Slice<Order>`
If your application or API contract does not strictly need to know the grand total of pages (e.g., if you are building a mobile "infinite scroll" or a simple "Load More" pagination), change the return type from `Page` to `Slice`.

    @Query("select os from Order os left join fetch os.items where os.user.id = :userId")
    Slice<Order> findByUserIdWithPagination(@Param("userId") Long userId, Pageable pageable);

**Why this works:** A `Slice` completely disables the automated count query execution. It only requests `pageSize + 1` rows from the database to check if a next page exists, saving critical database roundtrips and avoiding redundant relationship initialization.
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章