CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Spring Boot 实战入门/课时/Spring Data JPA 入门
RawBlame
课时目录16 节

Spring Boot 实战入门

  1. 01Spring Boot 是什么
  2. 02Starter 与自动配置
  3. 03配置与 Profile
  4. 04IoC 与依赖注入
  5. 05Bean 作用域与生命周期
  6. 06Spring AOP 入门
  7. 07Spring MVC 与 REST
  8. 08参数校验与异常处理
  9. 09JDBC 与 MyBatis
  10. 10Spring Data JPA 入门
  11. 11事务管理
  12. 12Spring Security 基础
  13. 13JWT 与 API 鉴权
  14. 14单元测试与 MockMvc
  15. 15Actuator 与健康检查
  16. 16综合练习与进阶路线

Spring Data JPA 入门

Entity、Repository、何时选 JPA vs MyBatis。

35 分钟

Spring Data JPA 入门

这一课你将能做到

  1. 定义 JPA 实体并映射到表,用注解控制主键策略、列名和枚举存储
  2. 用方法名派生查询零 SQL 实现常用检索,分页一行代码搞定
  3. 对比 JPA 与 MyBatis 的适用场景,能说清 save 的更新原理和 N+1 问题

为什么要学

上一课 MyBatis 的思路是「SQL 由你写,框架帮你搬」;这一课的 JPA 反过来——你描述「实体长什么样、要查什么」,框架自己生成 SQL。标准 CRUD 一个 SQL 都不用写,改个字段连 SQL 都不用动。

JPA(Java Persistence API,现为 Jakarta Persistence)是 ORM 规范,Hibernate 是它的参考实现,Spring Data JPA 再封装一层仓库接口。国内业务系统 MyBatis 系占多数,但 JPA 在海外项目、快速原型、领域驱动设计项目中非常常见,而且「实体映射、主键策略、关联关系」这些概念两边通用。两个都会,选型时才有话语权。

先建立图像

三层关系一图流:

你的代码
TaskRepository extends JpaRepository   ← Spring Data 提供 CRUD 实现
        ↓ 操作的是
Task 实体(@Entity,映射 task 表)
        ↓ 实际执行
Hibernate(JPA 实现)负责生成 SQL、管理实体状态、脏检查
        ↓
数据库

JPA 最特别的概念是实体有「状态」:从库里查出来的对象被 Hibernate「托管」,你改了它的字段,事务提交时 Hibernate 自动比对并发 UPDATE——不用显式调用 update。这个「脏检查」机制是 save 行为、N+1 问题、缓存问题的共同源头。

名词人话
Entity被 @Entity 标注、与表一一对应的类
Repository数据访问接口,方法名即查询语句
持久化上下文Hibernate 的「托管区」,管着本事务内所有实体的状态
脏检查事务提交前对比实体快照,变了就自动 UPDATE

跟着做

步骤 1:引入依赖与配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
spring:
  jpa:
    hibernate:
      ddl-auto: update        # 开发期自动按实体更新表结构;生产禁用,用迁移工具
    show-sql: true            # 控制台打印 SQL
    properties:
      hibernate:
        format_sql: true

注意与上一课互斥:JDBC/MyBatis 与 JPA 都引时数据源共用没问题,但别让两套框架建同一张表。本课建议把上一课的 schema.sql 暂时移走,让 JPA 自己建表。

步骤 2:定义实体

package com.example.taskflow.entity;

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "task")
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)   // 数据库自增
    private Long id;

    @Column(nullable = false, length = 50)
    private String title;

    @Column(nullable = false)
    private Integer priority;

    @Enumerated(EnumType.STRING)      // 枚举按名字存 varchar,别用 ORDINAL
    @Column(nullable = false, length = 20)
    private TaskStatus status = TaskStatus.TODO;

    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @PrePersist                       // 插入前回调:自动填创建时间
    public void prePersist() {
        if (createdAt == null) createdAt = LocalDateTime.now();
    }

    // JPA 实体规范要求:必须有无参构造(protected 即可)
    protected Task() { }

    public Task(String title, Integer priority) {
        this.title = title;
        this.priority = priority;
    }

    // getter/setter 全部生成(IDE: Alt+Insert),此处省略
    public Long getId() { return id; }
    public String getTitle() { return title; }
    public Integer getPriority() { return priority; }
    public TaskStatus getStatus() { return status; }
    public void setStatus(TaskStatus status) { this.status = status; }
    public LocalDateTime getCreatedAt() { return createdAt; }

    public enum TaskStatus { TODO, DOING, DONE }
}

关键注解读法:@Id + @GeneratedValue 定主键;@Column 控制列的 null/长度/列名;@Enumerated(STRING) 避免枚举按序号存储后「插队一个枚举值全表错位」的惨案;@PrePersist 是实体的初始化钩子(对应上一课 Bean 的 @PostConstruct)。

步骤 3:Repository——方法名即查询

