Article

数据访问与切片 Spring Data JPA

更新于:2026-07-14

第一章:Spring Data JPA 概述与环境搭建

1.1 什么是 Spring Data JPA

概念名称说明注意事项
Spring Data JPASpring Data 项目的一部分,旨在简化基于 JPA(Java Persistence API)的数据访问层(DAO)开发并非 JPA 实现,而是对 JPA 的封装和增强,允许开发者通过接口定义数据访问逻辑,无需编写实现类
JPAJava 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 方法
}
泛型参数说明
User实体类型
Long主键类型

此时已自动拥有 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)

方法名语法用途代码示例注意事项
findByIdOptional<T> findById(ID id)根据主键查询单个实体Optional<User> user = userRepository.findById(1L); if (user.isPresent()) { ... }返回 Optional 避免空指针;ID 类型必须匹配实体主键类型

2.4 查询所有(findAll)

方法名语法用途代码示例注意事项
findAllList<T> findAll()查询所有记录List<User> users = userRepository.findAll();数据量大时慎用,可能导致内存溢出;建议结合分页

2.5 删除实体(deleteById, delete, deleteAll)

方法名语法用途代码示例注意事项
deleteByIdvoid deleteById(ID id)根据 ID 删除实体userRepository.deleteById(1L);ID 必须存在,否则可能抛出异常(取决于实现)
deletevoid delete(T entity)删除指定实体User user = new User(); user.setId(1L); userRepository.delete(user);实体必须包含主键;若实体未被管理,仍可删除(通过主键)
deleteAllvoid deleteAll()删除所有记录userRepository.deleteAll();危险操作,建议在事务中使用或添加确认逻辑

2.6 判断是否存在(existsById)

方法名语法用途代码示例注意事项
existsByIdboolean existsById(ID id)判断某 ID 是否存在boolean exists = userRepository.existsById(1L);性能优于先查询再判断;适用于表单校验等场景

2.7 统计记录数(count)

方法名语法用途代码示例注意事项
countlong count()统计总记录数long total = userRepository.count();返回 long 类型;适用于分页总数展示

第三章:查询方法命名规则

3.1 基于方法名的自动查询解析机制

方法名语法用途代码示例注意事项
findByXxxList<T> findByXxx(...)根据属性名自动生成查询List<User> users = userRepository.findByName("Alice");属性名必须与实体字段匹配(大小写敏感)

3.2 常用关键词:And, Or, Between, LessThan, GreaterThan 等

方法名语法用途代码示例注意事项
findByXxxAndYyyList<T> findByXxxAndYyy(XxxType xxx, YyyType yyy)多条件 AND 查询List<User> users = userRepository.findByNameAndAge("Alice", 25);参数顺序必须与方法名中字段顺序一致
findByXxxOrYyyList<T> findByXxxOrYyy(XxxType xxx, YyyType yyy)多条件 OR 查询List<User> users = userRepository.findByNameOrAge("Alice", 30);同上,注意参数顺序
findByAgeBetweenList<T> findByAgeBetween(Integer min, Integer max)范围查询(闭区间)List<User> users = userRepository.findByAgeBetween(20, 30);包含边界值,即 [min, max]
findByAgeGreaterThanList<T> findByAgeGreaterThan(Integer age)大于某个值List<User> users = userRepository.findByAgeGreaterThan(25);不包含等于,即 >
findByAgeLessThanList<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

