Spring Boot实战:基于MySQL的CRUD接口开发与分层架构

发布时间:2026/10/10 6:30:17

Spring Boot实战:基于MySQL的CRUD接口开发与分层架构 这套 Spring Boot 系列的第 2 课我们直接进入正题用 Spring Boot 从数据库里把增删改查CRUD做出来。上一课我的目标是帮大家把工程跑起来能在浏览器里看到一个 Hello World这一课开始你写的接口会真正读写 MySQL数据不会因为重启就消失这会是一个能上真实项目的雏形。适合刚入门 Spring Boot、会用 Maven 建工程、但还不清楚数据层怎么写的人。我会从选型思路讲到最终接口跑通再把现场最容易踩的坑逐个拆开。整个过程我会尽量说清楚背后的原因而不是只丢一段代码让你抄。1. 动手前先想清楚CRUD 接口的设计与选型思路很多新手拿到“增删改查”四个字就直接开写代码这没错但容易写出一个所有逻辑都堆在 Controller 里的工程后期一加功能就乱。所以开头我先花点篇幅把设计思路理一理。1.1 四个动作对应四类 HTTP 请求CRUD 是 Create新增、Read查询、Update更新、Delete删除的缩写。在 RESTful 风格的接口里这四个动作不靠方法名区分而是靠 HTTP 方法和 URL 区分。POST /api/users新增一个用户GET /api/users查询用户列表GET /api/users/{id}查询单个用户PUT /api/users/{id}更新某个用户DELETE /api/users/{id}删除某个用户为什么要这么设计因为 URL 描述的是“资源”HTTP 方法描述的是“操作”。拿着这个规则去设计接口前后端联调时不用看文档就能猜出大概非常省沟通成本。第 2 课这四个接口正好覆盖 RESTful 最基本的用法后面的课程再扩展分页、条件查询时也仍然沿用这套规则。1.2 选 Spring Data JPA 还是 MyBatis国内用 Spring Boot 做数据访问主流无非两条路Spring Data JPA底层是 Hibernate和 MyBatis。对比点Spring Data JPAMyBatis开发速度简单 CRUD 极快不用写 SQL需要手写 SQL代码量多SQL 可控性复杂查询需要 JPQL 或原生 SQLSQL 完全可控调优方便学习曲线有 ORM 概念门槛但上手不难SQL 熟练的人几乎零门槛适合场景业务相对标准、CRUD 密集复杂查询、报表、SQL 优化要求高教学友好度代码少适合讲清分层思路代码多容易迷失在 SQL 里这一课我选择 Spring Data JPA主要原因不是它比 MyBatis 好而是对初学者来说它能用最少的代码把“分层思想”讲明白——实体、仓库、服务、控制器各司其职。等你理解了这套分层结构再去学 MyBatis会发现只是把“数据访问实现”换了一种写法架构思路完全复用。如果你想在生产项目里用 MyBatis这套分层照样适用。1.3 工程结构按层拆别把所有代码塞进 Controller我见过不少新手项目Controller 里直接写数据库查询甚至把 JDBC 代码堆在接口方法里。这在几十行的 Demo 中能跑但只要加一个业务判断Controller 就膨胀到几百行。这一课的工程结构我建议这样拆com.example.demo ├── controller // 接收 HTTP 请求做参数绑定 ├── service // 业务逻辑的处理层 ├── repository // 数据访问层Spring Data 仓库接口 └── entity // 实体类对应数据库表各层的依赖关系是Controller → Service → Repository → Entity。Controller 不直接访问 Repository这是为了把“接口参数处理”和“业务逻辑”解耦。比如以后注册用户时要加“检查用户名是否重复”的逻辑只需要改 Service 层Controller 一行不用动。第 2 课的业务逻辑很薄但你从第一课起就养成这个分层习惯后面会省很多重构的力气。2. 准备工作三步走依赖、配置、实体映射在写增删改查之前得先把项目环境和数据库连接搞定。这一节的三件事缺一不可依赖决定你能用什么配置决定项目连到哪台数据库实体类决定 Java 对象和数据库表之间怎么对应。2.1 pom.xml 加依赖注意版本别踩坑新建 Spring Boot 工程时我建议先用 2.7.x 版本。不是 3.x 不好而是 3.x 全系列把javax.*换成了jakarta.*网上大量旧教程还是写javax.persistence.*对上版本容易抄错。第 2 课先用 2.7.x 把流程跑通以后升级了心里也有底。在pom.xml里需要加四个依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependencyspring-boot-starter-web提供 Spring MVC 能力用来写接口。spring-boot-starter-data-jpa整合 Hibernate 和 Spring Data提供 ORM 能力。mysql-connector-javaMySQL 驱动注意 scope 是 runtime因为它只在运行时被加载。lombok用Data等注解省掉 getter/setter 代码减少实体类的模板噪音。如果不习惯 Lombok可以不用但代码会明显变长。2.2 application.yml 把数据库连接配好新建一个数据库springboot_demo然后在src/main/resources/application.yml里写配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/springboot_demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true重点说几个配置的含义serverTimezoneAsia/Shanghai必须加。MySQL 8 的驱动对时区很敏感不加会报The server time zone value ... is unrecognized之类的错误加了能省掉一个常见的启动失败。ddl-auto是最容易让新手困惑的项它有五个取值取值行为适用场景none不做任何表结构变更生产环境validate校验实体与表是否一致不一致报错生产环境严格模式update对比实体和表新增缺失的表和列开发阶段推荐create每次启动先删表再建表纯测试数据会丢create-drop启动建表关闭时删表测试类场景第 2 课用update最舒服启动时如果表不存在Hibernate 会自动帮你建表以后实体字段变了它也会自动加列。生产环境请用validate或干脆用数据库迁移工具之后可以专门聊。2.3 写实体类先用一张 user 表练手实体类是 Java 对象和数据库表之间的桥。我先建一个 User 实体对应一张t_user表package com.example.demo.entity; import lombok.Data; import javax.persistence.*; Data Entity Table(name t_user) public class User { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(nullable false, length 50) private String username; Column(unique true, length 100) private String email; Column private Integer age; }几个关键点在代码背后Table(name t_user)我把表名定为t_user而不是user。因为user在 MySQL 某些版本和场景里可能被当成特殊词用t_前缀能避免一堆莫名其妙的坑。字段名方面Hibernate 默认会把驼峰命名的username转成下划线格式但如果表是手工建的你需要在Column里显式写name。主键生成策略用GenerationType.IDENTITY对应 MySQL 的AUTO_INCREMENT。这里的id我用的是包装类Long而不是基本类型long原因在于查询数据库里不存在的那条记录时实体对象的id会是null而不是0判断起来更清晰。3. 完整实操增删改查四个接口从零到通准备工作做完现在开始写代码。这一节你会看到数据访问如何只写一个接口就具备增删改查的全部能力、业务逻辑为什么必须单独一层、以及 Controller 如何用几个注解就把 HTTP 请求变成方法调用。3.1 Repository一行接口就拥有所有数据库操作能力在repository包下建一个接口package com.example.demo.repository; import com.example.demo.entity.User; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepositoryUser, Long { }你没看错一个空接口就够了。Spring Data JPA 会在启动时自动为你生成实现类这个实现类包含save()、findById()、findAll()、deleteById()等一大批现成方法。它的底层逻辑是方法名解析机制框架看到findById就解析成“根据主键查询”看到findAll就解析成“查询全部”。如果你需要按邮箱精确查找直接在接口里加一句OptionalUser findByEmail(String email);框架会根据方法名自动生成where email ?的查询不需要写 SQL。这个机制在第 2 课先了解即可后面写条件查询时再深挖。3.2 Service业务逻辑放这里别让 Controller 碰数据库Service 层我通常写成“接口 实现类”两步。第 2 课逻辑很薄这样写看起来有点“过度设计”但这个习惯能帮你处理未来更复杂的场景比如一个“创建用户”操作要同时写日志、发通知、校验唯一性这些全要放在 Service 里。先定义接口package com.example.demo.service; import com.example.demo.entity.User; import java.util.List; public interface UserService { User createUser(User user); User updateUser(Long id, User user); void deleteUser(Long id); User getUserById(Long id); ListUser getAllUsers(); }再写实现类package com.example.demo.service.impl; import com.example.demo.entity.User; import com.example.demo.repository.UserRepository; import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; Service Transactional public class UserServiceImpl implements UserService { Autowired private UserRepository userRepository; Override public User createUser(User user) { return userRepository.save(user); } Override public User updateUser(Long id, User user) { User existUser userRepository.findById(id) .orElseThrow(() - new RuntimeException(用户不存在id id)); existUser.setUsername(user.getUsername()); existUser.setEmail(user.getEmail()); existUser.setAge(user.getAge()); return userRepository.save(existUser); } Override public void deleteUser(Long id) { userRepository.deleteById(id); } Override public User getUserById(Long id) { return userRepository.findById(id) .orElseThrow(() - new RuntimeException(用户不存在id id)); } Override public ListUser getAllUsers() { return userRepository.findAll(); } }提两个细节。Transactional加在类上作用是把每个方法包在一个数据库事务里。createUser里如果失败事务会回滚不会出现“写了一半”的脏数据。第 2 课我全程在写单表操作感觉不明显等以后一个方法里连续改三张表这个注解非常关键。更新操作为什么要先findById再set再save因为直接用一个前端传过来的 User 对象去save会把id也覆盖掉更要命的是如果前端没有传某个字段那个字段会被写成null。先查出数据库里已有的实体再只修改允许修改的字段是防止“字段被误覆盖”的标准写法。3.3 Controller暴露 RESTful API 的最后一道门Controller 的职责只有一个把 HTTP 请求转换成 Service 方法的调用参数再把方法的返回值转成 HTTP 响应。判断一个 Controller 写得好不好就看它有没有出现 if/else 和业务代码。package com.example.demo.controller; import com.example.demo.entity.User; import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import java.util.List; RestController RequestMapping(/api/users) public class UserController { Autowired private UserService userService; PostMapping ResponseStatus(HttpStatus.CREATED) public User createUser(RequestBody User user) { return userService.createUser(user); } GetMapping public ListUser getAllUsers() { return userService.getAllUsers(); } GetMapping(/{id}) public User getUserById(PathVariable Long id) { return userService.getUserById(id); } PutMapping(/{id}) public User updateUser(PathVariable Long id, RequestBody User user) { return userService.updateUser(id, user); } DeleteMapping(/{id}) ResponseStatus(HttpStatus.NO_CONTENT) public void deleteUser(PathVariable Long id) { userService.deleteUser(id); } }逐行拆解一下注解RestController相当于ControllerResponseBody方法返回值直接写入 HTTP 响应体返回的对象会被 Jackson 自动序列化成 JSON。RequestMapping(/api/users)给整个 Controller 定一个统一的 URL 前缀。PostMapping/GetMapping/PutMapping/DeleteMapping分别对应 POST、GET、PUT、DELETE 方法。PathVariable把 URL 路径里的{id}绑定到方法的id参数上。RequestBody把前端传来的 JSON 反序列化成 User 对象。ResponseStatus(HttpStatus.CREATED)新增成功后返回 201而不是默认的 200删除成功后返回 204 而不是 200。这是 REST 语义的一部分前端同学会很感激这种规范。新增和删除的响应状态码是初学者最容易忽略的点。201 表示“资源已创建”204 表示“无内容操作成功”这两个语义比一律返回 200 更精确。3.4 用 curl 把四个接口完整跑一遍代码写完启动 Spring Boot 工程确认控制台没有报错。第一次启动时Hibernate 会打印建表语句并自动创建t_user表。接着用终端里的 curl 做全流程验证比打开 Postman 更快新增用户curl -X POST http://localhost:8080/api/users \ -H Content-Type: application/json \ -d {username:user001,email:user001example.com,age:25}预期返回{id:1,username:user001,email:user001example.com,age:25}查询列表curl http://localhost:8080/api/users查询单条curl http://localhost:8080/api/users/1更新用户curl -X PUT http://localhost:8080/api/users/1 \ -H Content-Type: application/json \ -d {username:user002,email:user002example.com,age:26}删除用户curl -X DELETE http://localhost:8080/api/users/1删除后再次访问/api/users/1应该得到一个 500 错误因为 Service 里抛出了RuntimeException。这个错误页面不好看下一课我会讲全局异常处理怎么把这些错误统一成规范的 JSON 响应。第 2 课先保证接口能通异常处理先埋个伏笔。4. 现场排错最容易出现的五个坑这一节我把实际带项目时新手问得最多的五个问题整理出来。每个问题都对应一个非常具体的症状你可以直接按症状定位。4.1 启动失败数据库连接相关的连环问题最常见的启动失败场景几乎都集中在数据库连接上报错各不一样但根因就那么几个。报错关键信息原因处理方式Unknown database springboot_demo数据库没创建先去 MySQL 里执行CREATE DATABASE springboot_demo DEFAULT CHARSET utf8mb4;Access denied for user rootlocalhost用户名或密码错误检查application.yml里的passwordPublic Key Retrieval is not allowedMySQL 8 驱动默认不允许取公钥连接串加allowPublicKeyRetrievaltrueCommunications link failure数据库服务没启动或端口不对确认 MySQL 已启动端口是否 3306The server time zone value ... is unrecognized缺时区参数连接串加serverTimezoneAsia/Shanghai一句经验之谈遇到启动报错先看最上面的几行异常不要盯着最后一段堆栈。Spring Boot 的报错信息往往很长真正的根因通常在Caused by:后面的部分。4.2 数据写不进表表结构与主键策略的问题数据库能连上启动也正常但插入数据时报错或没有数据这类问题要分两看。如果你看到Field id doesnt have a default value基本可以断定主键策略没生效。检查实体类的GeneratedValue(strategy GenerationType.IDENTITY)是否写上了以及数据库表的id字段是不是自增类型。如果你之前是用create模式建的表又手工在数据库改了表结构两边不一致也会出现各种奇怪问题。还有种情况ddl-auto用的是update但表是老手工建的字段类型跟实体类不一致比如数据库里age是varchar实体里是Integer插入时可能报数据截断。解决办法很简单开发阶段把表删了让 Hibernate 重建但注意这会丢数据只适合测试环境。4.3 返回结果全是 nullJSON 序列化在跟你作对接口调通了但返回的 JSON 长这样{id:null,username:null,email:null,age:null}。这个问题的根因通常在 Jackson 序列化环节。Jackson 序列化时靠实体类的 getter 方法拿字段值而不是直接读字段。如果你用了 Lombok检查有没有把Data注解正确加上如果手动写的 getter检查方法名是不是符合getXxx()格式。IDE 里常有“代码改了但没重新编译”的情况清一下 target 目录重新构建。另一个容易忽略的情况是实体类某个字段加了 JSON 序列化忽略注解比如JsonIgnore那这个字段永远返回 null。这是因为设计上不想暴露字段比如密码但新手容易忘记这件事看到 null 还以为程序错了。4.4 更新接口把不想改的字段覆盖掉了这是最典型、也最伤数据的一个坑。很多新手写的更新逻辑是直接把前端传来的 User 对象save进去userRepository.save(user);问题在于前端只传了username和email没有传age那么user对象的age就是nullsave之后数据库里的age就被清空了。更危险的是如果前端传了一个不含id的对象save会把它当成新记录直接插入一条重复数据。正确做法就是我前面 Service 层演示的先从数据库查出已有实体再逐字段更新最后再save。这个写法看起来多写了几行但它保证了“只改你想改的字段”。4.5 方法名一个字母拼错项目直接起不来Spring Data JPA 的方法名解析是按约定来的比如findByUsername、findByEmailContaining。如果你把方法名写错比如findByEmaillSpring Boot 启动时就会直接报错Failed to create query method ... No property emaill found这里的经验是接口里每增加一个自定义方法启动时框架就会解析一次方法名这相当于白送了一道编译期检查。遇到这种报错别慌它会明确告诉你“找不到某个属性”对照实体类的字段名检查一遍大小写和拼写即可。还有个小技巧如果你只是要按主键查询直接用现成的findById()不需要自己定义方法名。只有当查询条件不满足内置方法时才去扩展自定义方法。最后分享一个我在实际排错时养成的习惯每次写完一个 CRUD 接口我不会只调用一次正常参数就收工而是把异常情况也都过一遍——传一个不存在的 id 看返回什么传空的 body 看会不会报错开着show-sql观察 Hibernate 实际执行的 SQL。界面通只是第一步真正的成长都发生在排查这些边界情况的过程中。下一课可以聊统一返回结果和全局异常处理把接口做得更规范到时你会发现第 2 课的分层结构做了一次很好的铺垫。
延伸阅读

