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):判断是否存在- 以及
saveAll、findAllById、deleteAll、deleteById、flush等几十个方法
所有这些都是 Spring Data JPA 在运行时通过动态代理自动提供的,你完全不用写 SQL,甚至不需要写 JPA 的 JPQL。
2. Repository 的三种定义方式
根据灵活度和控制力,Spring Data JPA 支持三种 Repository 片段组合:
- 继承官方接口:如
JpaRepository<T, ID>,适合多数场景。 - 自定义方法片段:在接口中定义自定义方法名,框架解析生成。
- 手动提供实现:当查询逻辑极其复杂时,编写一个独立的
UserRepositoryCustom接口及其实现类UserRepositoryCustomImpl,然后让UserRepository同时继承JpaRepository和UserRepositoryCustom。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...By、read...By、query...By、count...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 的接口,都可以直接使用 Pageable 和 Sort 参数。
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等关联时,用@EntityGraph或JOIN FETCH在查询时明确指定抓取策略,避免查出主实体后,再逐个查询关联实体。 - 只查询需要的字段:多用投影或 DTO 投影,减少不必要的数据传输和内存占用。
- 复杂统计用原生 SQL 或视图:JPA 的 Criteria API 在极复杂的报表统计上可能力不从心,此时可直接用
@Query(nativeQuery=true)或 Spring 的JdbcTemplate,不必硬套 JPA。 @Modifying注意事项:自动清空持久化上下文,如果后续还要操作同一实体,需要重新加载。
Spring Data JPA 的设计哲学就是“让简单的事情更简单,让复杂的事情成为可能”。掌握 Repository 体系、查询方法命名规则以及动态查询,你就能应对 95% 以上的日常数据访问场景,将精力从编写重复的 SQL 代码中解放出来,更专注于业务逻辑。