Skip to content

Spring Data JPA 核心全过程原理

Spring Data JPA 不是一个新的 ORM。它是在 JPA 规范和 Hibernate 这类 JPA Provider 之上,再封装一层 Repository 编程模型,让你少写 DAO 模板代码。

很多人学 Spring Data JPA 只记住一句话:“继承 JpaRepository 就能查数据库”。这远远不够。真正要理解的是:Repository 接口为什么没有实现类也能调用、方法名查询如何被解析、@Query 和 Specification 最后如何变成 SQL、分页为什么会执行 count、为什么 DTO 投影比返回 Entity 更稳、为什么复杂报表不要硬塞进 Repository。

学习目标

学完本章要能说清楚:

  1. Spring Data JPA、JPA、Hibernate 三者分别做什么。
  2. Repository 接口没有实现类,为什么可以执行。
  3. 方法名查询 findByStatusAndNameLike 如何被解析。
  4. @Query JPQL 和原生 SQL 有什么区别。
  5. Specification 如何通过 Criteria API 构造动态查询。
  6. Page 为什么会执行数据查询和 count 查询,Slice 为什么不查总数。
  7. DTO 投影为什么能减少懒加载、字段暴露和 N+1。
  8. save 到底是 persist 还是 merge,为什么更新不一定要显式 save。
  9. 商业项目中 Repository、Service、Entity、DTO 应该怎么分工。
  10. 线上出现慢查询、N+1、分页慢、懒加载异常时怎么排查。

三层关系

mermaid
flowchart TD
    A["Service 调用 Repository 接口"] --> B["Spring Data JPA Repository 代理"]
    B --> C["JPA EntityManager"]
    C --> D["Hibernate 实现 JPA"]
    D --> E["JDBC"]
    E --> F["数据库"]
层次作用典型对象
Spring Data JPA生成 Repository 代理,解析方法名、分页、排序JpaRepositoryRepositoryFactory
JPA持久化规范,定义接口和注解语义EntityManager@Entity、JPQL
HibernateJPA 常见实现,管理实体状态并生成 SQLSession、持久化上下文、脏检查
JDBC/数据库真正执行 SQLPreparedStatement、索引、事务、锁

所以排查问题不能只看 Repository 方法名。最终一定要回到 Hibernate 生成的 SQL 和数据库执行计划。

Repository 为什么没有实现类也能运行

Repository 是接口。

java
public interface AssetRepository extends JpaRepository<Asset, Long> {
    Optional<Asset> findByAssetCode(String assetCode);
}

业务代码可以直接注入:

java
@Service
public class AssetService {
    private final AssetRepository assetRepository;

    public AssetService(AssetRepository assetRepository) {
        this.assetRepository = assetRepository;
    }
}

原因是 Spring Data JPA 启动时会扫描 Repository 接口,为它创建代理对象。

mermaid
flowchart TD
    A["启动扫描 Repository 接口"] --> B["读取泛型 Entity 和 ID 类型"]
    B --> C["分析继承的 JpaRepository 方法"]
    C --> D["分析自定义方法名和 @Query"]
    D --> E["创建 Repository 代理对象"]
    E --> F["注册为 Spring Bean"]
    F --> G["Service 注入代理对象"]

代理对象收到方法调用后,会判断这是什么类型的方法:

mermaid
flowchart TD
    A["调用 Repository 方法"] --> B{"是否 JpaRepository 内置方法?"}
    B -->|"是"| C["调用 SimpleJpaRepository"]
    B -->|"否"| D{"是否 @Query?"}
    D -->|"是"| E["执行声明的 JPQL 或原生 SQL"]
    D -->|"否"| F{"是否方法名查询?"}
    F -->|"是"| G["解析方法名生成查询"]
    F -->|"否"| H["查找自定义实现"]
    C --> I["EntityManager"]
    E --> I
    G --> I
    H --> I

JpaRepository 内置方法做了什么

JpaRepository 提供了大量通用方法:

java
assetRepository.findById(1L);
assetRepository.save(asset);
assetRepository.deleteById(1L);
assetRepository.findAll(pageable);

这些方法通常由 SimpleJpaRepository 实现,底层调用 EntityManager

方法底层含义
findById调用 EntityManager.find
save新对象可能 persist,已有对象可能 merge
delete查询或引用实体后 remove
findAll(Pageable)生成分页查询和 count 查询
existsById判断主键是否存在

save 最容易误解。它不是简单的 update。

