位置:首页 > Java > Spring Data JPA 查询实战:方法命名与 @Query 注解详解

Spring Data JPA 查询实战:方法命名与 @Query 注解详解

时间:2026-08-27  |  作者:宇宙开黑者  |  阅读:0

目录

  1. 一、方法命名派生查询(Derived Query Methods)—— 最优雅的“零代码”方案
  2. 1.1 命名结构解剖
  3. 1.2 结果集控制:Top / First / Distinct
  4. 1.3 查询条件关键字大全
  5. 1.4 属性表达式与嵌套属性
  6. 1.5 完整示例

前言

Spring Data JPA 提供了两种核心查询方式,旨在平衡开发效率与灵活性。方法命名派生查询通过约定优于配置的理念,让框架自动解析属性生成 SQL,实现“零代码”高效检索。开发者只需遵循 find 前缀与 By 关键字规则,即可轻松处理常规 CRUD 操作。同时,面对复杂业务逻辑,@Query 注解提供了突破命名约束的万能钥匙,支持 JPQL 原生语句定制。本文将深入剖析这两种方式的语法细节、关键字应用及适用场景,帮助开发者构建既优雅又强大的数据访问层。

Spring Data JPA 查询实战:方法命名与 @Query 注解详解 的核心流程信息图
Spring Data JPA 查询实战:方用简体中文信息图概括Spring Data JPA 查询实战:方的核心流程、关键规则与实践要点。

一、方法命名派生查询(Derived Query Methods)—— 最优雅的“零代码”方案

方法命名派生查询是 Spring Data JPA 最核心、最便捷的特性。你只需遵循一套约定好的命名规则,框架便会在运行时自动生成 JPQL 查询语句,真正实现“不写一行 SQL,也能查数据”。

