第一章:Spring Data JPA 概述与环境搭建
1.1 什么是 Spring Data JPA
| 概念名称 | 说明 | 注意事项 |
|---|
| Spring Data JPA | Spring Data 项目的一部分,旨在简化基于 JPA(Java Persistence API)的数据访问层(DAO)开发 | 并非 JPA 实现,而是对 JPA 的封装和增强,允许开发者通过接口定义数据访问逻辑,无需编写实现类 |
| JPA | Java EE 的持久化规范,定义了对象关系映射(ORM)的标准 | 常见实现:Hibernate、EclipseLink、OpenJPA |
| Spring Data JPA 的角色 | 在 JPA 基础上提供 Repository 抽象、方法名查询、分页支持等高级特性 | 极大减少模板代码 |
核心思想:约定优于配置,通过接口方法命名自动生成查询逻辑。
1.2 Spring Data JPA 的核心优势与架构
| 优势 | 说明 |
|---|
| 减少样板代码 | 无需手动编写 CRUD 实现,继承 JpaRepository 即可获得基础操作 |
| 方法名自动解析 | 支持通过方法名定义查询(如 findByName),无需写 SQL |
| 灵活的自定义查询 | 支持 @Query 注解编写 JPQL 或原生 SQL |
| 内置分页与排序 | 通过 Pageable 和 Sort 参数轻松实现分页功能 |
| 与 Spring 生态无缝集成 | 天然支持 Spring Boot、事务管理、依赖注入等 |
架构组成:
+------------------+ +---------------------+
| Service Layer | --> | UserRepository |
+------------------+ +---------------------+
|
v
Spring Data JPA (Interface)
|
v
JPA Provider (e.g., Hibernate)
|
v
Database (e.g., MySQL)
| 层级 | 说明 |
|---|
| Repository 接口 | 开发者定义,继承 JpaRepository |
| Spring Data JPA | 运行时生成实现类 |
| JPA Provider | 如 Hibernate,负责与数据库交互 |
| Database | 实际数据存储 |
1.3 项目初始化与依赖配置(Maven/Gradle)
使用 Spring Boot 可快速初始化项目。
Maven 配置(pom.xml):
<dependencies>
<!-- Spring Boot Starter Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 数据库驱动,以 MySQL 为例 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Boot Starter Web(可选,用于测试) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
Gradle 配置(build.gradle):
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'mysql:mysql-connector-java'
implementation 'org.springframework.boot:spring-boot-starter-web'
}
注意:确保 Spring Boot 版本与依赖兼容,推荐使用 Spring Initializr 生成项目。
1.4 配置 application.yml 或 application.properties
application.yml(推荐):
spring:
datasource:
url: jdbc:mysql://localhost:3306/testdb?useSSL=false&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
application.properties:
spring.datasource.url=jdbc:mysql://localhost:3306/testdb?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
关键配置说明:
| 配置项 | 用途 | 建议值 |
|---|
| spring.jpa.hibernate.ddl-auto | 控制表结构生成策略 | 开发用 update,生产用 none |
| spring.jpa.show-sql | 是否打印 SQL 语句 | true(开发调试) |
| spring.jpa.properties.hibernate.dialect | 数据库方言 | 根据数据库选择,如 MySQL8Dialect |
1.5 创建实体类(@Entity, @Id, @GeneratedValue 等常用注解)
实体类映射数据库表。
示例:User 实体:
import javax.persistence.*;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", length = 50, nullable = false)
private String name;
@Column(name = "age")
private Integer age;
// 无参构造(JPA 要求)
public User() {}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
// getters and setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
常用注解说明:
| 注解 | 用途 |
|---|
| @Entity | 标记类为 JPA 实体 |
| @Table(name=“xxx”) | 指定对应数据库表名 |
| @Id | 标记主键字段 |
| @GeneratedValue | 主键生成策略(IDENTITY, AUTO, SEQUENCE, TABLE) |
| @Column | 配置字段映射(名称、长度、是否可空等) |
注意:必须提供无参构造函数;建议使用包装类型(如 Integer)避免基本类型默认值问题。
1.6 创建 Repository 接口并继承 JpaRepository
Repository 是数据访问的核心接口。
UserRepository 示例:
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository // 可选,Spring Boot 会自动扫描
public interface UserRepository extends JpaRepository<User, Long> {
// 继承了所有基础 CRUD 方法
}
此时已自动拥有 save, findById, findAll, deleteById 等方法。
1.7 编写第一个查询方法并测试
添加自定义查询方法:
在 UserRepository 中添加:
List<User> findByName(String name);
Spring Data JPA 会自动解析该方法,生成等价于 SELECT * FROM users WHERE name = ? 的查询。
编写测试类:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
void shouldFindUserByName() {
// 准备数据
userRepository.save(new User("Alice", 25));
// 执行查询
List<User> users = userRepository.findByName("Alice");
// 验证结果
assertThat(users).hasSize(1);
assertThat(users.get(0).getAge()).isEqualTo(25);
}
}
运行测试,应通过。控制台会输出生成的 SQL。
本章小结:
- 成功搭建 Spring Data JPA 开发环境
- 理解了核心组件:实体类、Repository 接口、配置文件
- 实现了第一个基于方法名的查询
第二章:基础 CRUD 操作
2.1 保存实体(save)
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| save | <S extends T> S save(S entity) | 保存一个实体,若 ID 为空则插入,否则更新 | userRepository.save(new User("Alice", 25)); | 实体类必须正确使用 @Entity 和主键注解;若 ID 已存在则执行更新操作 |
2.2 批量保存(saveAll)
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| saveAll | <S extends T> List<S> saveAll(Iterable<S> entities) | 批量保存多个实体 | List<User> users = Arrays.asList(new User("A", 20), new User("B", 22)); userRepository.saveAll(users); | 返回保存后的实体列表;性能优于循环调用 save;建议配合 @Transactional 使用 |
2.3 根据 ID 查询(findById)
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| findById | Optional<T> findById(ID id) | 根据主键查询单个实体 | Optional<User> user = userRepository.findById(1L); if (user.isPresent()) { ... } | 返回 Optional 避免空指针;ID 类型必须匹配实体主键类型 |
2.4 查询所有(findAll)
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| findAll | List<T> findAll() | 查询所有记录 | List<User> users = userRepository.findAll(); | 数据量大时慎用,可能导致内存溢出;建议结合分页 |
2.5 删除实体(deleteById, delete, deleteAll)
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| deleteById | void deleteById(ID id) | 根据 ID 删除实体 | userRepository.deleteById(1L); | ID 必须存在,否则可能抛出异常(取决于实现) |
| delete | void delete(T entity) | 删除指定实体 | User user = new User(); user.setId(1L); userRepository.delete(user); | 实体必须包含主键;若实体未被管理,仍可删除(通过主键) |
| deleteAll | void deleteAll() | 删除所有记录 | userRepository.deleteAll(); | 危险操作,建议在事务中使用或添加确认逻辑 |
2.6 判断是否存在(existsById)
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| existsById | boolean existsById(ID id) | 判断某 ID 是否存在 | boolean exists = userRepository.existsById(1L); | 性能优于先查询再判断;适用于表单校验等场景 |
2.7 统计记录数(count)
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| count | long count() | 统计总记录数 | long total = userRepository.count(); | 返回 long 类型;适用于分页总数展示 |
第三章:查询方法命名规则
3.1 基于方法名的自动查询解析机制
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| findByXxx | List<T> findByXxx(...) | 根据属性名自动生成查询 | List<User> users = userRepository.findByName("Alice"); | 属性名必须与实体字段匹配(大小写敏感) |
3.2 常用关键词:And, Or, Between, LessThan, GreaterThan 等
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| findByXxxAndYyy | List<T> findByXxxAndYyy(XxxType xxx, YyyType yyy) | 多条件 AND 查询 | List<User> users = userRepository.findByNameAndAge("Alice", 25); | 参数顺序必须与方法名中字段顺序一致 |
| findByXxxOrYyy | List<T> findByXxxOrYyy(XxxType xxx, YyyType yyy) | 多条件 OR 查询 | List<User> users = userRepository.findByNameOrAge("Alice", 30); | 同上,注意参数顺序 |
| findByAgeBetween | List<T> findByAgeBetween(Integer min, Integer max) | 范围查询(闭区间) | List<User> users = userRepository.findByAgeBetween(20, 30); | 包含边界值,即 [min, max] |
| findByAgeGreaterThan | List<T> findByAgeGreaterThan(Integer age) | 大于某个值 | List<User> users = userRepository.findByAgeGreaterThan(25); | 不包含等于,即 > |
| findByAgeLessThan | List<T> findByAgeLessThan(Integer age) | 小于某个值 | List<User> users = userRepository.findByAgeLessThan(30); | 即 < |
3.3 支持的返回类型
返回类型由开发者在方法声明中指定,框架自动适配。
| 返回类型 | 用途 | 示例方法签名 | 注意事项 |
|---|
| T | 返回单个实体 | User findFirstByName(String name) | 若结果多于一条,抛异常;建议用于唯一约束字段 |
| Optional<T> | 安全返回单个实体 | Optional<User> findByName(String name) | 推荐方式,避免空指针 |
| List<T> | 返回多个实体 | List<User> findByAge(Integer age) | 最常用,返回匹配的所有记录 |
| Page<T> | 分页结果 | Page<User> findByAge(Integer age, Pageable pageable) | 需传入 Pageable 参数,包含分页和排序信息 |
| Slice<T> | 分段结果(轻量级分页) | Slice<User> findByAge(Integer age, Pageable pageable) | 不统计总数,适合大数据集滚动加载 |
| Stream<T> | 流式处理结果 | Stream<User> findByName(String name) | 可结合 try-with-resources 使用,防止连接泄漏 |
3.4 使用 IsNull, IsNotNull, In, NotIn, Like, NotLike, StartingWith, EndingWith, Contains
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| findByNameIsNull | List<User> findByNameIsNull() | 查询字段为空的记录 | List<User> users = userRepository.findByNameIsNull(); | 对应 SQL IS NULL |
| findByNameIsNotNull | List<User> findByNameIsNotNull() | 查询字段非空的记录 | List<User> users = userRepository.findByNameIsNotNull(); | 对应 SQL IS NOT NULL |
| findByAgeIn | List<User> findByAgeIn(Collection<Integer> ages) | 查询字段值在集合中的记录 | List<User> users = userRepository.findByAgeIn(Arrays.asList(20, 25, 30)); | 参数为集合类型 |
| findByAgeNotIn | List<User> findByAgeNotIn(Collection<Integer> ages) | 查询字段值不在集合中的记录 | List<User> users = userRepository.findByAgeNotIn(Arrays.asList(18, 19)); | 同上 |
| findByNameLike | List<User> findByNameLike(String name) | 模糊匹配(需手动加 %) | List<User> users = userRepository.findByNameLike("%li%"); | 参数需包含通配符 % 或 _ |
| findByNameStartingWith | List<User> findByNameStartingWith(String prefix) | 以某字符串开头 | List<User> users = userRepository.findByNameStartingWith("Al"); | 自动添加 % 在末尾 |
| findByNameEndingWith | List<User> findByNameEndingWith(String suffix) | 以某字符串结尾 | List<User> users = userRepository.findByNameEndingWith("ce"); | 自动添加 % 在开头 |
| findByNameContaining | List<User> findByNameContaining(String infix) | 包含某字符串 | List<User> users = userRepository.findByNameContaining("li"); | 自动添加 % 在前后 |
3.5 忽略大小写查询(IgnoreCase)
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| findByNameIgnoreCase | List<User> findByNameIgnoreCase(String name) | 忽略大小写精确匹配 | List<User> users = userRepository.findByNameIgnoreCase("alice"); | 匹配 “Alice”, “ALICE” 等 |
| findByNameContainingIgnoreCase | List<User> findByNameContainingIgnoreCase(String name) | 忽略大小写模糊匹配 | List<User> users = userRepository.findByNameContainingIgnoreCase("li"); | 推荐用于搜索功能 |
3.6 限制结果数量(Top, First)
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| findFirstByName | Optional<User> findFirstByName(String name) | 返回第一个匹配记录 | Optional<User> user = userRepository.findFirstByName("Alice"); | 若无结果返回 empty;可与 Sort 结合 |
| findTop3ByAge | List<User> findTop3ByAge(Integer age) | 返回最多前 N 条记录 | List<User> users = userRepository.findTop3ByAge(25); | N 可替换为任意数字;常用于排行榜 |
第四章:使用 @Query 注解进行自定义查询
@Query 是 Spring Data JPA 中最强大的功能之一,允许开发者编写自定义的 JPQL 或原生 SQL 查询,适用于复杂查询场景。
4.1 使用 JPQL 编写自定义查询(@Query)
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| @Query (JPQL) | @Query("SELECT u FROM User u WHERE u.name = ?1") | 使用 JPQL 进行面向对象查询 | @Query("SELECT u FROM User u WHERE u.age > ?1") | JPQL 操作的是实体类和属性,而非数据库表和字段;?1 表示第一个参数 |
| List<User> findByName(String name); | | List<User> findByAgeGreaterThan(int age); | |
4.2 使用原生 SQL 查询(nativeQuery = true)
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| @Query (native) | @Query(value = "SELECT * FROM users WHERE name = ?1", nativeQuery = true) | 使用原生 SQL 直接操作数据库 | @Query(value = "SELECT * FROM users WHERE age > ?1", nativeQuery = true) | 必须设置 nativeQuery = true;SQL 针对具体数据库语法;返回结果需与实体映射一致 |
| List<User> findByNameNative(String name); | | List<User> findByAgeGreaterThanNative(int age); | |
4.3 参数绑定:位置参数与命名参数
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 位置参数 | @Query("SELECT u FROM User u WHERE u.name = ?1 AND u.age = ?2") | 按参数顺序绑定 | @Query("SELECT u FROM User u WHERE u.name = ?1 AND u.age = ?2") | ?1 表示第一个参数,?2 表示第二个;顺序必须一致 |
| | | List<User> findByNameAndAge(String name, Integer age); | |
| 命名参数 | @Query("SELECT u FROM User u WHERE u.name = :name") | 使用 :paramName 和 @Param 注解绑定 | @Query("SELECT u FROM User u WHERE u.name = :name AND u.age = :age") | 推荐使用命名参数,可读性强;参数名需与 @Param 一致 |
| List<User> findByName(@Param("name") String name); | | List<User> findByNameAndAge(@Param("name") String name, @Param("age") Integer age); | |
4.4 动态查询与 SpEL 表达式支持
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| SpEL 表达式 | @Query("SELECT u FROM User u WHERE u.name LIKE %:#{[0]}%") | 在查询中使用 SpEL 动态拼接 | @Query("SELECT u FROM User u WHERE u.name LIKE %:#{#name}%") | SpEL 支持 #{#param} 引用命名参数,#{[0]} 引用位置参数;需注意 SQL 注入风险 |
| | | List<User> findByNameLike(@Param("name") String name); | |
| 条件化查询 | @Query("SELECT u FROM User u WHERE (:name IS NULL OR u.name = :name)") | 实现可选条件查询 | @Query("SELECT u FROM User u WHERE (:name IS NULL OR u.name = :name) AND (:age IS NULL OR u.age = :age)") | 用于构建动态查询,null 值条件将被忽略 |
| | | List<User> findUsers(@Param("name") String name, @Param("age") Integer age); | |
4.5 更新与删除操作(@Modifying)
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| @Modifying + @Query | @Modifying | 执行更新或删除操作 | @Modifying | 必须添加 @Modifying 注解;默认在事务中执行;返回受影响行数 |
| @Query("UPDATE User u SET u.age = :age WHERE u.name = :name") | | @Query("UPDATE User u SET u.age = :age WHERE u.name = :name") | |
| int updateAgeByName(@Param("name") String name, @Param("age") Integer age); | | int updateAgeByName(@Param("name") String name, @Param("age") Integer age); | |
| 删除操作 | @Modifying | 自定义删除逻辑 | @Modifying | 同上,需 @Modifying;建议在 @Transactional 方法中调用 |
| @Query("DELETE FROM User u WHERE u.age < :age") | | @Query("DELETE FROM User u WHERE u.age < :age") | |
| int deleteByAgeLessThan(@Param("age") Integer age); | | int deleteByAgeLessThan(@Param("age") Integer age); | |
注意:@Modifying 方法通常需要调用方使用 @Transactional 注解,否则可能抛出异常。
4.6 返回自定义 DTO 对象(构造函数表达式)
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| 构造函数查询 | @Query("SELECT new com.example.dto.UserSummary(u.name, u.age) FROM User u WHERE u.age > :age") | 查询部分字段并封装为 DTO | 先定义 DTO 类,然后在 @Query 中使用 new 关键字调用 DTO 构造函数 | 必须在 JPQL 中使用 new 关键字调用 DTO 的构造函数;包名必须完整;构造函数参数顺序和类型必须匹配 |
// DTO 类
public class UserSummary {
private String name;
private Integer age;
public UserSummary(String name, Integer age) {
this.name = name;
this.age = age;
}
}
// Repository 方法
@Query("SELECT new com.example.dto.UserSummary(u.name, u.age) FROM User u WHERE u.age > :age")
List<UserSummary> findUserSummaries(@Param("age") Integer age);
注意:不支持原生 SQL 直接返回非实体类,需结合 @SqlResultSetMapping 或使用 JdbcTemplate。
第五章:分页与排序(Pageable & Sort)
分页与排序是数据访问的常见需求,Spring Data JPA 提供了统一的 Pageable 和 Sort 接口支持。
5.1 使用 Sort 对象进行排序
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| findAll(Sort) | List<T> findAll(Sort sort) | 根据指定排序规则查询所有记录 | List<User> users = userRepository.findAll(Sort.by("age").ascending()); | Sort.by("field") 创建排序;支持链式调用 .and(Sort.Order.asc("name")) 进行多字段排序 |
| | | // 降序 | |
| | | Sort.by(Sort.Order.desc("age")) | |
5.2 使用 Pageable 实现分页查询
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| findAll(Pageable) | Page<T> findAll(Pageable pageable) | 分页查询并返回分页元数据 | Pageable pageable = PageRequest.of(0, 10); // 第0页,每页10条 | PageRequest.of(page, size) 创建 Pageable;page 从 0 开始;返回 Page 包含总数、总页数等信息 |
| | | Page<User> page = userRepository.findAll(pageable); | |
| | | List<User> content = page.getContent(); | |
5.3 Page 与 Slice 的区别与使用场景
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| findAll(Pageable) → Page | Page<T> findAll(Pageable) | 返回完整分页信息(含总数) | Page<User> page = userRepository.findAll(PageRequest.of(0, 10)); | 适合需要显示”共 N 条”、“共 M 页”的场景;会执行 COUNT 查询,影响性能 |
| | | long total = page.getTotalElements(); | |
| findAll(Pageable) → Slice | Slice<T> findAll(Pageable) | 返回分段结果,不查询总数 | Slice<User> slice = userRepository.findAll(PageRequest.of(0, 10)); | 适合”加载更多”场景;性能更好;无法获取总数 |
| | | boolean hasNext = slice.hasNext(); | |
建议:大数据量分页使用 Slice,常规分页使用 Page。
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| findTopNByXxx | List<T> findTop3ByAgeOrderByAgeDesc(Integer age) | 限制返回前 N 条记录 | List<User> top3 = userRepository.findTop3ByAgeOrderByAgeDesc(30); | Top 或 First 后接数字;可结合排序使用 |
| findFirstByXxx | Optional<T> findFirstByName(String name) | 返回第一个匹配结果 | Optional<User> user = userRepository.findFirstByName("Alice"); | 与 Top1 类似;常用于唯一性查询 |
5.5 在 @Query 中支持分页和排序
| 方法名 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| @Query + Pageable | @Query("SELECT u FROM User u WHERE u.age > ?1") | 在自定义查询中支持分页 | @Query("SELECT u FROM User u WHERE u.age > :age") | 方法参数中必须包含 Pageable;框架自动处理分页逻辑 |
| Page<User> findByAgeGreaterThan(int age, Pageable pageable); | | Page<User> findByAgeGreaterThan(@Param("age") Integer age, Pageable pageable); | |
| @Query + Sort | @Query("SELECT u FROM User u WHERE u.name LIKE %:name%") | 在自定义查询中支持排序 | List<User> users = userRepository.findByNameLike("li", Sort.by("age").descending()); | 参数中传入 Sort 对象即可 |
| List<User> findByNameLike(@Param("name") String name, Sort sort); | | | |
注意:原生 SQL 分页需手动处理(如 MySQL 的 LIMIT),而 JPQL 分页由框架自动转换。
5.6 自定义分页查询的性能优化建议
| 建议 | 说明 |
|---|
| 避免 SELECT * | 在 @Query 中只选择必要字段,减少数据传输 |
| 使用索引字段分页 | 分页排序字段应建立数据库索引 |
| 优先使用 Slice | 若无需总数,使用 Slice 避免额外的 COUNT 查询 |
| 控制每页大小 | 设置合理的 size,避免内存溢出 |
| 延迟加载关联数据 | 避免 N+1 查询问题,合理使用 JOIN FETCH |
第六章:实体关联映射(Relationship Mapping)
Spring Data JPA 支持 JPA 规范中的四种基本关联关系:一对一、一对多、多对一、多对多。正确配置关联映射是实现复杂业务逻辑的基础。
6.1 一对一关联(@OneToOne)
| 方法/注解 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| @OneToOne | @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) | 建立两个实体间的一对一关系 | 参见下方代码示例 | mappedBy 表示被控方;@JoinColumn 指定外键字段;默认 eager 加载,建议设为 LAZY |
| @JoinColumn | @JoinColumn(name = "profile_id") | 指定外键字段 | @JoinColumn(name = "profile_id", referencedColumnName = "id") | |
// User.java
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "profile_id", referencedColumnName = "id")
private Profile profile;
// Profile.java(反向引用可选)
@OneToOne(mappedBy = "profile")
private User user;
6.2 一对多与多对一关联(@OneToMany, @ManyToOne)
| 方法/注解 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| @OneToMany | @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) | 一个实体对应多个子实体(如用户-订单) | private List<Order> orders = new ArrayList<>(); | 必须使用 mappedBy 指向多端的属性;建议 fetch = LAZY 避免 N+1 查询 |
| @ManyToOne | @ManyToOne(fetch = FetchType.EAGER) | 多个实体属于同一个父实体 | @ManyToOne(fetch = FetchType.EAGER) | @JoinColumn 定义外键;默认 EAGER 加载,可根据需要改为 LAZY |
| @JoinColumn | @JoinColumn(name = "user_id") | 定义外键 | @JoinColumn(name = "user_id") | |
| | | private User user; | |
// User.java
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Order> orders = new ArrayList<>();
// Order.java
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id")
private User user;
6.3 多对多关联(@ManyToMany)
| 方法/注解 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| @ManyToMany | @ManyToMany | 建立双向多对多关系(如用户-角色) | 参见下方代码示例 | 必须使用 @JoinTable 指定中间表;避免在集合中添加重复元素;建议初始化集合防止 NullPointer |
| @JoinTable | 指定中间表名和关联字段 | | name = "user_role" | |
// User.java
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(
name = "user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
// Role.java(反向)
@ManyToMany(mappedBy = "roles")
private Set<User> users;
6.4 级联操作(Cascade)
| 级联类型 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| CascadeType.PERSIST | cascade = CascadeType.PERSIST | 保存主实体时级联保存关联实体 | @OneToOne(cascade = CascadeType.PERSIST) | 仅触发 persist() 操作 |
| | | private Profile profile; | |
| CascadeType.MERGE | cascade = CascadeType.MERGE | 合并主实体时级联合并关联实体 | @OneToMany(cascade = CascadeType.MERGE) | 用于更新场景 |
| | | private List<Order> orders; | |
| CascadeType.REMOVE | cascade = CascadeType.REMOVE | 删除主实体时级联删除关联实体 | @OneToMany(cascade = CascadeType.REMOVE) | 危险操作,慎用 |
| | | private List<Order> orders; | |
| CascadeType.ALL | cascade = CascadeType.ALL | 包含所有级联操作 | @OneToOne(cascade = CascadeType.ALL) | 最常用,但也最危险,需评估业务需求 |
| | | private Profile profile; | |
建议:根据业务场景选择最小必要级联,避免意外数据删除。
6.5 获取策略(Fetch Type)
| 获取策略 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| FetchType.EAGER | fetch = FetchType.EAGER | 立即加载关联数据 | @ManyToOne(fetch = FetchType.EAGER) | 默认策略,可能导致性能问题(N+1 查询) |
| | | private User user; | |
| FetchType.LAZY | fetch = FetchType.LAZY | 延迟加载,访问时才查询 | @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) | 推荐用于集合关联;注意在事务外访问可能抛 LazyInitializationException |
| | | private List<Order> orders; | |
解决方案:使用 JOIN FETCH 在查询中预加载,或在 Service 层保持事务。
6.6 解决 N+1 查询问题
| 方法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| JPQL JOIN FETCH | @Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id") | 在查询中预加载关联数据 | @Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id") | 使用 FETCH 关键字强制加载关联;避免额外查询 |
| | | Optional<User> findWithOrdersById(@Param("id") Long id); | |
| EntityGraph | @EntityGraph(attributePaths = {"orders"}) | 定义实体图指定加载路径 | @EntityGraph(attributePaths = {"orders", "profile"}) | 可复用,适合多种查询场景 |
| | | User findById(Long id); | |
警告:未处理的 N+1 查询是性能杀手,务必在测试中监控 SQL 输出。
第七章:高级特性与扩展
本章介绍 Spring Data JPA 的高级功能,提升开发效率和系统灵活性。
7.1 自定义 Repository 实现
步骤一:自定义接口
public interface UserRepositoryCustom {
List<User> findByCustomLogic(String param);
}
步骤二:实现类(类名必须为 RepositoryName + Impl)
public class UserRepositoryImpl implements UserRepositoryCustom {
@PersistenceContext
private EntityManager entityManager;
public List<User> findByCustomLogic(String param) {
return entityManager.createQuery("SELECT u FROM User u WHERE ...", User.class)
.getResultList();
}
}
步骤三:主 Repository 继承
public interface UserRepository
extends JpaRepository<User, Long>, UserRepositoryCustom {
// 标准 + 自定义方法组合
}
| 说明 | 注意事项 |
|---|
| 接口名必须以 Custom 结尾或与主 Repository 名一致 | 类名必须为 RepositoryName + Impl;自动注入到主 Repository 中 |
| 无需额外配置,Spring 自动装配 | |
7.2 Auditing(审计) - 自动填充创建/修改信息
@SpringBootApplication
@EnableJpaAuditing
public class Application { ... }
| 注解 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| @EnableJpaAuditing | @Configuration @EnableJpaAuditing | 启用 JPA 审计功能 | 必须在配置类或启动类上添加 | 需配合实体审计注解使用 |
| @CreatedDate | 自动记录创建时间 | @CreatedDate | private LocalDateTime createdAt; | 需配合 @EnableJpaAuditing |
| @LastModifiedDate | 自动记录最后修改时间 | @LastModifiedDate | private LocalDateTime lastModifiedAt; | 更新时自动刷新 |
| @CreatedBy / @LastModifiedBy | 记录创建者/修改者 | @CreatedBy | private String createdBy; | 需实现 AuditorAware<T> 接口提供当前用户信息 |
@Entity
public class User {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime lastModifiedAt;
@CreatedBy
private String createdBy;
}
7.3 事件监听(Entity Listeners)
| 注解 | 用途 | 执行时机 | 注意事项 |
|---|
| @PrePersist | 实体持久化前触发 | entityManager.persist() 或 save() 时 | 可用于设置默认值、加密等 |
| @PostPersist | 实体持久化后触发 | INSERT SQL 执行后 | 不可用于修改实体状态 |
| @PreUpdate | 实体更新前触发 | merge() 操作前 | 可用于版本号递增 |
| @PostLoad | 实体加载后触发 | 从数据库读取后 | 可用于计算衍生字段 |
| @PreRemove | 删除前触发 | remove() 前 | 可用于软删除逻辑 |
@Entity
@EntityListeners(UserListener.class)
public class User { ... }
public class UserListener {
@PrePersist
public void prePersist(User user) {
user.setCreatedAt(LocalDateTime.now());
}
}
7.4 Specifications(动态查询)
| 方法 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| JpaSpecificationExecutor | public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> | 启用动态查询支持 | Specification<User> spec = (root, query, cb) -> cb.equal(root.get("name"), "Alice"); | 使用 Criteria API 构建复杂条件 |
| | | List<User> users = repo.findAll(spec); | |
| 动态组合条件 | Specifications.where(...).and(...).or(...) | 组合多个查询条件 | Specification<User> spec = Specification.where(hasName("Alice")).and(hasAge(25)); | 推荐直接使用 Specification.where().and().or() |
优势:构建灵活的搜索过滤器,避免大量命名查询方法。
7.5 锁机制(Locking)
| 锁类型 | 语法 | 用途 | 代码示例 | 注意事项 |
|---|
| PESSIMISTIC_READ | @Lock(LockModeType.PESSIMISTIC_READ) | 悲观读锁,允许并发读 | @Query("...") @Lock(LockModeType.PESSIMISTIC_READ) | 阻止写操作 |
| | | List<User> findByAge(int age); | |
| PESSIMISTIC_WRITE | @Lock(LockModeType.PESSIMISTIC_WRITE) | 悲观写锁,阻断读写 | @Lock(LockModeType.PESSIMISTIC_WRITE) | 强一致性场景使用,注意死锁 |
| | | User findById(Long id); | |
| OPTIMISTIC | @Version 注解 | 乐观锁,通过版本号控制 | @Version private Integer version; | 并发更新时检查版本,失败抛 OptimisticLockingFailureException |
7.6 自定义查询结果映射
| 方法 | 用途 | 说明 | 注意事项 |
|---|
| Projections(投影) | 查询部分字段 | 支持接口投影、类投影 | 接口投影更高效 |
| ResultTransformer | Hibernate 特有 | 自定义结果转换 | 非标准,不推荐 |
| Native Query + SqlResultSetMapping | 原生 SQL 映射到 DTO | 复杂场景使用 | 配置繁琐,优先考虑 JPQL |
推荐:使用构造函数表达式(见第四章)或接口投影。