简介这份资源面向具备一定Java基础、希望快速上手Spring Boot与JPA整合开发的开发者聚焦于用面向对象方式操作数据库这一核心场景。包内共10个文件以6个java源码为主辅以2个properties配置、1个xml与1个project工程文件压缩包约6KB体量轻巧便于直接导入IDE运行调试。内容围绕实体类定义、Repository接口、Service与Controller分层展开完整呈现从数据源配置到RESTful接口暴露的基础流程读者可据此理解Spring Data JPA自动配置、主键生成策略及CRUD方法调用等关键环节。目前已有254人学习适合作为入门练手或项目脚手架参考帮助读者快速搭建可运行的持久层结构并在此基础上扩展分页、条件查询与事务管理等进阶能力。1. 从一张“表结构改了三遍”的工单说起SpringBootJPA 到底能省掉多少事需求评审刚结束产品说“用户表加个字段顺便把订单关联查出来”。如果你手头是 MyBatis 那套 XML大概率要改实体、改 mapper、改 resultMap、再手动拼关联 SQL改完还得对着控制台看有没有漏字段。而换成 SpringBootJPA 这套组合多数场景下你只需要动实体类加一个注解Repository 里补一个派生查询方法名重启就能跑。这份资源给的就是一套能直接落地的 SpringBootJPA 工程骨架覆盖实体映射、Repository 派生查询、分页排序、事务边界和 REST 接口这几块最常写也最容易写乱的代码。适合正在从手写 SQL 往 ORM 迁移的后端也适合想搞清楚 JPA 那套“方法名即查询”到底怎么落地的开发者。下面按“能干什么 → 怎么跑 → 怎么改”的顺序拆开讲。2. 工程骨架与依赖先看清 SpringBootJPA 的分层再动手2.1 目录结构决定你后面改代码的手感拿到一份 SpringBootJPA 工程第一件事不是急着mvn spring-boot:run而是把目录扫一遍。这套骨架的典型分层是这样的src/main/java/com/example/demo ├── DemoApplication.java # 启动类SpringBootApplication ├── controller # REST 层只做参数校验和响应封装 │ └── UserController.java ├── service # 业务层事务边界在这里 │ ├── UserService.java │ └── impl/UserServiceImpl.java ├── repository # 数据访问层继承 JpaRepository │ └── UserRepository.java ├── entity # 实体层Entity 映射表 │ └── User.java └── dto # 出入参对象和 entity 解耦 ├── UserCreateRequest.java └── UserResponse.java分层本身不新鲜但 JPA 项目里最容易翻车的地方恰恰是“entity 直接当接口出入参用”。一旦表结构和接口契约绑死后面加个非数据库字段就得往实体上挂Transient越挂越乱。这套骨架把 dto 单独拆出来就是为了让你改表的时候不至于牵动接口。2.2 依赖只留必要的几个别一上来堆全家桶pom.xml里和 JPA 相关的核心依赖其实就三块Spring Boot 的 web starter、data-jpa starter加对应数据库驱动。常见做法是这样dependencies !-- Web 层提供 REST 能力 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- JPA HibernateORM 核心 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency !-- 数据库驱动示例用 MySQL换库只改这一处 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- 单元测试验证 Repository 派生查询 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies这里有个参数值得说mysql-connector-j的scope设成runtime意思是编译期不依赖驱动 API只在运行时加载。好处是换数据库时业务代码零改动坏处是编译期看不到驱动类写自定义方言时要注意。如果你用的是内存库做本地调试把驱动换成 H2 并把 scope 去掉即可。2.3 配置文件里那几个必须改对的项application.yml里 JPA 相关配置直接决定启动能不能过spring: datasource: url: jdbc:mysql://localhost:3306/demo_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: # 开发期用 update生产必须换 none 或 validate ddl-auto: update show-sql: true properties: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQLDialectddl-auto是这套组合里最需要警惕的参数。update在开发期很方便实体加字段它会自动改表但生产环境用它等于把表结构交给框架托管一旦实体写错字段类型可能被悄悄改掉。我一般开发用update上线前切validate让启动时校验实体和表是否一致不一致直接启动失败比运行到一半报错强。3. 核心模块拆解实体映射、Repository 派生查询与事务边界3.1 实体映射注解写对后面少一半麻烦实体类是 JPA 的地基注解写错后面全是玄学问题。一个带自增主键、时间戳和唯一约束的实体长这样Entity Table(name t_user, uniqueConstraints { UniqueConstraint(name uk_username, columnNames username) }) public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) // 依赖数据库自增 private Long id; Column(nullable false, length 64) private String username; Column(nullable false, length 128) private String email; Enumerated(EnumType.STRING) // 枚举按字符串存别用默认的 ORDINAL Column(length 16) private UserStatus status; Column(name created_at, updatable false) private LocalDateTime createdAt; PrePersist // 插入前自动填充 void onCreate() { this.createdAt LocalDateTime.now(); if (this.status null) { this.status UserStatus.ACTIVE; } } // getter / setter 省略 }几个关键点GeneratedValue(strategy IDENTITY)表示主键交给数据库自增适合 MySQL如果换 Oracle 常见做法是SEQUENCE。Enumerated一定要显式写EnumType.STRING默认的ORDINAL按枚举下标存哪天枚举顺序调一下历史数据全错位这是血泪经验。PrePersist用来做插入前的默认值填充比在 service 里到处 set 干净。3.2 Repository 派生查询方法名就是查询语句JpaRepository最爽的地方是派生查询方法名按规则写框架自动生成 SQLpublic interface UserRepository extends JpaRepositoryUser, Long { // 等价于 where username ? OptionalUser findByUsername(String username); // 等价于 where status ? order by created_at desc ListUser findByStatusOrderByCreatedAtDesc(UserStatus status); // 等价于 where email like %?% and status ? PageUser findByEmailContainingAndStatus(String keyword, UserStatus status, Pageable pageable); // 自定义 JPQL派生查询表达不了的场景用它 Query(select u from User u where u.createdAt :start and u.createdAt :end) ListUser findCreatedBetween(Param(start) LocalDateTime start, Param(end) LocalDateTime end); // 只查部分字段避免 select * Query(select new com.example.demo.dto.UserResponse(u.id, u.username, u.email) from User u where u.status :status) ListUserResponse findBriefByStatus(Param(status) UserStatus status); }派生查询的命名规则是findBy 条件 And/Or 排序。Containing对应like %?%StartingWith对应like ?%GreaterThanEqual对应。参数说明上Pageable是分页对象由 controller 层构造传进来PageUser返回的不只是数据还带总条数、总页数。最后那个new ...UserResponse(...)的写法叫构造器表达式直接查 DTO省掉一次实体到 DTO 的转换列表接口用它性能更好。3.3 事务边界别在 controller 上开事务事务该加在哪是 JPA 项目里争论最多的问题之一。这套骨架的做法是加在 service 实现类的方法上Service public class UserServiceImpl implements UserService { private final UserRepository userRepository; public UserServiceImpl(UserRepository userRepository) { this.userRepository userRepository; } Override Transactional(readOnly true) // 只读查询优化连接使用 public PageUserResponse pageByStatus(UserStatus status, int page, int size) { Pageable pageable PageRequest.of(page, size, Sort.by(createdAt).descending()); return userRepository.findByStatusOrderByCreatedAtDesc(status, pageable) .map(this::toResponse); } Override Transactional // 写操作默认回滚运行时异常 public UserResponse create(UserCreateRequest request) { userRepository.findByUsername(request.getUsername()).ifPresent(u - { throw new IllegalArgumentException(用户名已存在); }); User user new User(); user.setUsername(request.getUsername()); user.setEmail(request.getEmail()); return toResponse(userRepository.save(user)); } }Transactional(readOnly true)不只是语义标注Hibernate 会据此跳过脏检查查询性能有实际提升。写方法上的Transactional默认只对RuntimeException回滚如果你抛的是受检异常得显式写rollbackFor Exception.class这个坑很多人踩过。事务加在 service 而不是 controller是因为一个 service 方法可能调多个 repository事务要覆盖这一整段逻辑。3.4 REST 接口把分页参数和异常处理收口controller 层保持薄只做参数接收和响应封装RestController RequestMapping(/api/users) public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService userService; } GetMapping public PageUserResponse page(RequestParam(defaultValue ACTIVE) UserStatus status, RequestParam(defaultValue 0) int page, RequestParam(defaultValue 20) int size) { return userService.pageByStatus(status, page, size); } PostMapping ResponseStatus(HttpStatus.CREATED) public UserResponse create(RequestBody Valid UserCreateRequest request) { return userService.create(request); } }RequestParam的defaultValue让接口不传参也能跑方便前端联调。Valid配合 DTO 上的校验注解做入参校验校验失败会抛MethodArgumentNotValidException常见做法是配一个RestControllerAdvice统一转成标准错误响应避免把堆栈直接吐给前端。4. 跑起来与验证从建库到第一个接口返回4.1 建库和启动的最小步骤先把数据库准备好再启动应用-- 建库字符集和排序规则按业务选 CREATE DATABASE demo_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;# 确认 application.yml 里的账号密码和本地一致 # 然后启动 mvn spring-boot:run启动日志里看到Started DemoApplication就说明起来了。如果ddl-auto是update此时t_user表会被自动创建。第一次启动建议把show-sql打开确认建表语句和你的实体预期一致。4.2 用 curl 验证派生查询和分页接口起来后先插一条再查# 新增用户 curl -X POST http://localhost:8080/api/users \ -H Content-Type: application/json \ -d {username:alice,email:aliceexample.com} # 分页查询page 从 0 开始 curl http://localhost:8080/api/users?statusACTIVEpage0size10返回的 JSON 里除了content数组还有totalElements、totalPages、number这些分页元信息这是PageT序列化后的结构。如果totalElements一直是 0 但你确定库里有数据先检查status传的值和枚举名是否完全一致枚举匹配是大小写敏感的。4.3 用测试类锁住 Repository 行为派生查询的方法名一旦写错编译能过但运行时报“找不到属性”所以建议给 Repository 配测试DataJpaTest // 只加载 JPA 相关上下文启动快 class UserRepositoryTest { Autowired private UserRepository userRepository; Test void findByUsername_shouldReturnUser() { User user new User(); user.setUsername(bob); user.setEmail(bobexample.com); userRepository.save(user); OptionalUser found userRepository.findByUsername(bob); assertThat(found).isPresent(); assertThat(found.get().getEmail()).isEqualTo(bobexample.com); } }DataJpaTest默认每个测试方法跑在事务里并回滚不会污染数据库。它只装配 JPA 相关 bean比SpringBootTest快很多适合验证派生查询和 JPQL 是否正确。5. 避坑与排查JPA 项目里最容易翻车的几处5.1 N1 查询列表接口慢得莫名其妙现象查 20 条订单控制台打了 21 条 SQL。原因实体上配了OneToMany懒加载遍历时每条记录触发一次关联查询。解决用EntityGraph或 JPQL 的join fetch一次性把关联查出来比如Query(select distinct o from Order o join fetch o.items where o.status :status)。列表接口上线前一定要看 SQL 条数这是最常见的性能坑。5.2 懒加载异常LazyInitializationException现象service 里查出来的实体在 controller 序列化时报could not initialize proxy - no Session。原因事务在 service 方法结束时已提交session 关闭controller 再访问懒加载属性就取不到。解决要么在 service 里就把需要的字段转成 DTO要么给查询方法加Transactional(readOnly true)把 session 撑到序列化结束。我一般选前者DTO 转换放在事务内边界清晰。5.3 ddl-auto 用错生产表结构被改现象上线后某个字段类型变了或者多了个没设计的列。原因生产配置里ddl-auto还是update。解决生产环境改成validate让应用启动时只校验不修改表结构变更走独立的迁移脚本。这个参数改错一次后悔药可不好买。5.4 派生查询方法名拼错启动就报错现象应用启动失败日志里No property xxx found for type User。原因方法名里的属性名和实体字段对不上比如实体是createdAt方法写成findByCreateAt。解决对照实体字段逐个核对IDE 里方法名会有高亮提示别硬背。命名规范上属性名驼峰方法名里的条件词首字母大写。5.5 事务自调用失效加了注解却不回滚现象service 里 A 方法调本类的 B 方法B 上标了Transactional但 B 抛异常后数据没回滚。原因Spring 事务基于代理自调用不走代理注解不生效。解决把 B 抽到另一个 bean或者通过注入自身代理调用。这个坑隐蔽性强排查时先看调用是不是跨了代理。6. 二次开发把骨架改成你自己的业务模块拿到这套骨架后二次开发基本是“复制一条链路再改”的节奏。以新增一个“订单”模块为例步骤是固定的先在entity下建Order实体字段和注解照User抄再在repository下建OrderRepository继承JpaRepositoryOrder, Long把常用查询写成派生方法然后service下建接口和实现事务注解照旧最后controller暴露接口DTO 单独建。整条链路复制下来十分钟能跑通。改造时有几个技巧值得记。第一公共字段抽基类。createdAt、updatedAt、id这些每个实体都有抽一个BaseEntity用MappedSuperclass标注子类继承即可PrePersist和PreUpdate也放基类里省得每个实体重复写。第二软删除用Where或SQLDelete。如果业务要逻辑删除别在每个查询里手动加deleted false在实体上加Where(clause deleted false)框架自动过滤但要注意它对手写原生 SQL 不生效。第三分页参数统一封装。controller 里到处写PageRequest.of很啰嗦抽一个PageQuery对象接收page、size、sort在 service 里统一转换前端传参格式也统一。验证改造是否成功我的习惯是三步走先跑DataJpaTest确认 Repository 层查询正确再用 curl 打一遍接口确认序列化没问题最后打开show-sql看一遍实际 SQL 有没有 N1 或多查。这三步走完基本能挡住八成问题。从那以后我每次加新模块都强制先把实体和 Repository 的测试写完再写 service返工次数明显少了。希望帮到你。本文还有配套的精品资源点击获取