一、方法命名派生查询(Derived Que 对应的技术说明图
一、方法命名派生查询(Derived Que概括一、方法命名派生查询(Derived Que的核心概念、关键要点与实践提示。

1.1 命名结构解剖

一个派生查询方法由两部分组成,中间以 By 分隔:

  • 引导词(Introducer):定义操作类型,如 find、read、get、count、exists、delete 等。
  • 条件部分(Criteria):By 之后的部分,由属性名和关键字组合而成。

这种机制极大地简化了数据访问层的代码编写。例如,若需根据用户名查找用户,只需定义 findByUsername 方法,Spring Data JPA 会自动解析“Username”为实体属性,并生成对应的 WHERE 子句。开发者无需关心底层 SQL 的具体语法细节,只需关注业务逻辑与实体模型的映射关系。这种约定优于配置的理念,不仅降低了学习成本,还显著提升了代码的可读性与维护性,是处理常规 CRUD 操作的首选方案。

1.2 结果集控制:Top / First / Distinct

在引导词和 By 之间可插入 Top、First 或 Distinct,用于限制结果数量或去重:

2 结果集控制:Top / First / 对应的技术说明图
2 结果集控制:Top / First /概括结果集控制:Top / First /的核心概念、关键要点与实践提示。

当业务场景仅需获取前几条记录时,使用 TopFirst 关键字配合数字后缀(如 findByStatusOrderByCreateTimeDesc Top 10)即可轻松实现。这不仅避免了在 Java 代码中手动截取 List 集合的性能损耗,还直接将限制逻辑下推至数据库层,提升了查询效率。此外,Distinct 关键字可用于去除结果集中的重复项,确保返回数据的唯一性,特别适用于一对多关联查询中避免数据冗余的场景。

1.3 查询条件关键字大全

下表罗列了 Spring Data JPA 支持的所有查询关键字及其对应的 JPQL 片段,掌握这些组合,便能覆盖绝大多数简单查询。

关键字的丰富性赋予了派生查询极大的灵活性。从简单的相等匹配(Equals)到范围查询(BetweenLessThan),再到模糊匹配(LikeContaining),乃至逻辑组合(AndOr),几乎涵盖了所有常见的数据过滤需求。值得注意的是,关键字对大小写敏感,且需严格遵循实体属性的命名规范。对于复杂的多级属性导航(如 findByAddressCity),框架会自动处理嵌套对象的属性访问。熟练运用这些关键字,开发者能够以极少的代码量实现复杂的数据检索逻辑,是构建高效数据访问层的关键技能。

1.4 属性表达式与嵌套属性

条件部分可以引用嵌套属性,通过 . 或 _ 连接:

// 假设 User 有 Address 类型的 address 属性,Address 有 city 字段
List findByAddressCity(String city);

Spring Data JPA 会自动生成 JOIN 查询。若属性名本身包含下划线,框架会优先识别为属性路径。

1.5 完整示例

@Entity
@Table(name = "users")
public class User {
    @Id @GeneratedValue private Integer id;
    private String name;
    private Integer age;
    private Boolean active;
    // getters/setters
}

public interface UserRepository extends JpaRepository {
    // 等值查询
    List findByName(String name);
    // 模糊匹配(包含)
    List findByNameContaining(String infix);
    // 组合条件 + 排序
    List findByNameAndAgeOrderByAgeDesc(String name, int age);
    // 计数与存在判断
    long countByName(String name);
    boolean existsByName(String name);
    // 删除
    void deleteByName(String name);
}

1.6 何时告别方法命名?

方法命名虽好,但当方法名变得冗长、需要复杂关联或数据库特有函数时,就该改用 @Query 了。

二、@Query 注解——突破命名约束的万能钥匙

@Query 注解允许你在 Repository 方法上直接编写查询语句,支持 JPQL 和原生 SQL,是处理复杂查询的终极武器。

2.1 基础语法与参数绑定

① 基本用法

public interface UserRepository extends JpaRepository {
    // JPQL(操作实体)
    @Query("SELECT u FROM User u WHERE u.age > 1")
    List findUsersByAgeGreaterThan(int age);
    
    // 原生 SQL(操作表)
    @Query(value = "SELECT * FROM users WHERE age > 1", nativeQuery = true)
    List findUsersByAgeGreaterThanNative(int age);
}
属性说明
value查询语句(JPQL 或原生 SQL)
nativeQuery是否为原生 SQL,默认 false
countQuery分页时用于计数的查询语句(重要!)
countProjection计数时使用的投影字段

② 参数绑定方式

  • 位置参数(索引从 1 开始):
@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);
  • 集合参数(自动展开 IN 子句):
@Query("SELECT u FROM User u WHERE u.age IN :ages")
List findByAgeIn(@Param("ages") List ages);

2.2 高级查询技巧

① 多表关联(JOIN)

// 隐式 JOIN(通过属性路径)
@Query("SELECT u FROM User u WHERE u.address.city = :city")
List findByCity(@Param("city") String city);

// 显式 JOIN
@Query("SELECT u FROM User u JOIN u.orders o WHERE o.status = :status")
List findUsersWithOrderStatus(@Param("status") OrderStatus status);

// 左外连接
@Query("SELECT u FROM User u LEFT JOIN u.orders o WHERE o.total > :amount")
List findUsersWithLargeOrders(@Param("amount") BigDecimal amount);

② 子查询

// 查询年龄大于平均值的用户
@Query("SELECT u FROM User u WHERE u.age > (SELECT AVG(age) FROM User)")
List findUsersOlderThanAverage();

// EXISTS 子查询
@Query("SELECT u FROM User u WHERE EXISTS (SELECT 1 FROM Order o WHERE o.user = u AND o.total > :amount)")
List findUsersWithOrderAbove(@Param("amount") BigDecimal amount);

③ 投影(Projection)—— 只查部分字段

  • 接口投影(推荐):
public interface UserNameAndAge {
    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);
  • DTO 类投影(使用构造器表达式):
public class UserDTO {
    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);

④ 集合表达式与动态 IN

@Query("SELECT u FROM User u WHERE u.name IN :names")
List findByNameIn(@Param("names") Collection names);

传入空集合时可能产生异常,建议调用前做非空校验。

⑤ 使用 SpEL 表达式

// 引用实体名(多租户/动态表名)
@Query("SELECT u FROM #{#entityName} u WHERE u.name = :name")
List findByEntityName(@Param("name") String name);

// 动态排序(慎用于原生SQL)
@Query("SELECT u FROM User u ORDER BY #{#sortField} #{#sortDirection}")
List findAllSorted(@Param("sortField") String field, @Param("sortDirection") String direction);

2.3 更新与删除(@Modifying)

@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 包裹。

2.4 原生 SQL 的特殊处理

  • 结果映射:默认返回 Object[],若映射为实体,需确保查询字段与实体字段一致。
  • 分页:必须同时指定 countQuery,否则分页可能失效(详见第五章分页专项)。
  • 命名参数:支持 @Param,写法与 JPQL 相同。

2.5 实战案例集锦

案例 1:可选参数(COALESCE 模拟动态条件)

@Query("SELECT u FROM User u WHERE (:name IS NULL OR u.name = :name) AND (:age IS NULL OR u.age = :age)")
List searchUsers(@Param("name") String name, @Param("age") Integer age);

大数据量下慎用,建议改用 Specification

案例 2:计算字段 + 排序

@Query("SELECT u, (u.age - (SELECT AVG(age) FROM User)) AS ageDiff FROM User u WHERE u.active = true ORDER BY ageDiff DESC")
List findUsersWithAgeDifference();

案例 3:批量更新状态

@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);

案例 4:调用数据库函数(如 DATE)

@Query("SELECT o FROM Order o WHERE FUNCTION('DATE', o.createdDate) = CURRENT_DATE")
List findTodayOrders();

案例 5:分组聚合

@Query("SELECT u.department, COUNT(u), AVG(u.salary) FROM User u GROUP BY u.department HAVING AVG(u.salary) > :avg")
List getDepartmentStats(@Param("avg") double avg);

2.6 调试与最佳实践

  • SQL 日志:在 application.properties 开启:
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

三、Specification——类型安全的动态查询工厂

当业务场景中的查询条件动态变化(例如多字段组合搜索),且开发者希望在编译期就确保类型安全时,Specification 是最佳的技术选型。该机制基于 JPA 2.0 标准的 Criteria API 构建,其核心思想是将复杂的查询条件封装为规范(Specification)对象。这种设计使得查询逻辑不再依赖于字符串拼接,而是以面向对象的方式呈现,并支持灵活的自由组合。

3.1 启用 Specification

为了使 Repository 支持动态查询,需让其同时继承 JpaSpecificationExecutor。需要注意的是,JpaRepository 接口中已经包含了相关基础定义,因此直接继承即可。

public interface UserRepository extends JpaRepository, 
                                        JpaSpecificationExecutor {
}

通过上述配置,JpaSpecificationExecutor 接口提供了 findOnefindAllcount 以及 exists 等关键方法。这些方法均接受 Specification 作为参数,并且天然支持分页功能,只需在调用时传入 Pageable 参数即可实现高效的分页查询。

3.2 编写与使用 Specification

在代码实现层面,Specification 被定义为一个函数式接口。开发者只需实现其中的 toPredicate 方法,即可定义具体的查询逻辑。

@FunctionalInterface
public interface Specification {
    Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder builder);
}