mermaid
flowchart TD
    A["调用 save(entity)"] --> B{"实体是否被认为是新对象?"}
    B -->|"是"| C["EntityManager.persist"]
    B -->|"否"| D["EntityManager.merge"]
    C --> E["实体进入托管态,准备 insert"]
    D --> F["游离数据复制到托管实体,准备 update"]

如果你传入的是前端反序列化出来的不完整对象,merge 可能把空字段复制到托管实体,造成误更新。商业项目更推荐更新时先查托管实体,再修改允许更新的字段。

java
@Transactional
public void renameAsset(Long id, String name) {
    Asset asset = assetRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("资产不存在"));
    asset.rename(name);
}

这段代码不显式调用 save,但事务提交前 Hibernate 会脏检查托管实体并生成 update。

方法名查询如何解析

java
List<Asset> findByStatusAndAssetNameContainingOrderByIdDesc(Integer status, String keyword);

Spring Data JPA 会把方法名拆成几部分:

片段含义
find查询操作
By条件开始
Status实体属性 status
And条件连接
AssetNameContainingassetName like %?%
OrderByIdDesc按 id 倒序

解析过程:

mermaid
flowchart TD
    A["Repository 方法名"] --> B["识别操作类型 find/count/exists/delete"]
    B --> C["按 By 拆分条件"]
    C --> D["解析属性名和关键字"]
    D --> E["生成查询模型"]
    E --> F["转换为 JPQL / Criteria"]
    F --> G["Hibernate 生成 SQL"]

方法名查询适合简单固定条件。超过三四个条件后,可读性会快速下降。

不推荐:

java
List<Asset> findByDeptIdAndStatusAndAssetNameContainingAndCreateTimeBetweenAndDeletedOrderByIdDesc(...);

推荐改成 @Query、Specification,或明确写自定义 Repository。

@Query:JPQL 和原生 SQL

JPQL 面向实体和属性。

java
@Query("""
    select a
    from Asset a
    where a.status = :status
    order by a.id desc
""")
List<Asset> findEnabledAssets(@Param("status") Integer status);

这里的 Asset 是实体类,不是表名;status 是实体属性,不是数据库字段。

原生 SQL 面向表和列。

java
@Query(
    value = """
        select id, asset_code, asset_name, status
        from asset
        where status = :status
        order by id desc
    """,
    nativeQuery = true
)
List<Asset> findEnabledAssetsNative(@Param("status") Integer status);

选择建议:

查询方式优点风险
方法名查询简单快速复杂条件可读性差
JPQL面向实体,和 JPA 模型一致最终 SQL 不直观
原生 SQLSQL 可控,能用数据库特性可移植性差,映射要小心
Specification动态条件灵活复杂查询代码较绕
自定义 Repository控制力最高代码量更多

DTO 投影为什么重要

很多线上问题来自直接返回 Entity。

java
List<Asset> assets = assetRepository.findByStatus(1);
return assets;

风险:

  1. 暴露数据库字段。
  2. JSON 序列化触发懒加载。
  3. 双向关联导致循环引用。
  4. 列表接口加载了不需要的大字段。
  5. 后续修改表结构会影响接口结构。

DTO 投影:

java
public record AssetListItem(Long id, String assetCode, String assetName, String deptName) {
}
java
@Query("""
    select new com.demo.AssetListItem(a.id, a.assetCode, a.assetName, d.deptName)
    from Asset a
    join a.department d
    where a.status = :status
    order by a.id desc
""")
List<AssetListItem> listAssetItems(@Param("status") Integer status);

好处:

  1. 只查接口需要的字段。
  2. 避免返回完整对象图。
  3. 避免序列化阶段触发懒加载。
  4. 接口模型和数据库模型解耦。
  5. 更容易看清最终 SQL 意图。

Specification 动态查询全过程

页面筛选条件经常是动态的:

  1. 科室可选。
  2. 状态可选。
  3. 关键字可选。
  4. 时间范围可选。

如果用方法名查询,会组合爆炸。Specification 更适合这种场景。

java
public static Specification<Asset> hasStatus(Integer status) {
    return (root, query, cb) -> {
        if (status == null) {
            return cb.conjunction();
        }
        return cb.equal(root.get("status"), status);
    };
}

组合:

java
Specification<Asset> spec = Specification
        .where(hasStatus(query.status()))
        .and(hasDept(query.deptId()))
        .and(nameLike(query.keyword()));

Page<Asset> page = assetRepository.findAll(spec, pageable);

底层过程:

mermaid
flowchart TD
    A["多个 Specification"] --> B["组合 Predicate"]
    B --> C["CriteriaQuery"]
    C --> D["JPA Provider 生成 JPQL/SQL"]
    D --> E["执行分页查询"]
    E --> F["返回 Entity 或 DTO"]

Specification 不是万能的。复杂 join、复杂统计、数据库特有函数,写起来可能比 SQL 更难读。这时不要硬撑,改用 @Query、原生 SQL、MyBatis 或专门报表方案。

分页:Page 为什么慢

java
Page<Asset> page = assetRepository.findByStatus(1, pageable);

Page 通常会执行两类 SQL:

sql
select count(*) from asset where status = ?;
sql
select *
from asset
where status = ?
order by id desc
limit ?, ?;

流程:

mermaid
flowchart TD
    A["Repository 分页方法"] --> B["解析 Pageable 和 Sort"]
    B --> C["执行 count 查询"]
    C --> D["执行当前页数据查询"]
    D --> E["封装 total、pages、content"]

如果表很大、条件复杂、join 很多,count SQL 可能比数据查询还慢。

Slice 不查总数,只多查一条判断有没有下一页。

类型是否 count返回什么场景
Page总数、总页数、当前页数据后台管理需要精确页数
Slice当前页数据、是否有下一页信息流、滚动加载
游标分页基于上一页最后一条继续查大数据深分页

深分页问题:

sql
select * from asset order by id desc limit 1000000, 20;

数据库仍要扫描并跳过大量记录。优化方向:

  1. 限制最大页码。
  2. 使用游标分页:where id < lastId order by id desc limit 20
  3. 建合适索引。
  4. 列表页只查必要字段。
  5. 复杂检索交给 Elasticsearch。

@Modifying 更新为什么要事务

自定义更新:

java
@Modifying
@Query("update Asset a set a.status = :status where a.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") Integer status);

调用方:

java
@Transactional
public void disable(Long id) {
    assetRepository.updateStatus(id, 0);
}

@Modifying 告诉 Spring Data JPA:这不是查询,而是更新/删除语句。没有它,框架可能按查询处理。

为什么要事务?

  1. 更新 SQL 必须在事务中提交。
  2. 多个更新要一起成功或失败。
  3. 持久化上下文里可能已有旧实体,需要考虑清理或同步。

注意:批量 JPQL update 会绕过 Hibernate 对托管实体的逐个脏检查。若当前持久化上下文中已有相关实体,可能出现内存对象和数据库不一致。必要时使用 clearAutomatically 或手动清理上下文。

java
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("update Asset a set a.status = 0 where a.id = :id")
int disable(@Param("id") Long id);

商业 Demo:资产列表和状态变更

DTO

java
public record AssetListItem(
        Long id,
        String assetCode,
        String assetName,
        String deptName,
        Integer status
) {
}

Repository

java
public interface AssetRepository extends JpaRepository<Asset, Long>, JpaSpecificationExecutor<Asset> {
    Optional<Asset> findByAssetCode(String assetCode);

    @Query("""
        select new com.demo.AssetListItem(a.id, a.assetCode, a.assetName, d.deptName, a.status)
        from Asset a
        join a.department d
        where (:status is null or a.status = :status)
          and (:keyword is null or a.assetName like concat('%', :keyword, '%'))
        order by a.id desc
    """)
    Page<AssetListItem> pageList(@Param("status") Integer status,
                                 @Param("keyword") String keyword,
                                 Pageable pageable);

    @Modifying(clearAutomatically = true, flushAutomatically = true)
    @Query("update Asset a set a.status = :status where a.id = :id")
    int updateStatus(@Param("id") Long id, @Param("status") Integer status);
}

Service

java
@Service
public class AssetService {
    private final AssetRepository assetRepository;

    public AssetService(AssetRepository assetRepository) {
        this.assetRepository = assetRepository;
    }

    @Transactional(readOnly = true)
    public Page<AssetListItem> pageAssets(AssetQuery query) {
        int pageNo = Math.max(query.pageNo(), 1);
        int pageSize = Math.min(query.pageSize(), 100);
        Pageable pageable = PageRequest.of(pageNo - 1, pageSize);
        return assetRepository.pageList(query.status(), query.keyword(), pageable);
    }

    @Transactional(rollbackFor = Exception.class)
    public void disableAsset(Long id) {
        int rows = assetRepository.updateStatus(id, 0);
        if (rows == 0) {
            throw new IllegalArgumentException("资产不存在或状态未变化");
        }
    }
}

