Spring Data JPA 动态查询:Specification、QueryDSL 与原生 SQL 对比

Spring Data JPA 动态查询:Specification、QueryDSL 与原生 SQL 对比

摘要 :在企业级应用中,动态查询(根据运行时条件组合查询条件)是高频需求。Spring Data JPA 提供了三种主流方案:JPA Criteria API(封装为 Specification)、QueryDSL 和原生 SQL(@Query)。本文从 API 设计、类型安全、性能表现、可维护性四个维度深度对比这三种方案,并给出生产环境选型建议。

关键词:Spring Data JPA、Specification、QueryDSL、Criteria API、动态查询、原生 SQL、JPQL、类型安全


一、动态查询的业务场景与挑战

1.1 典型动态查询场景

考虑一个电商订单管理后台,用户可以通过多个条件筛选订单:

  • 订单状态:PENDING、PAID、SHIPPED、CANCELLED(可选)
  • 下单时间范围:起始日期 - 结束日期(可选)
  • 用户 ID 或用户名(可选)
  • 订单金额范围:最小金额 - 最大金额(可选)
  • 商品名称模糊匹配(可选)
  • 排序方式:按时间、金额、状态(可选)
  • 分页:页码 + 每页大小

挑战在于:这些条件的组合是 2^6 = 64 种可能。如果为每种组合写单独的查询方法,代码将爆炸式增长。

1.2 动态查询的核心需求

需求 说明
条件组合 根据传入参数动态拼接 WHERE 子句
类型安全 编译期检查字段名和类型,避免运行时错误
性能可控 生成的 SQL 可优化,支持索引提示
可维护性 代码可读,易于重构和扩展
分页排序 原生支持 Pageable 和 Sort

二、方案一:JPA Criteria API + Specification

2.1 Specification 接口设计

Spring Data JPA 将 JPA Criteria API 封装为 Specification 接口:

java 复制代码
package org.springframework.data.jpa.domain;

import org.springframework.data.jpa.domain.Specification;

public interface Specification<T> extends Serializable {
    @Nullable
    Predicate toPredicate(Root<T> root, 
                         CriteriaQuery<?> query, 
                         CriteriaBuilder criteriaBuilder);
}

2.2 完整实现:订单动态查询

java 复制代码
package com.example.repository.spec;

import com.example.entity.Order;
import com.example.entity.OrderStatus;
import jakarta.persistence.criteria.*;
import org.springframework.data.jpa.domain.Specification;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

public class OrderSpecification {

    private OrderSpecification() {}

    public static Specification<Order> withStatus(OrderStatus status) {
        return (root, query, cb) -> {
            if (status == null) return null;
            return cb.equal(root.get("status"), status);
        };
    }

    public static Specification<Order> withDateRange(LocalDateTime start, LocalDateTime end) {
        return (root, query, cb) -> {
            if (start == null && end == null) return null;
            List<Predicate> predicates = new ArrayList<>();
            if (start != null) {
                predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), start));
            }
            if (end != null) {
                predicates.add(cb.lessThanOrEqualTo(root.get("createdAt"), end));
            }
            return cb.and(predicates.toArray(new Predicate[0]));
        };
    }

    public static Specification<Order> withAmountRange(BigDecimal min, BigDecimal max) {
        return (root, query, cb) -> {
            if (min == null && max == null) return null;
            List<Predicate> predicates = new ArrayList<>();
            Path<BigDecimal> amountPath = root.get("totalAmount");
            if (min != null) {
                predicates.add(cb.greaterThanOrEqualTo(amountPath, min));
            }
            if (max != null) {
                predicates.add(cb.lessThanOrEqualTo(amountPath, max));
            }
            return cb.and(predicates.toArray(new Predicate[0]));
        };
    }

    public static Specification<Order> withUserId(Long userId) {
        return (root, query, cb) -> {
            if (userId == null) return null;
            return cb.equal(root.get("userId"), userId);
        };
    }

    public static Specification<Order> withProductNameLike(String productName) {
        return (root, query, cb) -> {
            if (productName == null || productName.isBlank()) return null;
            // 需要 JOIN orderItems 表
            Join<Object, Object> itemsJoin = root.join("orderItems", JoinType.INNER);
            Join<Object, Object> productJoin = itemsJoin.join("product", JoinType.INNER);
            return cb.like(
                cb.lower(productJoin.get("name")), 
                "%" + productName.toLowerCase() + "%"
            );
        };
    }

    /**
     * 组合所有条件
     */
    public static Specification<Order> buildDynamicQuery(
            OrderStatus status,
            LocalDateTime startDate,
            LocalDateTime endDate,
            BigDecimal minAmount,
            BigDecimal maxAmount,
            Long userId,
            String productName) {
        
        return Specification.where(withStatus(status))
            .and(withDateRange(startDate, endDate))
            .and(withAmountRange(minAmount, maxAmount))
            .and(withUserId(userId))
            .and(withProductNameLike(productName));
    }
}

