Spring Data JPA 最大的卖点之一,就是它能根据方法名自动生成查询语句,比如 findByUserNameAndStatus 这种命名约定查询,简单场景下非常好用。但一旦遇到多表关联、子查询、聚合统计或者数据库方言相关的函数调用,方法命名就会变得冗长甚至无能为力。这时候 @Query 注解就派上用场了,它允许你直接在 Repository 接口方法上书写 JPQL 或者原生 SQL,把查询的控制权交还给开发者。

@Query 注解基础与 JPQL 写法
@Query 注解的核心属性其实很少,常用的就三个:value 用于书写查询语句,nativeQuery 标记是否为原生 SQL,name 用于引用预先定义在实体上的命名查询。先看一个最基础的 JPQL 例子:
public interface UserRepository extends JpaRepository<User, Long> {
// JPQL 查询,注意操作的是实体和实体属性,而不是表名和列名
@Query("select u from User u where u.email = :email")
User findByEmail(@Param("email") String email);
// 模糊查询 + 排序
@Query("select u from User u where u.userName like %:keyword% order by u.createTime desc")
List<User> searchByKeyword(@Param("keyword") String keyword);
}这里要特别强调一点,JPQL 是面向对象的查询语言,它操作的是实体类和实体属性。from User 中的 User 是 @Entity 注解标注的类名,u.createTime 是实体里的字段名,而不是数据库表名和列名。初学者最常犯的错误就是把表名写进 JPQL,结果启动时报“无法识别的标识符”。
参数绑定推荐使用命名参数(:param)配合 @Param 注解,而不是位置参数(?1、?2)。位置参数在后续维护中一旦调整顺序就会悄悄引入 Bug,而命名参数的可读性和重构安全性都好得多。另外,JPQL 中写 like 模糊匹配时,可以直接把百分号写在语句里(如上例的 %:keyword%),Spring 会自动处理参数拼接。
原生 SQL 的使用场景与写法
当查询涉及数据库特有函数(如 MySQL 的 date_format、Oracle 的分析函数)、复杂的多表 join、或者需要绕开 JPA 的实体映射直接操作部分字段时,就需要把 nativeQuery 设为 true:
public interface OrderRepository extends JpaRepository<Order, Long> {
// 原生 SQL,直接写表名和列名
@Query(value = "select o.* from t_order o " +
"join t_user u on o.user_id = u.id " +
"where u.status = :status and o.amount > :amount",
nativeQuery = true)
List<Order> findBigOrdersByUserStatus(@Param("status") Integer status,
@Param("amount") BigDecimal amount);
}原生 SQL 的返回值处理比 JPQL 灵活一些。如果查询的就是实体的全部字段,可以直接返回实体列表;如果只查部分字段,可以返回 List<Map<String, Object>>、Object 数组,或者通过接口投影返回 DTO。其中接口投影是比较推荐的方式:
public interface OrderStatsProjection {
Long getUserId();
BigDecimal getTotalAmount();
Integer getOrderCount();
}
@Query(value = "select user_id as userId, sum(amount) as totalAmount, " +
"count(*) as orderCount from t_order group by user_id",
nativeQuery = true)
List<OrderStatsProjection> statsGroupByUser();注意别名列要和接口方法名对应上(Spring Data 会自动做驼峰匹配,但显式写别名最稳妥)。原生 SQL 有一个知名的坑:分页时 Pageable 是支持的,但 Sort 中的属性会被直接拼接到 SQL 的 order by 后面,属性名必须写数据库列名而不是实体字段名,否则会直接报错。更严重的是,如果原生 SQL 是分页查询且使用了 Page 返回值,Spring 会额外生成一条 count 查询来统计总数,这条 count SQL 有时会把原 SQL 包一层 select count(*) from (...),个别复杂 SQL 会出现语法问题,此时建议自定义 countQuery 属性来规避。
更新删除与动态查询进阶技巧
写 DML 语句(update、delete)时,必须额外加上 @Modifying 注解,否则框架会把它当成 select 解析,启动即报错。同时别忘了在调用方开启事务,常见做法是在 Service 层方法上加 @Transactional:
public interface UserRepository extends JpaRepository<User, Long> {
@Modifying
@Query("update User u set u.status = :status where u.lastLoginTime < :deadline")
int batchUpdateStatus(@Param("status") Integer status,
@Param("deadline") LocalDateTime deadline);
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public int freezeInactiveUsers() {
return userRepository.batchUpdateStatus(0,
LocalDateTime.now().minusMonths(6));
}
}这里要注意,@Modifying 默认不会清空一级缓存,如果更新后紧接着查询同实体,可能读到旧数据。解决办法是设置 @Modifying(clearAutomatically = true, flushAutomatically = true),让框架在更新后清空并刷新持久化上下文。另外,JPQL 的 update 只能更新简单属性,不能操作关联集合,这类需求老老实实用实体加载后再修改更可靠。
对于排序、分页这类动态需求,@Query 同样可以和 Pageable 无缝整合。JPQL 中不需要写 order by,直接在方法上加 Pageable 参数,框架会自动追加;还可以用 SpEL 表达式 ?#{#pageable} 处理一些特殊场景。排序方向和字段在运行时由前端传入,拼接进 Pageable 是安全的,因为框架最终会解析成表达式对象而不是字符串拼接,不存在 SQL 注入风险。
最后给一个选型建议:单表或简单关联查询优先用方法命名约定;涉及多表字段聚合、报表统计时用 JPQL 加接口投影;只有碰到数据库方言函数、存储过程调用或者需要精确控制执行计划的场景,才切换到原生 SQL。把这三层工具用对位置,数据访问层的代码会清爽很多。