这个 Demo 的原则:

  1. 列表返回 DTO,不返回 Entity。
  2. 分页限制最大 pageSize
  3. 状态更新放在 Service 事务中。
  4. 更新影响行数必须判断。
  5. 高频查询上线前看 SQL 和执行计划。

线上排查流程

Repository 方法报错

排查:

  1. 方法名中的属性是否真实存在于 Entity。
  2. @Param 名称是否和 JPQL 参数一致。
  3. JPQL 写的是实体名/属性名,不是表名/列名。
  4. 原生 SQL 返回列是否能映射到 Entity 或 DTO。
  5. Repository 是否被扫描到。

查询慢

mermaid
flowchart TD
    A["接口慢"] --> B["打开 SQL 日志拿到最终 SQL"]
    B --> C["统计 SQL 次数"]
    C --> D{"是否 N+1?"}
    D -->|"是"| E["改 DTO 投影、fetch join 或批量查询"]
    D -->|"否"| F["数据库 EXPLAIN"]
    F --> G{"count 是否慢?"}
    G -->|"是"| H["优化 count、改 Slice 或游标分页"]
    G -->|"否"| I["检查索引、排序、回表、锁等待、返回字段"]

分页慢

排查:

  1. 是 count 慢还是数据查询慢。
  2. 是否深分页。
  3. order by 字段是否有合适索引。
  4. 是否返回 Entity 导致加载大字段或关联。
  5. 是否复杂 join 造成 count 成本过高。

懒加载异常

排查:

  1. Controller 是否直接返回 Entity。
  2. JSON 序列化是否访问懒加载字段。
  3. Service 是否在事务内完成 DTO 转换。
  4. 查询是否应该使用 DTO 投影或 fetch join。

常见坑

问题原因正确做法
方法名无限变长条件太多@Query、Specification 或自定义实现
返回 Entity字段暴露、懒加载、循环引用返回 DTO
Pagecount 慢或深分页优化 count、用 Slice 或游标
自定义 update 不生效@Modifying 或事务加注解和 Service 事务
save 误覆盖merge 游离对象空字段先查托管实体再改字段
Repository 写业务职责混乱业务放 Service
不看 SQL隐式 N+1 和低效 SQL开日志并 EXPLAIN

面试标准回答

Repository 接口为什么能执行

text
Spring Data JPA 启动时扫描 Repository 接口,根据泛型识别 Entity 和主键类型,为接口创建代理对象并注册成 Spring Bean。调用方法时,代理会判断是 JpaRepository 内置方法、方法名查询、@Query 还是自定义实现,然后调用 EntityManager,底层由 Hibernate 生成 SQL 并通过 JDBC 执行。

方法名查询原理是什么

text
Spring Data JPA 会解析 Repository 方法名,例如 findByStatusAndNameContaining,把操作类型、属性名、连接词和关键字解析成查询模型,再转换为 JPQL 或 Criteria,最后交给 Hibernate 生成 SQL。方法名查询适合简单固定条件,复杂条件应该换 @Query、Specification 或自定义实现。

Page 和 Slice 区别

text
Page 会执行 count 查询和数据查询,能返回总数和总页数;Slice 通常不查总数,只判断是否有下一页。大表、复杂 join 或信息流场景如果不需要精确总数,Slice 或游标分页更合适。

为什么不建议直接返回 Entity

text
Entity 是持久化模型,直接返回会让接口和表结构耦合,还可能暴露字段、触发懒加载异常、造成循环引用和隐式 N+1。商业项目通常在 Service 内查询并转换 DTO,列表页优先 DTO 投影。

Spring Data JPA 适合什么场景

text
它适合领域模型清晰、CRUD 较多、查询复杂度中低的系统。简单查询用方法名,固定复杂查询用 JPQL,动态条件用 Specification,数据库特性用原生 SQL。复杂报表、强 SQL 控制、高性能批处理场景要评估 MyBatis、原生 SQL 或数仓。

关联知识点

  1. Spring Data JPA 首页
  2. Repository
  3. 查询方式
  4. 事务与分页
  5. Spring Data JPA 实战建议
  6. Hibernate/JPA 核心全过程原理
  7. ORM 事务与一致性
  8. ORM 面试题

本章小结

Spring Data JPA 的核心不是“继承接口就完事”,而是 Repository 代理把方法调用转成 JPA 操作,EntityManager 交给 Hibernate 管理实体状态和生成 SQL。简单 CRUD 用它很高效,但一旦涉及复杂查询、分页性能、懒加载和事务边界,就必须回到最终 SQL、执行计划和持久化上下文这条主线。