You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JPA EntityManager createNativeQuery分页:如何避免两次查询?

我来帮你解决这个原生查询分页的问题——确实,复杂条件下用JPA原生查询做分页很容易遇到总条数的难题,下面给你两种实用的解决方案,还能避开你代码里的SQL注入风险:

解决方案1:复用条件逻辑,执行两次查询(兼容性最好)

核心思路是把查询的条件部分抽成可复用的方法,分别构建总条数统计查询和分页数据查询,这样既不用重复写一堆if-else条件,也能拿到正确的总条数来构建PageImpl。

步骤1:抽离条件构建逻辑

先把拼接WHERE条件的代码单独写成一个方法,同时用参数占位符代替直接拼接用户输入(避免SQL注入):

private String buildCarFilterWhereClause(String model, String manufacturedYear, List<Object> parameters) {
    StringBuilder whereClause = new StringBuilder(" WHERE c.model = ?1");
    parameters.add(model);
    int paramIndex = 2;
    
    if (manufacturedYear != null) {
        whereClause.append(" AND c.manufactured_year = ?").append(paramIndex);
        parameters.add(manufacturedYear);
        paramIndex++;
    }
    
    whereClause.append(" AND c.for_sale = true");
    return whereClause.toString();
}

步骤2:构建并执行两次查询

在业务方法里调用上面的方法,分别生成统计总条数的SQL和分页数据的SQL,然后执行:

private static final int NO_OF_RESULTS_IN_A_PAGE = 30;

public Page<Car> getFilteredCars(int page, String model, String manufacturedYear) {
    int pageSize = NO_OF_RESULTS_IN_A_PAGE;
    Pageable pageable = PageRequest.of(page, pageSize, Sort.by("addedDate").descending());
    
    // 存储查询参数,避免SQL注入
    List<Object> params = new ArrayList<>();
    String whereClause = buildCarFilterWhereClause(model, manufacturedYear, params);

    // 1. 执行总条数查询
    String countSql = "SELECT COUNT(*) FROM car c" + whereClause;
    Query countQuery = entityManager.createNativeQuery(countSql);
    for (int i = 0; i < params.size(); i++) {
        countQuery.setParameter(i + 1, params.get(i));
    }
    Long totalElements = ((Number) countQuery.getSingleResult()).longValue();

    // 2. 执行分页数据查询
    String dataSql = "SELECT c.* FROM car c" + whereClause + " ORDER BY c.added_date DESC";
    Query dataQuery = entityManager.createNativeQuery(dataSql, Car.class);
    // 设置过滤参数
    for (int i = 0; i < params.size(); i++) {
        dataQuery.setParameter(i + 1, params.get(i));
    }
    // 设置分页参数
    dataQuery.setFirstResult(page * pageSize);
    dataQuery.setMaxResults(pageSize);

    List<Car> content = dataQuery.getResultList();

    return new PageImpl<>(content, pageable, totalElements);
}

这种方式的好处是兼容所有数据库,而且逻辑清晰,适合复杂的多条件场景。

解决方案2:用数据库窗口函数,一次查询获取数据+总条数(效率更高)

如果你的数据库支持窗口函数(比如MySQL 8.0+、PostgreSQL、Oracle等),可以用COUNT(*) OVER()一次性查询出分页数据和总条数,只需要执行一次SQL。

实现示例

private static final int NO_OF_RESULTS_IN_A_PAGE = 30;

public Page<Car> getFilteredCars(int page, String model, String manufacturedYear) {
    int pageSize = NO_OF_RESULTS_IN_A_PAGE;
    Pageable pageable = PageRequest.of(page, pageSize, Sort.by("addedDate").descending());
    
    List<Object> params = new ArrayList<>();
    String whereClause = buildCarFilterWhereClause(model, manufacturedYear, params);

    // 用窗口函数COUNT(*) OVER()获取总条数
    String sql = "SELECT c.*, COUNT(*) OVER() AS total_count " +
                 "FROM car c" + whereClause + 
                 " ORDER BY c.added_date DESC " +
                 "LIMIT ? OFFSET ?";

    Query query = entityManager.createNativeQuery(sql);
    // 设置过滤参数
    for (int i = 0; i < params.size(); i++) {
        query.setParameter(i + 1, params.get(i));
    }
    // 设置分页参数
    query.setParameter(params.size() + 1, pageSize);
    query.setParameter(params.size() + 2, page * pageSize);

    List<Object[]> results = query.getResultList();
    List<Car> content = new ArrayList<>();
    Long totalElements = 0L;

    if (!results.isEmpty()) {
        // 提取总条数(所有结果的total_count都是一样的,取第一条即可)
        totalElements = ((Number) results.get(0)[results.get(0).length - 1]).longValue();
        
        // 将结果映射为Car对象
        for (Object[] result : results) {
            // 这里可以手动映射字段,或者用@SqlResultSetMapping简化映射
            Car car = new Car();
            car.setId(((Number) result[0]).longValue());
            car.setModel((String) result[1]);
            car.setManufacturedYear((String) result[2]);
            // 映射其他字段...
            content.add(car);
        }
    }

    return new PageImpl<>(content, pageable, totalElements);
}

注意事项

  • 这种方式只需要一次数据库查询,性能更优,但依赖数据库的窗口函数支持;
  • 结果映射需要处理Object[],可以用JPA的@SqlResultSetMapping注解来简化映射逻辑,避免手动赋值。

关键提醒:避免SQL注入

你原来的代码直接拼接model、manufacturedYear到SQL字符串里,存在严重的SQL注入风险!上面两种方案都用了参数绑定的方式,一定要替换掉直接拼接字符串的写法,保护系统安全。

内容的提问来源于stack exchange,提问作者Kshitij Bajracharya

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:45:42