Spring Data JPA 查询终极指南:四种利器,一套打通需要先看清适用场景和关键步骤,避免只记结论却忽略实际限制。
在 Spring Data JPA 的查询世界里,开发者常常面临一个经典难题:面对层出不穷的数据访问需求,究竟该选用哪种查询方式?是依赖方法命名派生查询的极致简洁,还是拥抱 @Query 的无限自由?是钟情于 Specification 的类型安全与动态组合,还是青睐 Query by Example 的天然直观?

下文会逐一拆解这四种查询利器,从基础用法到高阶技巧(含投影、更新、子查询、动态条件拼接),并深入剖析分页与排序的统一实践,辅以大量实战案例,给出清晰的选择策略。无论你是刚入门的初学者,还是寻求进阶的开发者,都能在这里找到通往高效数据访问的钥匙。
方法命名派生查询是 Spring Data JPA 最核心、最便捷的特性。你只需遵循一套约定好的命名规则,框架便会在运行时自动生成 JPQL 查询语句,真正实现“不写一行 SQL,也能查数据”。
一个派生查询方法由两部分组成,中间以 By 分隔:
find、read、get、count、exists、delete 等。By 之后的部分,由属性名和关键字组合而成。 复制代码List<User> findByName(String name); ─┬─ ──┬── 引导词 条件部分| 引导词 | 含义 | 示例 |
|---|---|---|
find…By | 返回匹配的实体或集合 | findByName(String name) |
read…By | 同 find | readByName(String name) |
get…By | 同 find | getByName(String name) |
count…By | 返回匹配结果的数量 | countByName(String name) |
exists…By | 判断是否存在匹配结果 | existsByName(String name) |
delete…By | 删除匹配的实体(先查后删) | deleteByName(String name) |
在引导词和 By 之间可插入 Top、First 或 Distinct,用于限制结果数量或去重:
复制代码// 年龄最大的前3条List<User> findTop3ByAge();// 年龄最大的第1条(等价于 Top1)User findFirstByAge();// 去重查询List<User> findDistinctByLastNameAndFirstName(String lastName, String firstName);下表罗列了 Spring Data JPA 支持的所有查询关键字及其对应的 JPQL 片段,掌握这些组合,便能覆盖绝大多数简单查询。
| 关键字 | 示例方法 | JPQL 片段 |
|---|---|---|
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is, Equals | findByFirstname、findByFirstnameIs | … where x.firstname = ?1 |
Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age <= ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull, Null | findByAgeIsNull | … where x.age is null |
IsNotNull, NotNull | findByAgeIsNotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1(参数追加 %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1(参数前置 %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1(参数前后加 %) |
NotContaining | findByFirstnameNotContaining | … where x.firstname not like ?1(参数前后加 %) |
In | findByAgeIn | … where x.age in ?1 |
NotIn | findByAgeNotIn | … where x.age not in ?1 |
True | findByActiveTrue | … where x.active = true |
False | findByActiveFalse | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstname) = UPPER(?1) |
OrderBy | findByAgeOrderByAgeDesc | … order by x.age desc |
条件部分可以引用嵌套属性,通过 . 或 _ 连接:
复制代码// 假设 User 有 Address 类型的 address 属性,Address 有 city 字段List<User> findByAddressCity(String city);Spring Data JPA 会自动生成 JOIN 查询。若属性名本身包含下划线,框架会优先识别为属性路径。
复制代码@Entity@Table(name = "users")public classUser {@Id @GeneratedValueprivate Integer id;private String name;private Integer age;private Boolean active;// getters/setters}public interfaceUserRepositoryextendsJpaRepository<User, Integer> {// 等值查询 List<User> findByName(String name);// 模糊匹配(包含) List<User> findByNameContaining(String infix);// 组合条件 + 排序 List<User> findByNameAndAgeOrderByAgeDesc(String name, int age);// 计数与存在判断long countByName(String name);boolean existsByName(String name);// 删除void deleteByName(String name);}方法命名虽好,但当方法名变得冗长、需要复杂关联或数据库特有函数时,就该改用 @Query 了。
@Query 注解允许你在 Repository 方法上直接编写查询语句,支持 JPQL 和 原生 SQL,是处理复杂查询的终极武器。
复制代码public interfaceUserRepositoryextendsJpaRepository<User, Long> {// JPQL(操作实体)@Query("SELECT u FROM User u WHERE u.age > ?1") List<User> findUsersByAgeGreaterThan(int age);// 原生 SQL(操作表)@Query(value = "SELECT * FROM users WHERE age > ?1", nativeQuery = true) List<User> findUsersByAgeGreaterThanNative(int age);}| 属性 | 说明 |
|---|---|
value | 查询语句(JPQL 或原生 SQL) |
nativeQuery | 是否为原生 SQL,默认 false |
countQuery | 分页时用于计数的查询语句(重要!) |
countProjection | 计数时使用的投影字段 |
复制代码@Query("SELECT u FROM User u WHERE u.name = ?1 AND u.age = ?2")User findByNameAndAge(String name, int age); 复制代码@Query("SELECT u FROM User u WHERE u.name = :name AND u.age = :age")User findByNameAndAge(@Param("name") String name, @Param("age")int age); 复制代码@Query("SELECT u FROM User u WHERE u.age IN :ages")List<User> findByAgeIn(@Param("ages") List<Integer> ages); 复制代码// 隐式 JOIN(通过属性路径)@Query("SELECT u FROM User u WHERE u.address.city = :city")List<User> findByCity(@Param("city") String city);// 显式 JOIN@Query("SELECT u FROM User u JOIN u.orders o WHERE o.status = :status")List<User> findUsersWithOrderStatus(@Param("status") OrderStatus status);// 左外连接@Query("SELECT u FROM User u LEFT JOIN u.orders o WHERE o.total > :amount")List<User> findUsersWithLargeOrders(@Param("amount") BigDecimal amount); 复制代码// 查询年龄大于平均值的用户@Query("SELECT u FROM User u WHERE u.age > (SELECT AVG(age) FROM User)")List<User> findUsersOlderThanAverage();// EXISTS 子查询@Query("SELECT u FROM User u WHERE EXISTS (SELECT 1 FROM Order o WHERE o.user = u AND o.total > :amount)")List<User> findUsersWithOrderAbove(@Param("amount") BigDecimal amount); 复制代码public interfaceUserNameAndAge { String getName();int getAge();}@Query("SELECT u.name AS name, u.age AS age FROM User u WHERE u.id = :id")UserNameAndAge findUserNameAndAgeById(@Param("id") Long id); 复制代码public classUserDTO {private String name;private int age;public UserDTO(String name, int age) { this.name = name; this.age = age; }// getters}@Query("SELECT new com.example.dto.UserDTO(u.name, u.age) FROM User u WHERE u.id = :id")UserDTO findUserDTOById(@Param("id") Long id); 复制代码@Query("SELECT u FROM User u WHERE u.name IN :names")List<User> findByNameIn(@Param("names") Collection<String> names);传入空集合时可能产生异常,建议调用前做非空校验。
复制代码// 引用实体名(多租户/动态表名)@Query("SELECT u FROM #{#entityName} u WHERE u.name = :name")List<User> findByEntityName(@Param("name") String name);// 动态排序(慎用于原生SQL)@Query("SELECT u FROM User u ORDER BY #{#sortField} #{#sortDirection}")List<User> findAllSorted(@Param("sortField") String field, @Param("sortDirection") String direction);@Query 可执行 UPDATE/DELETE,但必须与 @Modifying 联用。
复制代码@Modifying@Query("UPDATE User u SET u.active = false WHERE u.lastLoginDate < :cutoff")int deactivateInactiveUsers(@Param("cutoff") LocalDateTime cutoff);返回值为受影响行数。
更新后一级缓存可能残留旧数据,可通过 clearAutomatically 自动清理:
复制代码@Modifying(clearAutomatically = true)@Query("UPDATE User u SET u.active = false WHERE u.id = :id")int deactivateUser(@Param("id") Long id);若需要立即刷新缓存,可同时设置 flushAutomatically = true。
@Modifying 方法必须在事务中执行,通常在 Service 层用 @Transactional 包裹。
Object[],若映射为实体,需确保查询字段与实体字段一致。countQuery,否则分页可能失效(详见第五章分页专项)。@Param,写法与 JPQL 相同。 复制代码@Query("SELECT u FROM User u WHERE (:name IS NULL OR u.name = :name) AND (:age IS NULL OR u.age = :age)")List<User> searchUsers(@Param("name") String name, @Param("age") Integer age); 复制代码@Query("SELECT u, (u.age - (SELECT AVG(age) FROM User)) AS ageDiff FROM User u WHERE u.active = true ORDER BY ageDiff DESC")List<Object[]> findUsersWithAgeDifference(); 复制代码@Modifying(clearAutomatically = true)@Query("UPDATE Order o SET o.status = :newStatus WHERE o.status = :oldStatus AND o.createdDate < :cutoff")int batchUpdateOrderStatus(@Param("newStatus") OrderStatus newStatus,@Param("oldStatus") OrderStatus oldStatus,@Param("cutoff") LocalDateTime cutoff); 复制代码@Query("SELECT o FROM Order o WHERE FUNCTION('DATE', o.createdDate) = CURRENT_DATE")List<Order> findTodayOrders(); 复制代码@Query("SELECT u.department, COUNT(u), AVG(u.salary) FROM User u GROUP BY u.department HAVING AVG(u.salary) > :avg")List<Object[]> getDepartmentStats(@Param("avg") double avg);application.properties 开启: 复制代码spring.jpa.show-sql=truespring.jpa.properties.hibernate.format_sql=truelogging.level.org.hibernate.SQL=DEBUGlogging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACESELECT *,使用投影;为分页查询测试 countQuery 效率。当查询条件动态变化(如多字段组合搜索),且希望编译期类型安全时,Specification 是最佳搭档。它基于 JPA 2.0 的 Criteria API,将查询条件的构建以规范(Specification)对象的形式封装,并可自由组合。
让 Repository 同时继承 JpaSpecificationExecutor(JpaRepository 已包含):
复制代码public interfaceUserRepositoryextendsJpaRepository<User, Long>, JpaSpecificationExecutor<User> {}JpaSpecificationExecutor 提供了 findOne、findAll、count、exists 等方法,均接受 Specification 参数,且天然支持分页(传入 Pageable)。
Specification 是一个函数式接口,只需实现 toPredicate 方法:
复制代码@FunctionalInterfacepublic interfaceSpecification<T> { Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder);}示例:查询年龄大于 18 的用户。
复制代码Specification<User> ageGreaterThan18 = (root, query, builder) -> builder.greaterThan(root.get("age"), 18);List<User> users = userRepository.findAll(ageGreaterThan18);多条件组合:
复制代码Specification<User> spec = (root, query, builder) -> { Predicate ageBetween= builder.between(root.get("age"), 18, 30);Predicate isActive= builder.isTrue(root.get("active"));return builder.and(ageBetween, isActive);};List<User> users = userRepository.findAll(spec);将常用规范抽取为静态方法:
复制代码public classUserSpecs {public static Specification<User> ageGreaterThan(int age) {return (root, query, builder) -> builder.greaterThan(root.get("age"), age); } public static Specification<User> isActive() {return (root, query, builder) -> builder.isTrue(root.get("active")); } public static Specification<User> nameContains(String keyword) {return (root, query, builder) -> builder.like(root.get("name"), "%" + keyword + "%"); }}组合使用(AND / OR):
复制代码List<User> users = userRepository.findAll( Specification.where(UserSpecs.ageGreaterThan(18)) .and(UserSpecs.isActive()) .and(UserSpecs.nameContains("张")));使用字符串 "age" 存在字段改名导致运行时错误的风险。通过 JPA 静态元模型(如 Hibernate JPAModelGen 生成 User_ 类),可获得编译期检查:
复制代码Specification<User> ageGreaterThan18 = (root, query, builder) -> builder.greaterThan(root.get(User_.age), 18); // 编译期安全!启用方式:在 Maven/Gradle 中添加 org.hibernate:hibernate-jpamodelgen 注解处理器,编译后自动生成 User_ 类。
delete(Specification))按示例查询(QBE)提供了一种极简的动态查询方式:你只需填充一个样本实体对象,框架便会自动以其非空字段为条件生成查询,无需编写任何字段名。
复制代码// 创建探针(null 属性默认忽略)User probe=newUser();probe.setName("张三");probe.setAge(25);Example<User> example = Example.of(probe);List<User> results = userRepository.findAll(example);默认行为:精确匹配所有非空字段,字符串采用数据库默认匹配方式(通常为 =)。
复制代码ExampleMatcher matcher= ExampleMatcher.matching() .withIgnorePaths("id", "createdAt") // 忽略这些字段 .withStringMatcher(StringMatcher.CONTAINING) // 全局包含匹配 .withIgnoreCase() // 全局忽略大小写 .withMatcher("email", match -> match.endsWith()) .withMatcher("name", match -> match.startsWith().ignoreCase());Example<User> example = Example.of(probe, matcher);List<User> results = userRepository.findAll(example);字符串匹配策略:
| 策略 | 说明 | SQL 效果 |
|---|---|---|
DEFAULT | 存储特定默认值(通常精确匹配) | = ? |
EXACT | 精确匹配 | = ? |
STARTING | 前缀匹配 | LIKE ?% |
ENDING | 后缀匹配 | LIKE %? |
CONTAINING | 包含匹配 | LIKE %?% |
自 Spring Data JPA 2.5 起,支持链式调用进行排序、分页和投影:
复制代码User probe=newUser();probe.setActive(true);Example<User> example = Example.of(probe);List<User> results = userRepository.findBy(example, query -> query .sortBy(Sort.by("age").descending()) .page(PageRequest.of(0, 10)) .stream() .collect(Collectors.toList()));适用场景:
局限性:
(a=1 and b=2) or (c=3))分页是几乎所有业务系统都绕不开的需求。在 Spring Data JPA 中,无论你使用上述哪一种查询方式,分页机制都是高度统一的——核心依赖 Pageable 和 Page / Slice 接口。下面我们先了解基础,再逐一展示四种方式如何具体落地。
Pageable 的常用实现类,通过 PageRequest.of(page, size, sort) 创建。Page,适用于无限滚动等场景。 复制代码// 创建分页请求:第0页,每页10条,按年龄降序Pageable pageable= PageRequest.of(0, 10, Sort.by("age").descending());// 多字段排序Pageable pageable= PageRequest.of(0, 10, Sort.by("age").descending().and(Sort.by("id").ascending()));派生查询方法只需在参数列表中增加 Pageable 参数,返回值改为 Page 或 Slice,框架便会自动解析并生成分页 SQL。
复制代码public interfaceUserRepositoryextendsJpaRepository<User, Long> {// 返回 Page(含总记录数) Page<User> findByNameContaining(String keyword, Pageable pageable);// 返回 Slice(不含总记录数,性能更高) Slice<User> findByAgeGreaterThan(int age, Pageable pageable);// 结合排序(Pageable 中已包含排序,无需单独加 OrderBy) Page<User> findByActiveTrue(Pageable pageable);}调用示例:
复制代码// 查询姓名包含"张"的用户,按年龄降序分页Pageable pageable= PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "age"));Page<User> page = userRepository.findByNameContaining("张", pageable);System.out.println("总记录数:" + page.getTotalElements());System.out.println("总页数:" + page.getTotalPages());System.out.println("当前页数据:" + page.getContent());@Query 对分页的支持极为灵活,JPQL 和原生 SQL 都适用。
直接在方法参数中加入 Pageable,无需在 JPQL 中写 OFFSET / LIMIT,框架会自动拼接:
复制代码@Query("SELECT u FROM User u WHERE u.age > :age")Page<User> findByAgeGreaterThan(@Param("age")int age, Pageable pageable);当 JPQL 查询包含多表 JOIN 或复杂条件时,自动生成的 count 查询可能效率低下,甚至因语法问题报错。此时应手动指定 countQuery:
复制代码@Query(value = "SELECT u FROM User u LEFT JOIN u.orders o WHERE o.total > :amount", countQuery = "SELECT COUNT(u) FROM User u WHERE EXISTS (SELECT 1 FROM Order o WHERE o.user = u AND o.total > :amount)")Page<User> findUsersWithLargeOrders(@Param("amount") BigDecimal amount, Pageable pageable);原生 SQL 的分页语句因数据库方言而异(如 MySQL 的 LIMIT、Oracle 的 ROWNUM),Spring Data JPA 会根据方言自动拼接,但 count 查询必须手工提供,否则分页会失效:
复制代码@Query(value = "SELECT * FROM users WHERE age > ?1 ORDER BY id", countQuery = "SELECT COUNT(*) FROM users WHERE age > ?1", nativeQuery = true)Page<User> findUsersByAgeGreaterThanNative(int age, Pageable pageable);Specification 与 Pageable 是天生的搭档,通过 JpaSpecificationExecutor 提供的方法可以直接传入分页参数:
复制代码public interfaceUserRepositoryextendsJpaRepository<User, Long>, JpaSpecificationExecutor<User> { // 无需额外定义方法,父接口已提供}调用示例:
复制代码// 构建动态条件Specification<User> spec = (root, query, builder) -> { Predicate agePredicate= builder.between(root.get("age"), 18, 30);Predicate activePredicate= builder.isTrue(root.get("active"));return builder.and(agePredicate, activePredicate);};// 分页 + 排序Pageable pageable= PageRequest.of(0, 10, Sort.by("age").descending());Page<User> page = userRepository.findAll(spec, pageable);结合静态元模型(类型安全):
复制代码Specification<User> spec = (root, query, builder) -> builder.greaterThan(root.get(User_.age), 18);Page<User> page = userRepository.findAll(spec, PageRequest.of(0, 10, Sort.by(User_.AGE).descending()));QueryByExampleExecutor 同样原生支持分页,使用方式与 Specification 极其相似:
复制代码public interfaceUserRepositoryextendsJpaRepository<User, Long> {// 继承自 QueryByExampleExecutor,无需额外定义}传统分页写法:
复制代码// 构建探针User probe=newUser();probe.setName("张");probe.setActive(true);// 构建匹配器ExampleMatcher matcher= ExampleMatcher.matching() .withStringMatcher(StringMatcher.CONTAINING) .withIgnorePaths("id", "createdAt");Example<User> example = Example.of(probe, matcher);// 分页查询Pageable pageable= PageRequest.of(0, 10, Sort.by("age").ascending());Page<User> page = userRepository.findAll(example, pageable);流式 API(FetchableFluentQuery)分页(Spring Data JPA 2.5+):
复制代码// 获取 ListList<User> results = userRepository.findBy(example, query -> query .sortBy(Sort.by("age").descending()) .page(PageRequest.of(0, 10)) .stream() .collect(Collectors.toList()));// 若需要完整 Page 对象Page<User> page = userRepository.findBy(example, query -> query .sortBy(Sort.by("age").descending()) .page(PageRequest.of(0, 10)));| 特性 | Page | Slice |
|---|---|---|
| 是否执行 count 查询 | 是(消耗性能) | 否(性能高) |
| 能否获取总记录数 | 可以 | 不可以 |
| 能否获取总页数 | 可以 | 不可以 |
| 适用场景 | 后台管理系统、需要显示总条数的表格 | 移动端列表、无限滚动、API 网关透传 |
page、size、sort 参数,转换为 Pageable 对象,避免在 Service 层硬编码。page 参数做合理性校验,防止恶意请求导致内存溢出。countQuery。Sort.by 静态工厂: 复制代码// 安全且优雅的排序构建Sort sort= Sort.by("age").descending() .and(Sort.by("id").ascending());现在,我们在之前的对比表中增加“分页支持”这一关键维度:
| 对比维度 | 方法命名派生 | @Query | Specification | QBE |
|---|---|---|---|---|
| 代码量 | ⭐⭐⭐⭐⭐ 最少 | ⭐⭐⭐ 中等 | ⭐⭐ 较多 | ⭐⭐⭐⭐ 较少 |
| 可读性 | 方法名即文档 | 需阅读 JPQL/SQL | 需理解 Criteria API | 需理解匹配器配置 |
| 动态性 | 固定条件 | 固定条件(除非拼装字符串) | 天然动态 | 天然动态 |
| 类型安全 | ️ 无编译检查 | ️ 无编译检查 | 配合元模型可编译检查 | ️ 无编译检查 |
| 灵活性 | 受关键字限制 | 最高(任意 JPQL/SQL) | 高(编程构建) | 受匹配器限制 |
| 复杂查询能力 | 弱(简单条件) | 极强(任意复杂度) | 强(可构建复杂条件) | 弱(单层简单条件) |
| 分页支持 | 原生支持 | 原生支持(可自定义 count) | 原生支持 | 原生支持 |
| 重构友好 | 需同步改方法名 | 需同步改 JPQL | 配合元模型自动感知 | 修改 setter 调用即可 |
| 分页场景 | 推荐方式 | 理由 |
|---|---|---|
| 单表简单条件分页(如根据名称模糊查询) | 方法命名派生 | 代码极简,无需额外 SQL |
| 单表动态多条件分页(如后台搜索表单) | Specification | 条件灵活组合,类型安全 |
| 多表关联分页(如用户+订单联合查询) | @Query + 自定义 count | 可精准控制 JOIN 和 count 查询,避免性能陷阱 |
| 动态条件且不想写复杂代码的原型开发 | QBE | 以样本对象驱动,上手快 |
| 需要调用数据库特有函数的分页(如全文检索) | @Query(原生 SQL) | 可编写数据库专属 SQL,不受 JPQL 限制 |
在实际项目中,这四种方式并非互斥,而是可以共存于同一个 Repository 中。例如:
复制代码public interfaceUserRepositoryextendsJpaRepository<User, Long>, JpaSpecificationExecutor<User> { // 简单查询 → 派生 List<User> findByName(String name);// 复杂关联 → @Query@Query("SELECT u FROM User u JOIN u.orders o WHERE o.total > :amount") List<User> findUsersWithOrderTotalGreaterThan(@Param("amount") BigDecimal amount);// 动态多条件 → Specification(由外部调用)// 由 Service 层构建 Specification 传入 findAll(Specification)// 快速原型 → QBE(由外部调用)// 由 Service 层构建 Example 传入 findAll(Example)}Spring Data JPA 的四种查询方式——方法命名派生、@Query、Specification 和 Query by Example——各有侧重,互为补充。方法命名派生用最少的代码搞定最常见的查询;@Query 让你在复杂场景下掌控全局,并精细优化分页性能;Specification 则以类型安全的方式编织动态条件;QBE 则用最自然的方式表达动态查询。
在分页能力上,它们殊途同归,均通过统一的 Pageable 机制实现了优雅的支持。选择何种方式,应综合考量查询复杂度、动态性需求、类型安全要求和性能敏感度:
@QuerySpecificationQBE掌握这四种武器,并根据实际场景灵活选择,你便能在 Spring Data JPA 的查询世界里游刃有余,既写出简洁优雅的代码,又能应对层出不穷的业务变化。希望这份涵盖了分页细节与进阶特性的完整指南,能切实帮助你在实际项目中做出最优决策,高效、高质量地完成数据访问层的构建。