更多相关文章

2026/10/10 6:25:17

机器学习驱动的恶意加密流量监测:从特征到决策

简介:基于机器学习的恶意加密流量监测平台项目资料,面向信息安全、人工智能及相关专业的毕业设计、课程设计与初期课题立项,可用于恶意流量识别、加密流量分析、入侵检测等方向的完整方案复现。压缩包内共有68个文件,整体约1.1MB&…

2026/10/10 6:25:17

苹果CMS+原生JAVA影视APP:三端对接实战与避坑指南

简介:一份面向影视平台快速搭建的完整开发源码,基于原生 Java 打造 Android 影视 App,并完整对接苹果 CMS,同时支持 PC、WAP 与 APP 三端访问,帮助开发者、创业团队快速完成多端影视内容管理、发布与二次定制。压缩包共…

2026/10/10 6:25:17

基于WebSocket与Canvas的轻量级实时数据处理系统实战

1. 从“rea”这个标题说起:一个极简命名背后的完整项目思维第一次看到“rea”这个标题的时候,我脑子里蹦出来的第一反应是——这大概率又是一个被随手起的项目代号。做技术的人都有这个毛病,项目文件夹命名极其随意,什么“test”“…

2026/10/10 7:35:21

MCP Server 生产级实践:从跑通到敢上线的四个关键步骤