2.3 Repository 接口与使用

java 复制代码
package com.example.repository;

import com.example.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderRepository 
    extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {
}
java 复制代码
package com.example.service;

import com.example.dto.OrderSearchRequest;
import com.example.entity.Order;
import com.example.repository.OrderRepository;
import com.example.repository.spec.OrderSpecification;
import org.springframework.data.domain.*;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class OrderSearchService {

    private final OrderRepository orderRepository;

    public OrderSearchService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public Page<Order> searchOrders(OrderSearchRequest request, int page, int size) {
        Specification<Order> spec = OrderSpecification.buildDynamicQuery(
            request.getStatus(),
            request.getStartDate(),
            request.getEndDate(),
            request.getMinAmount(),
            request.getMaxAmount(),
            request.getUserId(),
            request.getProductName()
        );

        // 构建排序
        Sort sort = request.getSortBy() != null
            ? Sort.by(request.getSortDirection(), request.getSortBy())
            : Sort.by(Sort.Direction.DESC, "createdAt");

        Pageable pageable = PageRequest.of(page, size, sort);
        return orderRepository.findAll(spec, pageable);
    }

    public long countOrders(OrderSearchRequest request) {
        Specification<Order> spec = OrderSpecification.buildDynamicQuery(
            request.getStatus(),
            request.getStartDate(),
            request.getEndDate(),
            request.getMinAmount(),
            request.getMaxAmount(),
            request.getUserId(),
            request.getProductName()
        );
        return orderRepository.count(spec);
    }
}

2.4 Specification 的优缺点

维度 评价
类型安全 中等。字段名是字符串(root.get("status")),重构时容易遗漏
可读性 较差。Criteria API 代码冗长,Lambda 形式稍好但仍不如 SQL 直观
灵活性 高。完全控制 JPA Criteria,可处理复杂 JOIN、子查询、聚合
性能 依赖 JPA 实现(Hibernate)的 SQL 生成能力
学习成本 高。需要理解 Criteria API 的 Root、Path、Predicate、Join 概念

三、方案二:QueryDSL

3.1 QueryDSL 简介与配置

QueryDSL 通过 APT(Annotation Processing Tool)在编译期生成 Q 类 (如 QOrderQUser),实现完全类型安全的查询。

xml 复制代码
<dependencies>
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-jpa</artifactId>
        <classifier>jakarta</classifier>
        <version>5.1.0</version>
    </dependency>
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <version>5.1.0</version>
        <classifier>jakarta</classifier>
        <scope>provided</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>com.mysema.maven</groupId>
            <artifactId>apt-maven-plugin</artifactId>
            <version>1.1.3</version>
            <executions>
                <execution>
                    <goals>
                        <goal>process</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>target/generated-sources/java</outputDirectory>
                        <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

编译后生成的 Q 类(位于 target/generated-sources/java/com/example/entity/QOrder.java):

java 复制代码
package com.example.entity;

import com.querydsl.core.types.dsl.*;

public class QOrder extends EntityPathBase<Order> {
    public static final QOrder order = new QOrder("order");
    public final NumberPath<Long> id = createNumber("id", Long.class);
    public final EnumPath<OrderStatus> status = createEnum("status", OrderStatus.class);
    public final DateTimePath<java.time.LocalDateTime> createdAt = createDateTime("createdAt", java.time.LocalDateTime.class);
    public final NumberPath<java.math.BigDecimal> totalAmount = createNumber("totalAmount", java.math.BigDecimal.class);
    public final NumberPath<Long> userId = createNumber("userId", Long.class);
    public final ListPath<OrderItem, QOrderItem> orderItems = this.<OrderItem, QOrderItem>createList("orderItems", OrderItem.class, QOrderItem.class, PathInits.DIRECT2);
    // ...
}

