1. 从零开始认识Query注解

Spring Data JPA的@Query注解就像是为JPA定制的GPS导航器。假设你正在开发电商平台订单管理系统,常规的findByXxx方法已经无法满足复杂的分页检索需求。此时@Query注解就派上了用场。

下面这个典型示例展示了如何查询最近七天内的订单数据:

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    
    // 使用JPQL查询指定时间范围内的订单
    @Query("SELECT o FROM Order o WHERE o.createTime BETWEEN :start AND :end")
    List<Order> findRecentOrders(@Param("start") LocalDateTime startTime, 
                                @Param("end") LocalDateTime endTime);
    
    // 强制刷新缓存的最新订单查询
    @Query(value = "SELECT * FROM orders WHERE status = 'PAID'", 
           nativeQuery = true)
    @Modifying(flushAutomatically = true)
    List<Order> findPaidOrdersNative();
}

这段代码充分展示了两种查询方式的差异:

  • 上半部分使用JPQL进行类型安全的对象查询
  • 下半部分通过原生SQL直接操作数据库表结构

2. 动态条件查询的终极形态

2.1 JPA Criteria API实践

当遇到包含5-10个可筛选条件的商品查询页面时,硬编码的查询语句会变成开发噩梦。来看这个基于Criteria API实现动态查询的示例:

public class ProductSpecification implements Specification<Product> {

    private ProductQueryCriteria criteria;

    @Override
    public Predicate toPredicate(Root<Product> root, 
                                CriteriaQuery<?> query, 
                                CriteriaBuilder builder) {
        List<Predicate> predicates = new ArrayList<>();
        
        // 价格区间筛选
        if(criteria.getMinPrice() != null && criteria.getMaxPrice() != null) {
            predicates.add(builder.between(root.get("price"), 
                criteria.getMinPrice(), criteria.getMaxPrice()));
        }
        
        // 关键字全文检索
        if(StringUtils.hasText(criteria.getKeyword())) {
            predicates.add(builder.or(
                builder.like(root.get("name"), "%" + criteria.getKeyword() + "%"),
                builder.like(root.get("description"), "%" + criteria.getKeyword() + "%")
            ));
        }
        
        return query.where(predicates.toArray(new Predicate[0]))
                   .orderBy(builder.desc(root.get("createTime")))
                   .getRestriction();
    }
}

2.2 QueryDSL的动态之美

基于QueryDSL的查询实现更加直观:

public List<Product> searchProducts(ProductQueryParam param) {
    QProduct product = QProduct.product;
    
    BooleanBuilder builder = new BooleanBuilder();
    
    // 分类过滤
    if(param.getCategoryId() != null) {
        builder.and(product.category.id.eq(param.getCategoryId()));
    }
    
    // 库存状态
    if(param.isInStock()) {
        builder.and(product.stock.gt(0));
    }
    
    // 排序组合
    return queryFactory.selectFrom(product)
                      .where(builder)
                      .orderBy(product.sales.desc(), 
                              product.createTime.asc())
                      .fetch();
}

这种链式语法能直观反应查询逻辑的组成方式

3. 实用场景分析

3.1 分页组合查询

在处理用户管理后台的分页列表时,典型查询需求包含:

  • 时间范围筛选
  • 状态过滤
  • 关键字搜索
  • 多重排序
@Query(value = "SELECT u FROM User u WHERE "
        + "(u.registerTime BETWEEN :start AND :end) "
        + "AND (:status IS NULL OR u.status = :status) "
        + "AND (u.username LIKE %:keyword% OR u.email LIKE %:keyword%)",
       countQuery = "SELECT count(u) FROM User u WHERE ...")
Page<User> searchUsers(@Param("start") Date start,
                      @Param("end") Date end,
                      @Param("status") UserStatus status,
                      @Param("keyword") String keyword,
                      Pageable pageable);

这里使用了countQuery参数确保分页统计的准确性

3.2 统计聚合场景

商品销量统计报表的典型实现:

public interface SalesReportRepository extends JpaRepository<Order, Long> {

    // 按分类分组统计销售数据
    @Query("SELECT new com.example.SalesSummary(c.name, SUM(o.amount)) "
          + "FROM Order o JOIN o.product p JOIN p.category c "
          + "WHERE o.status = 'COMPLETED' "
          + "GROUP BY c.id")
    List<SalesSummary> getCategorySales();
}

这里使用了构造函数表达式将查询结果直接转换为DTO对象

4. 技术选型指南

4.1 @Query注解的优缺点

优势特征:

  • 精确控制查询语句
  • 支持DTO投影
  • 编译时SQL校验
  • 参数灵活绑定

局限之处:

  • 动态条件拼接困难
  • 复杂查询可读性下降
  • 更新维护成本较高

4.2 动态查询方案对比

维度 Criteria API QueryDSL Specification
类型安全 ★★★★☆ ★★★★★ ★★★★☆
学习曲线 较陡峭 中等 平缓
调试友好度 一般 优秀 中等
功能扩展性 原生支持 依赖第三方 Spring整合

5. 专家级注意事项

  1. N+1问题防范: 在关联查询中,始终通过JOIN FETCH预先加载必要数据:
@Query("SELECT u FROM User u JOIN FETCH u.roles WHERE u.department = :dept")
List<User> findUsersWithRoles(@Param("dept") Department department);
  1. 参数绑定禁区: 绝对禁止拼接未校验的字符串参数:
// 错误示范:存在SQL注入风险
@Query("SELECT * FROM users WHERE name = '"+ "#{name}" + "'")
List<User> searchByName(@Param("name") String name);
  1. 性能优化手段: 对大数据量查询采用流式处理:
@QueryHints(value = @QueryHint(name = HINT_FETCH_SIZE, value = "100"))
@Query("SELECT u FROM User u WHERE u.status = :status")
Stream<User> streamActiveUsers(@Param("status") UserStatus status);

6. 架构师总结要点

通过对@Query注解的深入挖掘和动态查询技术的灵活搭配,我们找到了解决复杂业务查询的金钥匙。在实际项目开发中:

  • 简单查询优先使用Spring Data JPA的声明式方法
  • 中等复杂度业务选择@Query与JPQL组合
  • 动态查询场景采用QueryDSL或Specification模式
  • 性能关键操作考虑原生SQL优化

最终的方案选择应该像挑选瑞士军刀一样,根据具体的业务场景的切割需求,选择最合适的工具组合。