人人都会AI编程

13.4 Spring Data JPA:Repository 体系、查询方法、动态查询

更新时间:2026-07-11

Spring Data JPA 是 Spring Data 家族中最成熟、使用最广泛的成员。它并不是一个 JPA 实现,而是在 JPA 规范之上构建的一层抽象,旨在彻底消除数据访问层的样板代码。其核心思路可以总结为一句话:你定义接口,框架负责实现。开发者只需按照约定的规则声明接口方法,Spring Data JPA 会在运行时自动生成对应的代理实现,无需写一行 DAO 实现代码。

13.4.1 Repository 体系:从接口到自动实现

Spring Data JPA 的 Repository 体系由三个核心接口层层递进构成:

Repository (标记接口)
  └── CrudRepository (通用 CRUD)
        └── PagingAndSortingRepository (分页与排序)
              └── JpaRepository (JPA 专用扩展)

在实际项目中,我们通常直接继承 JpaRepository,因为它已经继承了前面所有父接口的能力,并额外提供了批量操作、刷新、持久化上下文同步等 JPA 特有方法。

1. 定义 Repository 接口

一个典型的实体和对应的 Repository 如下:

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
    private Integer age;
    private LocalDate birthday;
    // 省略 getter/setter
}

public interface UserRepository extends JpaRepository<User, Long> {
    // 只需定义接口,无需实现类
}

此时,UserRepository 已经自动拥有了以下能力:

  • save(User entity):保存或更新实体(有 ID 则更新,无 ID 则插入)
  • findById(Long id):根据主键查询
  • findAll():查询所有记录
  • delete(User entity):删除实体
  • count():统计总数
  • existsById(Long id):判断是否存在
  • 以及 saveAllfindAllByIddeleteAlldeleteByIdflush 等几十个方法

所有这些都是 Spring Data JPA 在运行时通过动态代理自动提供的,你完全不用写 SQL,甚至不需要写 JPA 的 JPQL。

2. Repository 的三种定义方式

根据灵活度和控制力,Spring Data JPA 支持三种 Repository 片段组合:

  • 继承官方接口:如 JpaRepository<T, ID>,适合多数场景。
  • 自定义方法片段:在接口中定义自定义方法名,框架解析生成。
  • 手动提供实现:当查询逻辑极其复杂时,编写一个独立的 UserRepositoryCustom 接口及其实现类 UserRepositoryCustomImpl,然后让 UserRepository 同时继承 JpaRepositoryUserRepositoryCustom。Spring Data 会自动将自定义实现的方法合并到最终代理中。
// 自定义接口
public interface UserRepositoryCustom {
    List<User> findUsersByComplexCondition(SearchCriteria criteria);
}

// 自定义实现(类名必须为 核心接口名 + Impl 或配置的后缀)
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
    @PersistenceContext
    private EntityManager em;

    @Override
    public List<User> findUsersByComplexCondition(SearchCriteria criteria) {
        // 使用 EntityManager 编写复杂查询
        String jpql = "SELECT u FROM User u WHERE ...";
        return em.createQuery(jpql, User.class).getResultList();
    }
}

// 合并后的 Repository
public interface UserRepository extends JpaRepository<User, Long>, 
                                           UserRepositoryCustom {
}

这种组合方式让你既享受了自动实现的便利,又保留了应对极端复杂查询的终极灵活性。

13.4.2 查询方法:方法名即查询

Spring Data JPA 最具效率的特性就是方法命名查询(Derived Query)。你只需按照规则定义方法签名,框架会解析方法名并自动生成对应的 JPA 查询。

1. 方法命名规则

方法名以 find...Byread...Byquery...Bycount...By 开头,后接属性表达式条件关键字。常见的条件关键字包括:

| 关键字 | 示例 | JPQL 等价 |
|--------|------|-----------|
| And | findByNameAndAge | where name = ?1 and age = ?2 |
| Or | findByNameOrEmail | where name = ?1 or email = ?2 |
| Between | findByAgeBetween | where age between ?1 and ?2 |
| LessThan | findByAgeLessThan | where age < ?1 |
| GreaterThanEqual | findByAgeGreaterThanEqual | where age >= ?1 |
| Like | findByNameLike | where name like ?1 |
| In | findByAgeIn | where age in ?1 |
| OrderBy | findByNameOrderByAgeDesc | where name = ?1 order by age desc |
| IgnoreCase | findByNameIgnoreCase | where upper(name) = upper(?1) |
| IsNull | findByEmailIsNull | where email is null |
| IsNotNull | findByEmailIsNotNull | where email is not null |
| Not | findByNameNot | where name <> ?1 |
| Top/First | findTop5ByOrderByAgeDesc | 返回前 5 条记录 |

2. 真实示例

public interface UserRepository extends JpaRepository<User, Long> {

    // 根据姓名精确查询
    List<User> findByName(String name);