方法名语法用途代码示例注意事项
findByNameIsNullList<User> findByNameIsNull()查询字段为空的记录List<User> users = userRepository.findByNameIsNull();对应 SQL IS NULL
findByNameIsNotNullList<User> findByNameIsNotNull()查询字段非空的记录List<User> users = userRepository.findByNameIsNotNull();对应 SQL IS NOT NULL
findByAgeInList<User> findByAgeIn(Collection<Integer> ages)查询字段值在集合中的记录List<User> users = userRepository.findByAgeIn(Arrays.asList(20, 25, 30));参数为集合类型
findByAgeNotInList<User> findByAgeNotIn(Collection<Integer> ages)查询字段值不在集合中的记录List<User> users = userRepository.findByAgeNotIn(Arrays.asList(18, 19));同上
findByNameLikeList<User> findByNameLike(String name)模糊匹配(需手动加 %List<User> users = userRepository.findByNameLike("%li%");参数需包含通配符 %_
findByNameStartingWithList<User> findByNameStartingWith(String prefix)以某字符串开头List<User> users = userRepository.findByNameStartingWith("Al");自动添加 % 在末尾
findByNameEndingWithList<User> findByNameEndingWith(String suffix)以某字符串结尾List<User> users = userRepository.findByNameEndingWith("ce");自动添加 % 在开头
findByNameContainingList<User> findByNameContaining(String infix)包含某字符串List<User> users = userRepository.findByNameContaining("li");自动添加 % 在前后

3.5 忽略大小写查询(IgnoreCase)

方法名语法用途代码示例注意事项
findByNameIgnoreCaseList<User> findByNameIgnoreCase(String name)忽略大小写精确匹配List<User> users = userRepository.findByNameIgnoreCase("alice");匹配 “Alice”, “ALICE” 等
findByNameContainingIgnoreCaseList<User> findByNameContainingIgnoreCase(String name)忽略大小写模糊匹配List<User> users = userRepository.findByNameContainingIgnoreCase("li");推荐用于搜索功能

3.6 限制结果数量(Top, First)

方法名语法用途代码示例注意事项
findFirstByNameOptional<User> findFirstByName(String name)返回第一个匹配记录Optional<User> user = userRepository.findFirstByName("Alice");若无结果返回 empty;可与 Sort 结合
findTop3ByAgeList<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) → PagePage<T> findAll(Pageable)返回完整分页信息(含总数)Page<User> page = userRepository.findAll(PageRequest.of(0, 10));适合需要显示”共 N 条”、“共 M 页”的场景;会执行 COUNT 查询,影响性能
long total = page.getTotalElements();
findAll(Pageable) → SliceSlice<T> findAll(Pageable)返回分段结果,不查询总数Slice<User> slice = userRepository.findAll(PageRequest.of(0, 10));适合”加载更多”场景;性能更好;无法获取总数
boolean hasNext = slice.hasNext();

建议:大数据量分页使用 Slice,常规分页使用 Page。

5.4 在方法名中使用 Pagination 关键词

方法名语法用途代码示例注意事项
findTopNByXxxList<T> findTop3ByAgeOrderByAgeDesc(Integer age)限制返回前 N 条记录List<User> top3 = userRepository.findTop3ByAgeOrderByAgeDesc(30);Top 或 First 后接数字;可结合排序使用
findFirstByXxxOptional<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.PERSISTcascade = CascadeType.PERSIST保存主实体时级联保存关联实体@OneToOne(cascade = CascadeType.PERSIST)仅触发 persist() 操作
private Profile profile;
CascadeType.MERGEcascade = CascadeType.MERGE合并主实体时级联合并关联实体@OneToMany(cascade = CascadeType.MERGE)用于更新场景
private List<Order> orders;
CascadeType.REMOVEcascade = CascadeType.REMOVE删除主实体时级联删除关联实体@OneToMany(cascade = CascadeType.REMOVE)危险操作,慎用
private List<Order> orders;
CascadeType.ALLcascade = CascadeType.ALL包含所有级联操作@OneToOne(cascade = CascadeType.ALL)最常用,但也最危险,需评估业务需求
private Profile profile;

建议:根据业务场景选择最小必要级联,避免意外数据删除。

6.5 获取策略(Fetch Type)

获取策略语法用途代码示例注意事项
FetchType.EAGERfetch = FetchType.EAGER立即加载关联数据@ManyToOne(fetch = FetchType.EAGER)默认策略,可能导致性能问题(N+1 查询)
private User user;
FetchType.LAZYfetch = 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自动记录创建时间@CreatedDateprivate LocalDateTime createdAt;需配合 @EnableJpaAuditing
@LastModifiedDate自动记录最后修改时间@LastModifiedDateprivate LocalDateTime lastModifiedAt;更新时自动刷新
@CreatedBy / @LastModifiedBy记录创建者/修改者@CreatedByprivate 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(动态查询)

方法语法用途代码示例注意事项
JpaSpecificationExecutorpublic 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(投影)查询部分字段支持接口投影、类投影接口投影更高效
ResultTransformerHibernate 特有自定义结果转换非标准,不推荐
Native Query + SqlResultSetMapping原生 SQL 映射到 DTO复杂场景使用配置繁琐,优先考虑 JPQL

推荐:使用构造函数表达式(见第四章)或接口投影。