1. 从"能跑"到"敢上线":MCP Server 的鸿沟到底在哪很多人第一次写 MCP Server 的经历都差不多:照着官方 SDK 的示例,定义一个 tool,写个 handler,本地用客户端连上,看到工具被正确调用…

2026/10/10 7:35:21

Android Fragment重叠问题详解:成因、排查与解决方案

如果你写过一段时间的安卓应用,大概率遇到过这样一个诡异场景:某个页面上明明只该有一个弹窗或一个子页面,结果界面上出现了两份一模一样的 Fragment,点掉一层还有一层。我最早是在一个资讯类 App 的详情页踩到这个坑的——用户连续双击“展开更多”按钮,底部弹出的面板叠了两层…

2026/10/10 7:35:21

多智能体协作实战:从提示词堆砌到团队化分工调度

做AI应用这些年,我越来越觉得“单智能体包打天下”这个思路在真实业务约束下并不可靠。最近我搭了一套内部代号叫agency-agents的模拟项目,核心就是让多个智能体像一个小团队一样分工协作。它解决的场景很典型:一次任务里既要做资料搜集&…

2026/10/10 7:35:21

基于Python的Django+Flask畜牧站疾病防控与检测系统实战解析

做基层畜牧站的信息化项目,最头疼的不是算法,而是把一堆琐碎的防疫流程理顺。最近在开发一个基于Python的畜牧站疾病防控与检测系统,技术栈选了DjangoFlask这个组合。很多人第一反应是“一个项目为什么要混用两个框架”,其实真正落…

2026/10/10 7:30:21

Spring Boot昆虫标本管理系统实战:从数据库设计到毕业答辩全流程

简介:这是一份面向高校毕业设计场景的Spring Boot昆虫标本管理系统完整项目资料。系统围绕昆虫标本汇总、标本分类、论坛管理、留言咨询及图片识别等功能模块展开,采用Java语言与MySQL数据库,以B/S结构实现管理员与用户双端操作,可…

2026/10/10 7:31:36

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/9 20:15:56

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/8 6:05:44

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 0:04:53

从逻辑门到计算机:数字电路核心原理与全加器搭建实战

如果你拆过一台旧电脑的主板,盯着那些黑乎乎的小芯片看上一会儿,可能会冒出同一个疑问:这堆引脚密集的元件,到底是怎么“变”出那么复杂的应用的?答案并不在某个神秘的部件里,而是在所有芯片内部都在反复使…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