3.2 Repository 基类与具体实现

java 复制代码
package com.example.repository;

import com.example.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;

public interface OrderQuerydslRepository 
    extends JpaRepository<Order, Long>, 
            QuerydslPredicateExecutor<Order>,
            OrderQuerydslCustom {
}
java 复制代码
package com.example.repository;

import com.example.dto.OrderSearchRequest;
import com.example.entity.Order;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface OrderQuerydslCustom {
    Page<Order> searchByQuerydsl(OrderSearchRequest request, Pageable pageable);
}
java 复制代码
package com.example.repository.impl;

import com.example.dto.OrderSearchRequest;
import com.example.entity.*;
import com.example.repository.OrderQuerydslCustom;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import org.springframework.data.domain.*;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class OrderQuerydslCustomImpl implements OrderQuerydslCustom {

    private final JPAQueryFactory queryFactory;

    public OrderQuerydslCustomImpl(EntityManager entityManager) {
        this.queryFactory = new JPAQueryFactory(entityManager);
    }

    @Override
    public Page<Order> searchByQuerydsl(OrderSearchRequest request, Pageable pageable) {
        QOrder qOrder = QOrder.order;
        QOrderItem qOrderItem = QOrderItem.orderItem;
        QProduct qProduct = QProduct.product;

        // 动态条件构建
        BooleanBuilder builder = new BooleanBuilder();
        
        if (request.getStatus() != null) {
            builder.and(qOrder.status.eq(request.getStatus()));
        }
        if (request.getStartDate() != null) {
            builder.and(qOrder.createdAt.goe(request.getStartDate()));
        }
        if (request.getEndDate() != null) {
            builder.and(qOrder.createdAt.loe(request.getEndDate()));
        }
        if (request.getMinAmount() != null) {
            builder.and(qOrder.totalAmount.goe(request.getMinAmount()));
        }
        if (request.getMaxAmount() != null) {
            builder.and(qOrder.totalAmount.loe(request.getMaxAmount()));
        }
        if (request.getUserId() != null) {
            builder.and(qOrder.userId.eq(request.getUserId()));
        }
        if (request.getProductName() != null && !request.getProductName().isBlank()) {
            builder.and(
                qOrder.orderItems.any().product.name
                    .containsIgnoreCase(request.getProductName())
            );
        }

        // 查询数据
        List<Order> results = queryFactory
            .selectFrom(qOrder)
            .leftJoin(qOrder.orderItems, qOrderItem).fetchJoin()
            .leftJoin(qOrderItem.product, qProduct).fetchJoin()
            .where(builder)
            .orderBy(toOrderSpecifiers(pageable.getSort(), qOrder))
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetch();

        // 查询总数
        Long total = queryFactory
            .select(qOrder.count())
            .from(qOrder)
            .where(builder)
            .fetchOne();

        return new PageImpl<>(results, pageable, total != null ? total : 0L);
    }

    private OrderSpecifier<?>[] toOrderSpecifiers(Sort sort, QOrder qOrder) {
        if (sort.isEmpty()) {
            return new OrderSpecifier[] { qOrder.createdAt.desc() };
        }

        return sort.stream()
            .map(order -> {
                OrderSpecifier<?> specifier;
                switch (order.getProperty()) {
                    case "createdAt" -> specifier = order.isAscending() 
                        ? qOrder.createdAt.asc() : qOrder.createdAt.desc();
                    case "totalAmount" -> specifier = order.isAscending() 
                        ? qOrder.totalAmount.asc() : qOrder.totalAmount.desc();
                    case "status" -> specifier = order.isAscending() 
                        ? qOrder.status.asc() : qOrder.status.desc();
                    default -> specifier = qOrder.createdAt.desc();
                }
                return specifier;
            })
            .toArray(OrderSpecifier[]::new);
    }
}

3.3 QueryDSL 的优缺点