package com.example.taskflow.repository;

import com.example.taskflow.entity.Task;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface TaskRepository extends JpaRepository<Task, Long> {

    // 以下全部不用写实现,方法名解析成 WHERE 条件
    List<Task> findByStatus(Task.TaskStatus status);

    List<Task> findByTitleContainingOrderByPriorityAsc(String keyword);

    List<Task> findByPriorityLessThanEqual(Integer maxPriority);

    long countByStatus(Task.TaskStatus status);

    // 复杂查询退回 JPQL(面向实体的 SQL)
    @Query("select t from Task t where t.status = :status and t.priority <= :max")
    List<Task> findUrgent(@Param("status") Task.TaskStatus status, @Param("max") int max);

    // 分页:参数传 Pageable,返回带总数与分页信息的 Page
    Page<Task> findByStatus(Task.TaskStatus status, Pageable pageable);
}

方法名关键字速查:And、Or、Between、LessThan(Equal)、GreaterThan(Equal)、Containing(LIKE %x%)、StartingWith、In、IsNull、OrderBy 字段 Asc/Desc。IDEA 会实时提示可组合的字段,写错直接标红。

步骤 4:Service 与 CRUD 验证

package com.example.taskflow.service;

import com.example.taskflow.entity.Task;
import com.example.taskflow.repository.TaskRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class TaskJpaService {

    private final TaskRepository taskRepository;

    public TaskJpaService(TaskRepository taskRepository) {
        this.taskRepository = taskRepository;
    }

    @Transactional
    public Task create(String title, int priority) {
        return taskRepository.save(new Task(title, priority));   // 新对象 → INSERT
    }

    @Transactional
    public Task complete(long id) {
        Task task = taskRepository.findById(id)
                .orElseThrow(() -> new BusinessException(ErrorCode.TASK_NOT_FOUND));
        task.setStatus(Task.TaskStatus.DONE);
        return task;      // 不调用 save!托管实体被修改,事务提交时自动 UPDATE
    }

    @Transactional(readOnly = true)
    public Page<Task> page(int page, int size) {
        return taskRepository.findAll(
                PageRequest.of(page - 1, size, Sort.by("priority").ascending()));
    }

    @Transactional(readOnly = true)
    public List<Task> urgent() {
        return taskRepository.findUrgent(Task.TaskStatus.TODO, 3);
    }
}

**「complete 不调用 save 也生效」**是 JPA 的招牌行为:findById 返回的实体在事务内被持久化上下文托管,事务提交时 Hibernate 脏检查发现 status 变了,自动发 UPDATE。忘记这一点,就会写出事务外改实体「改了个寂寞」的 bug。

Controller 直接复用第 7 课的套路(构造器注入 Service + 统一响应),不再重复。

步骤 5:分页结果长什么样

访问分页接口(page=1, size=10),返回体:

{
  "content": [ {"id":1,"title":"学习 Spring Boot 配置",...} ],
  "totalElements": 25,
  "totalPages": 3,
  "number": 0,
  "size": 10
}

content 是当前页数据,totalElements 驱动前端总数展示,number 从 0 起算。对比 MyBatis 需要单独写 COUNT 语句,这里一条都没写。注意 Page 直接作为 JSON 返回会输出一堆执行时才算的字段,生产建议转成自己的分页 DTO(total、pages、records 三件套)。

步骤 6:看一眼 Hibernate 生成的 SQL

show-sql: true 后控制台(节选):

Hibernate: insert into task (title,priority,status,created_at) values (?,?,?,?)
Hibernate: select t1_0.id,t1_0.created_at,t1_0.priority,t1_0.status,t1_0.title from task t1_0 where t1_0.id=?
Hibernate: update task set status=? where id=?

三步对应 create → findById → complete 的自动 UPDATE。「JPA 不写 SQL」的真相是「SQL 换个地方生成」——性能出问题时依然要回到 SQL 层面分析,这也是你先学 MyBatis 再学 JPA 的价值。

讲清楚要点

  1. save 的两条路径要分清:对象是新的(id 为 null)→ persist → INSERT;对象有 id 且被托管 → 脏检查 → UPDATE。游离对象(事务外查出来再改)save 会先 SELECT 比对再决定合并——理解这个机制才能解释「为什么改了没存上」或「为什么多了一条 SELECT」。
  2. readOnly = true 是免费的优化:只读事务 Hibernate 关闭脏检查快照,还能让驱动做只读优化;查询方法养成标注习惯。
  3. ddl-auto 的生产纪律:update/create-drop 只属于开发期;生产表结构变更用 Flyway/Liquibase 迁移脚本管理,谁能跑 SQL 变更必须有审批。
  4. 关联关系先学「用」,再学「映射」:真实项目大量使用关联查询,但 @OneToMany/@ManyToOne 的级联、懒加载语义复杂,初学者先掌握单表 + 显式 JOIN 查询(@Query),不着急上完整对象图。
  5. N+1 问题(进阶必知):查 1 次 list 再对每条记录查 1 次关联 = 1 + N 条 SQL。解法:@EntityGraph、JOIN FETCH 的 JPQL。先用 SQL 日志训练自己「看见 N+1」。