示例:以下代码展示了如何查询年龄大于 18 的用户记录。

Specification ageGreaterThan18 = (root, query, builder) -> 
    builder.greaterThan(root.get("age"), 18);

List users = userRepository.findAll(ageGreaterThan18);

多条件组合:在实际开发中,往往需要处理多个条件的逻辑关系,代码如下所示。

Specification 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 users = userRepository.findAll(spec);

3.3 复用与组合

为了提高代码的可维护性,建议将常用的查询规范抽取为静态方法,以便在不同模块间复用。

public class UserSpecs {
    public static Specification ageGreaterThan(int age) {
        return (root, query, builder) -> builder.greaterThan(root.get("age"), age);
    }
    public static Specification isActive() {
        return (root, query, builder) -> builder.isTrue(root.get("active"));
    }
    public static Specification nameContains(String keyword) {
        return (root, query, builder) -> 
            builder.like(root.get("name"), "%" + keyword + "%");
    }
}

此外,Specification 支持通过 AND 或 OR 逻辑进行组合使用,从而构建出极其复杂的动态查询语句。

List users = userRepository.findAll(
    Specification.where(UserSpecs.ageGreaterThan(18))
                  .and(UserSpecs.isActive())
                  .and(UserSpecs.nameContains("张"))
);

3.4 结合 JPA 静态元模型(终极类型安全)