    // 根据姓名和年龄组合查询
    List<User> findByNameAndAge(String name, Integer age);

    // 忽略大小写模糊查询
    List<User> findByNameContainingIgnoreCase(String keyword);

    // 查询年龄在某个区间的用户,按生日降序
    List<User> findByAgeBetweenOrderByBirthdayDesc(Integer minAge, Integer maxAge);

    // 统计某个姓名下的用户数
    long countByName(String name);

    // 判断邮箱是否存在
    boolean existsByEmail(String email);

    // 查询前 3 条年龄大于指定值的记录
    List<User> findTop3ByAgeGreaterThan(Integer age);
}

当你调用 findByNameAndAge 时,Spring Data JPA 自动生成如下 JPA 查询(无需手动编写):

select u from User u where u.name = ?1 and u.age = ?2

3. 方法命名查询的局限性

  • 方法名过长时会变得难以阅读,例如 findTop10ByDepartmentNameAndSalaryGreaterThanAndHireDateBetweenOrderByLastNameDesc
  • 对于多条件模糊组合查询(特别是可选条件),命名方式无法灵活支持,此时需要转向 @Query 或动态查询。

13.4.3 @Query 注解:自主掌控查询

当方法命名无法满足复杂需求,或者你希望优化查询性能(如联表、特定函数)时,可以使用 @Query 注解直接声明 JPQL 或原生 SQL。

1. 使用 JPQL

public interface UserRepository extends JpaRepository<User, Long> {

    @Query("SELECT u FROM User u WHERE u.email LIKE %:domain% AND u.age > :minAge")
    List<User> findByEmailDomainAndMinAge(@Param("domain") String domain,
                                          @Param("minAge") int minAge);
}

JPQL 查询的是对象及其属性,而非数据库表和列,因此与底层数据库类型解耦。使用 :param?1 占位符绑定参数。

2. 更新与删除操作

对于 UPDATE、DELETE 操作,除了 @Query 还需要标注 @Modifying@Transactional,因为这类操作会改变数据状态。

@Modifying
@Transactional
@Query("UPDATE User u SET u.age = :age WHERE u.id IN :ids")
int updateAgeByIds(@Param("age") int age, @Param("ids") List<Long> ids);

返回值 int 表示受影响的行数。

3. 使用原生 SQL

通过设置 nativeQuery = true,可以直接编写原生 SQL,这会绕过 JPA 的实体映射,直接操作数据库表。

@Query(value = "SELECT * FROM users WHERE email = ?1", nativeQuery = true)
User findByEmailNative(String email);

原生 SQL 查询的结果仍然会自动映射到实体(前提是返回字段与实体属性名匹配或使用 @SqlResultSetMapping 手动映射)。一般来说,能用 JPQL 解决的场景尽量不用原生 SQL,以保持数据库平台的可移植性。

4. 投影(Projection)减少数据量

如果你只需要实体中的部分字段,可以使用接口或 DTO 投影,避免查询全字段。

// 定义投影接口
public interface UserNameEmail {
    String getName();
    String getEmail();
}

// 在 Repository 中使用
@Query("SELECT u.name AS name, u.email AS email FROM User u WHERE u.age > ?1")
List<UserNameEmail> findNameAndEmailByAgeGreaterThan(int age);

这种方式只查询需要的列,在数据量较大时性能提升明显。

13.4.4 动态查询:JPA Specification 与 JpaSpecificationExecutor

现实中的后台管理界面往往需要:根据用户勾选的多个可选条件来组合查询(姓名、年龄范围、注册日期、邮箱模糊匹配等)。方法命名和 @Query 都难以优雅应对这种“参数可能为空”的动态组合场景。Spring Data JPA 提供了 JpaSpecificationExecutor 接口来解决这一问题。

1. 基本用法

首先,让 Repository 额外继承 JpaSpecificationExecutor<T>

public interface UserRepository extends JpaRepository<User, Long>,
                                         JpaSpecificationExecutor<User> {
    // 同时拥有 JPA 和动态查询能力
}

JpaSpecificationExecutor 增加了几个以 Specification 为参数的方法:

  • List<T> findAll(Specification<T> spec)
  • Page<T> findAll(Specification<T> spec, Pageable pageable)
  • List<T> findAll(Specification<T> spec, Sort sort)
  • long count(Specification<T> spec)
  • Optional<T> findOne(Specification<T> spec)

核心就是 Specification 接口,它只有一个方法:

public interface Specification<T> {
    Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, 
                          CriteriaBuilder cb);
}

你需要在调用 findAll 时动态构建 Predicate(JPA Criteria API 的谓词)。

2. 构建动态查询

假设有这样一个搜索表单:姓名(模糊)、最小年龄、最大年龄、邮箱(模糊),全部可选。

public class UserQuery {
    private String name;
    private Integer minAge;
    private Integer maxAge;
    private String email;
    // getter/setter...
}