你可能会懵的地方

  • JPA、Hibernate、Spring Data JPA 三个名字什么关系? JPA 是规范(接口和注解标准);Hibernate 是实现(真正生成 SQL 的引擎);Spring Data JPA 是 Spring 的封装(Repository 抽象、方法名解析、事务集成)。你写的代码对着 Spring Data JPA,底层干活的是 Hibernate。

  • 为什么实体必须有 protected 无参构造? Hibernate 通过反射创建实例再逐字段填充,没有无参构造它无从下手。这是规范的硬性要求,别把构造器全私有化。

  • @Transactional 加在哪一层? Service 层。Repository 的内置方法自带事务(SimpleJpaRepository 已标注),但跨多个 Repository 操作时必须在 Service 方法上开事务,保证「多个写操作同生共死」——下一课专门展开。

  • id 用 Long 还是 long? Long。新增场景 id 还没生成,基本类型 long 只能是 0,无法表达「未持久化」;包装类型 null 语义清晰,save 后 Hibernate 回填。

  • JPA 能写原生 SQL 吗? 能:@Query(value = "...", nativeQuery = true),返回原生类型或投影接口。复杂报表、DBA 优化过的 SQL 都走这条路——「纯 JPA 不写 SQL」从来不是教条。

错法对照

错法问题正解
事务外修改实体(没 @Transactional)脏检查不触发,改了不生效修改操作放进事务方法
枚举用 @Enumerated(ORDINAL)插入新枚举值导致历史数据语义错乱@Enumerated(EnumType.STRING)
生产环境 ddl-auto: update表结构失控,无变更记录生产用 Flyway/Liquibase
实体当接口出入参直接返回懒加载字段序列化报错、字段耦合返回独立 DTO
findById 放循环里逐条查N+1,DB 压力爆炸批量 findByIdAll / JOIN FETCH

卡点急救

  1. 启动报 No identifier specified for entity:实体没标 @Id,或注解 import 错成了 Spring 的(必须是 jakarta.persistence.Id)。
  2. 修改实体不生效:99% 是不在事务内。给 Service 方法加 @Transactional;确认调用经过代理(同类 this 调用会失效,参考第 6 课)。
  3. PersistentObjectException: detached entity passed to persist:给「已有 id 的对象」save 却被当成新增。更新场景先 findById 再改字段,或用 merge 语义(save 本身兼容,但要保证主键策略正确)。
  4. 建表没生成:ddl-auto 配置确认是 update/create;检查实体是否在扫描包内;H2 内存库每次重启清空属正常,用 DB_CLOSE_DELAY=-1 保持会话内数据。

面试常问

  • save 和 saveAndFlush 的区别?save 的更新原理? save 按主键判断新增或更新:新对象 persist,已有对象 merge;INSERT/UPDATE 通常延迟到事务提交或 flush 时执行。saveAndFlush 立即刷库(同事务内要用生成的主键或捕获约束异常时用)。更新的本质是持久化上下文的脏检查,不是显式 UPDATE 语句。

  • 什么是 N+1 问题?怎么解决? 查询主表 1 次后,对每条记录再发 1 次关联查询,共 1+N 条 SQL。解决:JPQL 的 JOIN FETCH、@EntityGraph、批量 IN 查询;发现手段是开 SQL 日志观察条数。

  • JPA 与 MyBatis 怎么选型? JPA:标准 CRUD、领域模型驱动、跨库移植、快速开发占优;MyBatis:SQL 可控、复杂关联与调优、DBA 协作友好、国内生态更广。混合使用也是常见实践:CRUD 走 JPA、报表走原生 SQL。

  • JPA 实体的生命周期状态? 瞬态(new 出来未托管)、托管(在持久化上下文中,脏检查生效)、游离(session 关闭后仍带 id)、删除(待删除)。托管态的修改会在 flush 时自动同步,这是理解一切 JPA 怪现象的钥匙。

自测三问

  1. 「不调用 save 直接改实体字段也能落库」的机制叫什么?它的前提条件是什么?
  2. 方法名派生查询 findByStatusAndPriorityLessThan 会生成什么样的 WHERE?
  3. ddl-auto 四个值 update / create / create-drop / validate 各适合什么环境?

小结一句话

JPA 把「实体 ↔ 表」双向映射交给框架,标准 CRUD 零 SQL、分页一行搞定——但托管与脏检查的心智模型不建立,坑就藏在「为什么没存上」里。

延伸

  • Spring Data JPA 官方文档
  • Hibernate ORM 文档
  • 下一课:事务管理——让「多步写操作」要么全成、要么全不算。
加载本节练习…
← 上一课JDBC 与 MyBatis下一课 →事务管理