维度 评价
类型安全 极高。字段引用通过 Q 类,IDE 自动补全,重构安全
可读性 好。接近 Java 链式 API,比 Criteria 直观
灵活性 高。支持 JPA 和原生 SQL 两种模式,可切换
性能 可控。支持 fetchJoin() 解决 N+1,支持投影查询减少字段
学习成本 中等。需要理解 Q 类生成机制和 QueryDSL 表达式
构建复杂度 较高。需要配置 APT 处理器,首次构建会生成源码

四、方案三:原生 SQL / JPQL + @Query

4.1 使用 JPQL 的 @Query 注解

java 复制代码
package com.example.repository;

import com.example.entity.Order;
import com.example.entity.OrderStatus;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

@Repository
public interface OrderNativeRepository extends JpaRepository<Order, Long> {

    /**
     * 使用 JPQL 的动态查询(需要手动处理可选参数)
     * 注意:JPQL 对 NULL 参数的 IS NULL 判断是运行时行为
     */
    @Query("""
        SELECT DISTINCT o FROM Order o
        LEFT JOIN FETCH o.orderItems oi
        LEFT JOIN FETCH oi.product p
        WHERE (:status IS NULL OR o.status = :status)
          AND (:startDate IS NULL OR o.createdAt >= :startDate)
          AND (:endDate IS NULL OR o.createdAt <= :endDate)
          AND (:minAmount IS NULL OR o.totalAmount >= :minAmount)
          AND (:maxAmount IS NULL OR o.totalAmount <= :maxAmount)
          AND (:userId IS NULL OR o.userId = :userId)
          AND (:productName IS NULL OR LOWER(p.name) LIKE LOWER(CONCAT('%', :productName, '%')))
        """)
    Page<Order> searchByJpql(
        @Param("status") OrderStatus status,
        @Param("startDate") LocalDateTime startDate,
        @Param("endDate") LocalDateTime endDate,
        @Param("minAmount") BigDecimal minAmount,
        @Param("maxAmount") BigDecimal maxAmount,
        @Param("userId") Long userId,
        @Param("productName") String productName,
        Pageable pageable
    );

    /**
     * 原生 SQL 查询(复杂报表场景)
     */
    @Query(value = """
        SELECT 
            o.status,
            COUNT(*) as order_count,
            SUM(o.total_amount) as total_revenue,
            AVG(o.total_amount) as avg_order_value
        FROM orders o
        WHERE o.created_at >= :startDate
          AND o.created_at <= :endDate
        GROUP BY o.status
        """, nativeQuery = true)
    List<OrderStatisticsProjection> getOrderStatistics(
        @Param("startDate") LocalDateTime startDate,
        @Param("endDate") LocalDateTime endDate
    );

    /**
     * 使用 EntityManager 的 Criteria 动态构建(混合方案)
     */
    @Query(value = """
        SELECT o.* FROM orders o
        WHERE 1=1
        AND (:status IS NULL OR o.status = CAST(:status AS VARCHAR))
        AND (:userId IS NULL OR o.user_id = :userId)
        ORDER BY :sortColumn :sortDirection
        LIMIT :limit OFFSET :offset
        """, nativeQuery = true)
    List<Order> searchByNativeSql(
        @Param("status") String status,
        @Param("userId") Long userId,
        @Param("sortColumn") String sortColumn,
        @Param("sortDirection") String sortDirection,
        @Param("limit") int limit,
        @Param("offset") int offset
    );
}

4.2 原生 SQL 动态查询的改进方案(使用 EntityManager)

当条件过于复杂时,纯 @Query 难以满足。可以结合 EntityManager 动态构建原生 SQL:

java 复制代码
package com.example.repository.impl;

import com.example.dto.OrderSearchRequest;
import com.example.entity.Order;
import jakarta.persistence.*;
import org.springframework.data.domain.*;
import org.springframework.stereotype.Repository;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Repository
public class OrderNativeSearchRepository {

    @PersistenceContext
    private EntityManager entityManager;

