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 代理,解析方法名、分页、排序JpaRepository、RepositoryFactory
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
Page 慢count 慢或深分页优化 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、执行计划和持久化上下文这条主线。