Service 层写法:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public Page<User> searchUsers(UserQuery query, Pageable pageable) {
        Specification<User> spec = (root, criteriaQuery, cb) -> {
            List<Predicate> predicates = new ArrayList<>();

            // 姓名模糊匹配
            if (query.getName() != null && !query.getName().isEmpty()) {
                predicates.add(
                    cb.like(root.get("name"), "%" + query.getName() + "%")
                );
            }

            // 最小年龄
            if (query.getMinAge() != null) {
                predicates.add(
                    cb.greaterThanOrEqualTo(root.get("age"), query.getMinAge())
                );
            }

            // 最大年龄
            if (query.getMaxAge() != null) {
                predicates.add(
                    cb.lessThanOrEqualTo(root.get("age"), query.getMaxAge())
                );
            }

            // 邮箱模糊
            if (query.getEmail() != null && !query.getEmail().isEmpty()) {
                predicates.add(
                    cb.like(root.get("email"), "%" + query.getEmail() + "%")
                );
            }

            // 将所有条件用 AND 连接
            return cb.and(predicates.toArray(new Predicate[0]));
        };

        return userRepository.findAll(spec, pageable);
    }
}

这样的代码在运行时只拼接非空条件生成的 SQL,例如当用户只填了姓名时,SQL 为 WHERE name LIKE ?;同时填姓名和最小年龄时,SQL 变为 WHERE name LIKE ? AND age >= ?。动态性完全由 Java 代码控制。

3. 封装工具类简化重复代码

Predicate 的构建过程常有大量重复的判空和拼接,可以封装一个 SpecificationBuilder 工具类来简化:

public class UserSpecification {
    public static Specification<User> fromQuery(UserQuery query) {
        return (root, cq, cb) -> {
            List<Predicate> predicates = new ArrayList<>();
            addLike(predicates, cb, root.get("name"), query.getName());
            addBetween(predicates, cb, root.get("age"), 
                       query.getMinAge(), query.getMaxAge());
            addLike(predicates, cb, root.get("email"), query.getEmail());
            return cb.and(predicates.toArray(new Predicate[0]));
        };
    }

    private static void addLike(List<Predicate> predicates, CriteriaBuilder cb,
                                Path<String> path, String value) {
        if (StringUtils.hasText(value)) {
            predicates.add(cb.like(path, "%" + value + "%"));
        }
    }

    private static void addBetween(List<Predicate> predicates, CriteriaBuilder cb,
                                   Path<Integer> path, Integer min, Integer max) {
        if (min != null) {
            predicates.add(cb.greaterThanOrEqualTo(path, min));
        }
        if (max != null) {
            predicates.add(cb.lessThanOrEqualTo(path, max));
        }
    }
}

之后 Service 调用就变得非常简洁:

public Page<User> searchUsers(UserQuery query, Pageable pageable) {
    return userRepository.findAll(UserSpecification.fromQuery(query), pageable);
}

13.4.5 分页与排序

Spring Data JPA 与分页排序结合得极其自然。所有继承了 JpaRepository 的接口,都可以直接使用 PageableSort 参数。

1. 自动分页

// 返回 Page 对象,包含数据、总条数、总页数等
Page<User> findByAgeGreaterThan(int age, Pageable pageable);

调用时通过 PageRequest 创建分页参数:

Pageable pageable = PageRequest.of(0, 10, Sort.by("name").descending());
Page<User> result = userRepository.findByAgeGreaterThan(20, pageable);

List<User> content = result.getContent();   // 当前页数据
int totalPages = result.getTotalPages();    // 总页数
long total = result.getTotalElements();     // 总记录数
int number = result.getNumber();            // 当前页码(从 0 开始)

2. 动态查询+分页

前面演示的 findAll(Specification, Pageable) 已经体现了动态查询与分页的结合,这里不再赘述。排序同样可以传入 Sort.by() 对象。

13.4.6 实用建议与常见陷阱

  • 避免 N+1 查询:当实体中存在 @OneToMany@ManyToOne 等关联时,用 @EntityGraphJOIN FETCH 在查询时明确指定抓取策略,避免查出主实体后,再逐个查询关联实体。
  • 只查询需要的字段:多用投影或 DTO 投影,减少不必要的数据传输和内存占用。
  • 复杂统计用原生 SQL 或视图:JPA 的 Criteria API 在极复杂的报表统计上可能力不从心,此时可直接用 @Query(nativeQuery=true) 或 Spring 的 JdbcTemplate,不必硬套 JPA。
  • @Modifying 注意事项:自动清空持久化上下文,如果后续还要操作同一实体,需要重新加载。

Spring Data JPA 的设计哲学就是“让简单的事情更简单,让复杂的事情成为可能”。掌握 Repository 体系、查询方法命名规则以及动态查询,你就能应对 95% 以上的日常数据访问场景,将精力从编写重复的 SQL 代码中解放出来,更专注于业务逻辑。