    public Page<Order> searchByDynamicSql(OrderSearchRequest request, Pageable pageable) {
        StringBuilder sql = new StringBuilder(
            "SELECT o.* FROM orders o " +
            "LEFT JOIN order_items oi ON o.id = oi.order_id " +
            "LEFT JOIN products p ON oi.product_id = p.id " +
            "WHERE 1=1 "
        );
        StringBuilder countSql = new StringBuilder(
            "SELECT COUNT(DISTINCT o.id) FROM orders o " +
            "LEFT JOIN order_items oi ON o.id = oi.order_id " +
            "LEFT JOIN products p ON oi.product_id = p.id " +
            "WHERE 1=1 "
        );

        Map<String, Object> params = new HashMap<>();

        if (request.getStatus() != null) {
            sql.append(" AND o.status = :status");
            countSql.append(" AND o.status = :status");
            params.put("status", request.getStatus().name());
        }
        if (request.getStartDate() != null) {
            sql.append(" AND o.created_at >= :startDate");
            countSql.append(" AND o.created_at >= :startDate");
            params.put("startDate", request.getStartDate());
        }
        if (request.getEndDate() != null) {
            sql.append(" AND o.created_at <= :endDate");
            countSql.append(" AND o.created_at <= :endDate");
            params.put("endDate", request.getEndDate());
        }
        if (request.getMinAmount() != null) {
            sql.append(" AND o.total_amount >= :minAmount");
            countSql.append(" AND o.total_amount >= :minAmount");
            params.put("minAmount", request.getMinAmount());
        }
        if (request.getMaxAmount() != null) {
            sql.append(" AND o.total_amount <= :maxAmount");
            countSql.append(" AND o.total_amount <= :maxAmount");
            params.put("maxAmount", request.getMaxAmount());
        }
        if (request.getUserId() != null) {
            sql.append(" AND o.user_id = :userId");
            countSql.append(" AND o.user_id = :userId");
            params.put("userId", request.getUserId());
        }
        if (request.getProductName() != null && !request.getProductName().isBlank()) {
            sql.append(" AND LOWER(p.name) LIKE LOWER(:productName)");
            countSql.append(" AND LOWER(p.name) LIKE LOWER(:productName)");
            params.put("productName", "%" + request.getProductName() + "%");
        }

        // 排序
        if (pageable.getSort().isSorted()) {
            sql.append(" ORDER BY ");
            pageable.getSort().forEach(order -> {
                sql.append("o.").append(order.getProperty())
                   .append(" ").append(order.getDirection())
                   .append(",");
            });
            sql.deleteCharAt(sql.length() - 1); // 移除最后一个逗号
        } else {
            sql.append(" ORDER BY o.created_at DESC");
        }

        // 分页
        sql.append(" LIMIT :limit OFFSET :offset");
        params.put("limit", pageable.getPageSize());
        params.put("offset", pageable.getOffset());

        // 执行查询
        Query query = entityManager.createNativeQuery(sql.toString(), Order.class);
        params.forEach(query::setParameter);
        List<Order> results = query.getResultList();

        // 执行计数
        Query countQuery = entityManager.createNativeQuery(countSql.toString());
        params.entrySet().stream()
            .filter(e -> !e.getKey().equals("limit") && !e.getKey().equals("offset"))
            .forEach(e -> countQuery.setParameter(e.getKey(), e.getValue()));
        Long total = ((Number) countQuery.getSingleResult()).longValue();

        return new PageImpl<>(results, pageable, total);
    }
}

4.3 原生 SQL 的优缺点

维度 评价
类型安全 低。字段名是字符串,SQL 语法错误在运行时暴露
可读性 极高(对熟悉 SQL 的人)。查询逻辑直接可见
灵活性 极高。可使用数据库特有语法(窗口函数、CTE、全文检索)
性能 可控。可手写最优 SQL,添加索引提示、查询提示
可移植性 低。绑定特定数据库方言
维护性 中等。SQL 字符串难以重构,但逻辑清晰

五、三方案综合对比与选型建议

5.1 多维度对比矩阵

维度 Specification (Criteria) QueryDSL 原生 SQL / JPQL
类型安全 ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
代码可读性 ⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
复杂查询支持 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
动态条件构建 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐(需手动拼接)
IDE 支持 ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
性能调优 ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
学习成本 ⭐⭐⭐⭐(高) ⭐⭐⭐ ⭐⭐
构建复杂度 ⭐⭐⭐ ⭐⭐⭐⭐(APT 配置) ⭐⭐
JPA 移植性 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐(原生 SQL 时)
分页/排序原生支持 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐(需手动处理)

5.2 场景化选型建议