使用字符串 "age" 存在字段改名导致运行时错误的风险。通过 JPA 静态元模型(如 Hibernate JPAModelGen 生成 User_ 类),可获得编译期检查:

启用方式:在 Maven/Gradle 中添加 org.hibernate:hibernate-jpamodelgen 注解处理器,编译后自动生成 User_ 类。

3.5 Specification 的适用场景

  • 后台管理系统多条件筛选(如用户列表搜索)
  • 动态报表查询
  • 需要复用和组合查询条件的场景
  • 执行基于条件的删除(delete(Specification)

四、Query by Example(QBE)—— 用样本对象驱动查询

按示例查询(QBE)提供了一种极简的动态查询方式:你只需填充一个样本实体对象,框架便会自动以其非空字段为条件生成查询,无需编写任何字段名。

4.1 核心概念

  • Probe(探针):填充了属性值的领域对象实例。
  • ExampleMatcher(匹配器):定义字段匹配规则(如字符串匹配模式、忽略大小写等)。
  • Example:Probe + ExampleMatcher 的组合。

4.2 基本用法

默认行为:精确匹配所有非空字段,字符串采用数据库默认匹配方式(通常为 =)。

4.3 ExampleMatcher:精细控制匹配规则

字符串匹配策略

4.4 FetchableFluentQuery(流式查询)

自 Spring Data JPA 2.5 起,支持链式调用进行排序、分页和投影:

4.5 QBE 的适用场景与局限性

适用场景

五、分页与排序全攻略:四种方式统一实践

分页是几乎所有业务系统都绕不开的需求。在 Spring Data JPA 中,无论你使用上述哪一种查询方式,分页机制都是高度统一的——核心依赖 PageablePage / Slice 接口。下面我们先了解基础,再逐一展示四种方式如何具体落地。

5.1 分页核心概念

  • Pageable:分页请求对象,封装了页码、每页大小、排序信息。
  • PageRequestPageable 的常用实现类,通过 PageRequest.of(page, size, sort) 创建。
  • Page:分页结果对象,包含数据列表、总记录数、总页数等完整分页信息(会额外执行 count 查询)。
  • Slice:分页结果切片,仅知道是否有下一页,不执行 count 查询,性能优于 Page,适用于无限滚动等场景。

5.2 方式一:方法命名派生查询 + 分页

派生查询方法只需在参数列表中增加 Pageable 参数,返回值改为 PageSlice,框架便会自动解析并生成分页 SQL。

调用示例

此方式适用于查询条件动态变化,且不想编写复杂代码的场景。同时,当领域对象频繁重构时,使用此方法可以减少字段变更带来的影响,非常适合快速原型开发。

然而,该方法也存在明显的局限性。首先,它不支持嵌套或分组条件(如 (a=1 and b=2) or (c=3))。其次,同一字段只能有一个过滤值,无法处理复杂的逻辑组合。此外,字符串匹配能力受数据库限制,且不支持集合类型属性的匹配。因此,在遇到复杂查询需求时,建议考虑其他更灵活的分页实现方式。

5.3 方式二:@Query 注解 + 分页

@Query 对分页的支持极为灵活,JPQL 和原生 SQL 都适用。

① JPQL 标准分页

直接在方法参数中加入 Pageable,无需在 JPQL 中写 OFFSET / LIMIT,框架会自动拼接:

@Query("SELECT u FROM User u WHERE u.age > :age")
Page findByAgeGreaterThan(@Param("age") int age, Pageable pageable);

② 自定义 count 查询(性能优化关键)

当 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 findUsersWithLargeOrders(@Param("amount") BigDecimal amount, Pageable pageable);

③ 原生 SQL 分页(必须指定 countQuery)

原生 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 findUsersByAgeGreaterThanNative(int age, Pageable pageable);

注意:原生 SQL 的 ORDER BY 必须写在主查询中,且如果 Pageable 中带有 Sort,二者可能冲突,建议统一在 Pageable 中控制排序,主查询中不写 ORDER BY。

5.4 方式三:Specification + 分页

Specification 与 Pageable 是天生的搭档,通过 JpaSpecificationExecutor 提供的方法可以直接传入分页参数:

public interface UserRepository extends JpaRepository, 
                                        JpaSpecificationExecutor {
    // 无需额外定义方法,父接口已提供
}

调用示例

// 构建动态条件
Specification 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 page = userRepository.findAll(spec, pageable);

结合静态元模型(类型安全)

Specification spec = (root, query, builder) -> 
    builder.greaterThan(root.get(User_.age), 18);

Page page = userRepository.findAll(spec, PageRequest.of(0, 10, Sort.by(User_.AGE).descending()));

5.5 方式四:Query by Example(QBE)+ 分页

QueryByExampleExecutor 同样原生支持分页,使用方式与 Specification 极其相似:

public interface UserRepository extends JpaRepository {
    // 继承自 QueryByExampleExecutor,无需额外定义
}

传统分页写法:

// 构建探针
User probe = new User();
probe.setName("张");
probe.setActive(true);

// 构建匹配器
ExampleMatcher matcher = ExampleMatcher.matching()
    .withStringMatcher(StringMatcher.CONTAINING)
    .withIgnorePaths("id", "createdAt");

Example example = Example.of(probe, matcher);

// 分页查询
Pageable pageable = PageRequest.of(0, 10, Sort.by("age").ascending());
Page page = userRepository.findAll(example, pageable);

流式 API(FetchableFluentQuery)分页(Spring Data JPA 2.5+):

// 获取 List
List results = userRepository.findBy(example, 
    query -> query
        .sortBy(Sort.by("age").descending())
        .page(PageRequest.of(0, 10))
        .stream()
        .collect(Collectors.toList())
);

// 若需要完整 Page 对象
Page page = userRepository.findBy(example, 
    query -> query
        .sortBy(Sort.by("age").descending())
        .page(PageRequest.of(0, 10))
);

5.6 Page 与 Slice 的选择策略

Page 和 Slice 均实现了 Pageable 接口,但核心区别在于是否执行 count 查询。Page 会额外执行一条 count 语句以获取总记录数,从而计算总页数,适用于需要展示总页数的场景;而 Slice 仅查询当前页数据,不执行 count 查询,性能更高,适用于无限滚动或只需判断“是否有下一页”的场景。若业务仅需遍历大量数据而无需知道总数,优先使用 Slice 以避免不必要的性能损耗。

5.7 分页最佳实践

  1. 统一封装分页请求:在 Controller 层接收 page、size、sort 参数,转换为 Pageable 对象,避免在 Service 层硬编码。
  2. 避免超大页码:对 page 参数做合理性校验,防止恶意请求导致内存溢出。
  3. count 查询优化:对于复杂 JOIN 查询,务必测试自动生成的 count SQL,必要时手动指定 countQuery。
  4. 排序字段白名单:若排序字段由前端传入,请做白名单校验,防止 SQL 注入(尤其是原生 SQL 场景)。
  5. 善用 Sort.by 静态工厂:
// 安全且优雅的排序构建
Sort sort = Sort.by("age").descending()
                .and(Sort.by("id").ascending());

六、四大查询方式终极对比与选择策略

现在,我们在之前的对比表中增加“分页支持”这一关键维度:

对比维度方法命名派生@QuerySpecificationQBE
代码量 最少 中等 较多 较少
可读性方法名即文档需阅读 JPQL/SQL需理解 Criteria API需理解匹配器配置
动态性 固定条件 固定条件(除非拼装字符串) 天然动态 天然动态
类型安全 无编译检查 无编译检查 配合元模型可编译检查 无编译检查
灵活性受关键字限制最高(任意 JPQL/SQL)高(编程构建)受匹配器限制
复杂查询能力弱(简单条件)极强(任意复杂度)强(可构建复杂条件)弱(单层简单条件)
分页支持 原生支持 原生支持(可自定义 count) 原生支持 原生支持
重构友好需同步改方法名需同步改 JPQL配合元模型自动感知修改 setter 调用即可

尽管四种方式都支持分页,但 @Query 在复杂分页场景(如多表 JOIN 的 count 优化)中拥有最强的控制力;而方法命名派生和 QBE 在简单分页场景中则更加便捷。

分页场景选择速查表

分页场景推荐方式理由
单表简单条件分页(如根据名称模糊查询)方法命名派生代码极简,无需额外 SQL
单表动态多条件分页(如后台搜索表单)Specification条件灵活组合,类型安全
多表关联分页(如用户+订单联合查询)@Query + 自定义 count可精准控制 JOIN 和 count 查询,避免性能陷阱
动态条件且不想写复杂代码的原型开发QBE以样本对象驱动,上手快
需要调用数据库特有函数的分页(如全文检索)@Query(原生 SQL)可编写数据库专属 SQL,不受 JPQL 限制

协同使用建议

在实际项目中,这四种方式并非互斥,而是可以共存于同一个 Repository 中。例如:

public interface UserRepository extends JpaRepository,
                                       JpaSpecificationExecutor {
    
    // 简单查询 → 派生
    List findByName(String name);
    
    // 复杂关联 → @Query
    @Query("SELECT u FROM User u JOIN u.orders o WHERE o.total > :amount")
    List findUsersWithOrderTotalGreaterThan(@Param("amount") BigDecimal amount);
    
    // 动态多条件 → Specification(由外部调用)
    // 由 Service 层构建 Specification 传入 findAll(Specification)
    
    // 快速原型 → QBE(由外部调用)
    // 由 Service 层构建 Example 传入 findAll(Example)
}

七、结语

Spring Data JPA 查询策略深度解析

Spring Data JPA 提供了四种核心查询方式:方法命名派生、@Query、Specification 以及 Query by Example(QBE)。这四种机制各有侧重且互为补充,共同构成了灵活的数据访问体系。方法命名派生通过约定优于配置的原则,以极简的代码实现了最常见查询场景的高效处理;@Query 注解则赋予开发者在复杂业务场景下对 SQL 逻辑的全局掌控力,并支持对分页性能进行精细化调优;Specification 接口以类型安全的方式,优雅地编织出动态且复杂的查询条件;而 QBE 则通过最自然的对象映射方式,简化了动态查询的表达过程。

分页机制与选型指南

在分页能力方面,这四种方式殊途同归,均通过统一的 Pageable 机制实现了优雅且一致的支持。在实际开发中,选择何种查询方式应综合考量查询复杂度、动态性需求、类型安全要求以及性能敏感度。具体选型建议如下:

  • 简单固定条件:优先选择方法命名派生查询,代码简洁且易于维护。
  • 复杂固定逻辑:推荐使用 @Query,可精确控制 SQL 执行细节。
  • 动态组合条件:适用 Specification,利用其类型安全特性构建灵活查询。
  • 快速动态样本:采用 QBE,以最自然的方式表达动态查询意图。

掌握这四种“武器”,并根据实际业务场景灵活选择,你便能在 Spring Data JPA 的查询世界中游刃有余。这不仅能帮助你写出简洁优雅的代码,更能从容应对层出不穷的业务变化。希望这份涵盖分页细节与进阶特性的完整指南,能切实辅助你在实际项目中做出最优决策,从而高效、高质量地完成数据访问层的构建工作。

List findByName(String name);
           ─┬─   ──┬──
          引导词  条件部分
引导词含义示例
find…By返回匹配的实体或集合findByName(String name)
read…By同 findreadByName(String name)
get…By同 findgetByName(String name)
count…By返回匹配结果的数量countByName(String name)
exists…By判断是否存在匹配结果existsByName(String name)
delete…By删除匹配的实体(先查后删)deleteByName(String name)
// 年龄最大的前3条
List findTop3ByAge();

// 年龄最大的第1条(等价于 Top1)
User findFirstByAge();

// 去重查询
List findDistinctByLastNameAndFirstName(String lastName, String firstName);
关键字示例方法JPQL 片段
AndfindByLastnameAndFirstname… where x.lastname = 1 and x.firstname = 2
OrfindByLastnameOrFirstname… where x.lastname = 1 or x.firstname = 2
Is, EqualsfindByFirstname、findByFirstnameIs… where x.firstname = 1
BetweenfindByStartDateBetween… where x.startDate between 1 and 2
LessThanfindByAgeLessThan… where x.age < 1
LessThanEqualfindByAgeLessThanEqual… where x.age <= 1
GreaterThanfindByAgeGreaterThan… where x.age > 1
GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= 1
AfterfindByStartDateAfter… where x.startDate > 1
BeforefindByStartDateBefore… where x.startDate < 1
IsNull, NullfindByAgeIsNull… where x.age is null
IsNotNull, NotNullfindByAgeIsNotNull… where x.age not null
LikefindByFirstnameLike… where x.firstname like 1
NotLikefindByFirstnameNotLike… where x.firstname not like 1
StartingWithfindByFirstnameStartingWith… where x.firstname like 1(参数追加 %)
EndingWithfindByFirstnameEndingWith… where x.firstname like 1(参数前置 %)
ContainingfindByFirstnameContaining… where x.firstname like 1(参数前后加 %)
NotContainingfindByFirstnameNotContaining… where x.firstname not like 1(参数前后加 %)
InfindByAgeIn… where x.age in 1
NotInfindByAgeNotIn… where x.age not in 1
TruefindByActiveTrue… where x.active = true
FalsefindByActiveFalse… where x.active = false
IgnoreCasefindByFirstnameIgnoreCase… where UPPER(x.firstname) = UPPER(1)
OrderByfindByAgeOrderByAgeDesc… order by x.age desc
Specification ageGreaterThan18 = (root, query, builder) -> 
    builder.greaterThan(root.get(User_.age), 18); // 编译期安全!
// 创建探针(null 属性默认忽略)
User probe = new User();
probe.setName("张三");
probe.setAge(25);

Example example = Example.of(probe);
List 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 example = Example.of(probe, matcher);
List results = userRepository.findAll(example);
策略说明SQL 效果
DEFAULT存储特定默认值(通常精确匹配)=
EXACT精确匹配=
STARTING前缀匹配LIKE %
ENDING后缀匹配LIKE %
CONTAINING包含匹配LIKE %%
User probe = new User();
probe.setActive(true);

Example example = Example.of(probe);

List results = userRepository.findBy(example, 
    query -> query
        .sortBy(Sort.by("age").descending())
        .page(PageRequest.of(0, 10))
        .stream()
        .collect(Collectors.toList())
);
// 创建分页请求:第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()));
public interface UserRepository extends JpaRepository {

    // 返回 Page(含总记录数)
    Page findByNameContaining(String keyword, Pageable pageable);
    
    // 返回 Slice(不含总记录数,性能更高)
    Slice findByAgeGreaterThan(int age, Pageable pageable);
    
    // 结合排序(Pageable 中已包含排序,无需单独加 OrderBy)
    Page findByActiveTrue(Pageable pageable);
}
// 查询姓名包含"张"的用户,按年龄降序分页
Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "age"));
Page page = userRepository.findByNameContaining("张", pageable);

System.out.println("总记录数:" + page.getTotalElements());
System.out.println("总页数:" + page.getTotalPages());
System.out.println("当前页数据:" + page.getContent());
特性PageSlice
是否执行 count 查询 是(消耗性能) 否(性能高)
能否获取总记录数 可以 不可以
能否获取总页数 可以 不可以
适用场景后台管理系统、需要显示总条数的表格移动端列表、无限滚动、API 网关透传
@Query Specification Query by Example SELECT * countQuery

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多