java 复制代码
/**
 * 选型决策树:
 * 
 * 1. 是否需要数据库特有功能(窗口函数、CTE、全文检索)?
 *    → 是:使用原生 SQL(@Query nativeQuery = true)或 MyBatis
 *    
 * 2. 查询条件是否简单(< 5 个可选条件)?
 *    → 是:使用多个 JPA 派生查询方法(findByStatusAndDateBetween...)
 *    
 * 3. 是否追求类型安全和 IDE 自动补全?
 *    → 是:使用 QueryDSL(团队有足够技术储备)
 *    
 * 4. 是否需要零额外依赖、快速上手?
 *    → 是:使用 Specification(Spring Data JPA 原生支持)
 *    
 * 5. 是否需要复杂报表 / 聚合查询?
 *    → 使用原生 SQL + @Query 或 Spring JDBC Template
 */

5.3 生产环境混合策略

在实际项目中,建议采用 混合策略

java 复制代码
@Service
public class OrderService {

    private final OrderRepository orderRepository;           // Specification 标准查询
    private final OrderQuerydslRepository orderQuerydslRepo; // QueryDSL 复杂关联查询
    private final OrderNativeSearchRepository orderNativeRepo; // 原生 SQL 报表查询

    // 标准列表查询(条件组合,需要分页)
    public Page<Order> searchOrders(OrderSearchRequest request) {
        return orderRepository.findAll(
            OrderSpecification.buildDynamicQuery(request), 
            PageRequest.of(0, 20)
        );
    }

    // 复杂查询(多层 JOIN,需要 fetch join 优化)
    public Page<Order> searchWithComplexJoins(OrderSearchRequest request) {
        return orderQuerydslRepo.searchByQuerydsl(request, PageRequest.of(0, 20));
    }

    // 运营报表(聚合、分组、窗口函数)
    public List<OrderStatisticsProjection> getStatistics(LocalDateTime start, LocalDateTime end) {
        return orderNativeRepo.getOrderStatistics(start, end);
    }

    // 导出大数据量(流式查询,避免内存溢出)
    @Transactional(readOnly = true)
    public Stream<Order> exportOrders(OrderSearchRequest request) {
        // 使用原生 SQL + JDBC 流式结果集
        return orderNativeRepo.streamSearch(request);
    }
}

六、性能优化注意事项

6.1 N+1 问题与解决

方案 解决方式 代码示例
Specification FetchType.EAGER(不推荐)或手动 @EntityGraph root.fetch("orderItems", JoinType.LEFT)
QueryDSL fetchJoin() .leftJoin(qOrder.orderItems, qOrderItem).fetchJoin()
原生 SQL 手动 JOIN + 结果集映射 LEFT JOIN order_items oi ON o.id = oi.order_id

6.2 分页性能优化

大数据量分页的深分页问题:

java 复制代码
// 传统 OFFSET 在深分页时性能极差(MySQL 需要扫描 OFFSET + LIMIT 行)
SELECT * FROM orders ORDER BY id DESC LIMIT 10 OFFSET 100000;

// 优化方案:使用游标分页(Keyset Pagination)
SELECT * FROM orders 
WHERE id < :lastSeenId  
ORDER BY id DESC 
LIMIT 10;

总结

Spring Data JPA 的动态查询方案没有绝对的最优解,只有最适合当前场景的解法:

  • Specification(JPA Criteria):Spring 原生支持,无额外依赖,适合中等复杂度、追求框架纯粹性的项目。缺点是代码冗长、类型安全不够彻底。
  • QueryDSL:类型安全的最佳选择,IDE 友好,重构安全。适合团队技术储备充足、查询复杂度较高的项目。需要配置 APT 构建插件。
  • 原生 SQL / JPQL:性能调优的最强手段,可读性最高,适合复杂报表、数据库特有功能、或已有 DBA 优化 SQL 的场景。缺点是维护成本高、可移植性差。

生产建议

  1. 80% 的常规查询使用 Specification(利用 Spring Data 原生集成)
  2. 15% 的复杂关联查询使用 QueryDSL(享受类型安全和 fetchJoin)
  3. 5% 的报表 / 聚合查询使用 原生 SQL(手写最优执行计划)
  4. 无论哪种方案,深分页场景都优先使用 Keyset Pagination 替代 OFFSET
  5. 始终用 EXPLAIN 验证生成的 SQL,确保索引被正确使用