尧图精选

Spring Boot用户增删查改实战:三层架构从入门到规范

🕒 发布时间:2026/10/1 22:26:38 📁 来源:尧图网络
刚学 Spring Boot 的时候我也是从“用户增删查改”起步的。这个功能看起来简单——无非就是对着数据库做四次操作——但把这四个操作写得规范、写得有章法却难倒了不少人。后台私信里问得最多的不是“Mapper 里 SQL 怎么写”而是“Controller 到底该写多少代码”“逻辑放 Service 还是放 Controller”“为什么我写的代码别人看了直摇头”。说白了问题就出在没搞懂三层设计模式。这篇博文就围绕“基于 Spring Boot 的用户增删查改”展开把三层架构一层层剥开讲清楚每一层的职责边界再把代码从零到一敲给你看。适合刚接触 Spring Boot 的初学者也适合写过 CRUD 但总觉得代码结构别扭的同学。看完你就能理解为什么说 Controller 要瘦、Service 要厚、Mapper 只管数据以及这一套规范能帮你避免哪些坑。1. 内容整体设计与思路拆解1.1 三层设计模式到底在解决什么问题很多新手写增删查改习惯直接把所有代码堆在一个类里方法里先写 JDBC 连接、再拼 SQL、再处理结果集、再把数据包装成 JSON 返回给前端。功能确实跑得通但问题也显而易见——这锅粥熬到后面你根本分不清哪段逻辑负责什么改一个需求要动一整锅代码。三层设计模式Controller-Service-Mapper的核心思路就是把“接收请求、处理业务、访问数据”这三件职责完全不同的事拆到三个独立层面里去。每一层只干自己那一摊活层与层之间通过接口或者方法调用衔接不越界。我习惯用一个生活化的类比来解释这件事。你把三层架构想象成一家餐厅Controller 是门口的服务员只负责接待客人、记录客人点什么菜、把菜端上桌Service 是后厨的厨师负责决定菜怎么做、放多少盐、什么时候出锅Mapper 是食材仓库管理员只负责按单子把食材取出来——是猪肉还是牛肉、是冷藏的还是冷冻的服务员和厨师都不关心他们只说“我要什么”仓库管理员负责把东西找出来。如果服务员直接冲进仓库拿食材短期看是省事了但一旦仓库要换供货渠道、或者统计口径要调整你发现所有服务员都得跟着改整个餐厅就乱了。代码同理如果把 SQL 和业务逻辑混在 Controller 里一旦数据库表结构变了或者业务规则改了你就要在每一个接口方法里找需要动的地方改完这一处忘了那一处线上事故就是这么来的。1.2 为什么 Spring Boot 特别适合做三层架构Spring Boot 之所以适合落地三层设计模式核心在于它对组件管理的高度自动化——Spring 容器帮你把各层的对象创建和依赖关系都管起来了你只需要用注解声明“这个类属于哪一层”框架就会自动完成装配。具体来说Spring Boot 提供了三个对应三层的组件注解Controller 层用RestController或ControllerService 层用ServiceMapper 层用Mapper或者Repository。这三个注解看起来不起眼但它们背后关联着 Spring 的组件扫描和依赖注入机制——你只需要在 Service 里写一个字段加上AutowiredSpring 容器就会把对应的 Mapper 实现自动塞进来不需要你手动new对象。这带来的直接好处是层与层之间彻底解耦了。比如你把 Mapper 的实现换成 MyBatis-Plus或者换了数据库厂商改用 JPAService 层的代码几乎不用动——因为 Service 依赖的是接口或者抽象约定而不是某个具体的实现类。这就是开闭原则在架构层面的体现对扩展开放对修改关闭。另外 Spring Boot 的自动配置还省了搭建环境的功夫。以前做三层架构光配置 Spring 容器、管理 Bean 生命周期、处理事务就够喝一壶的。现在引入spring-boot-starter-web、spring-boot-starter-jdbc或mybatis-spring-boot-starter再写一个application.yml把数据源填上三层骨架就自动立起来了。你专注的应该是业务逻辑怎么写而不是环境怎么搭。1.3 用户增删查改的场景选择与边界界定选定“用户管理”作为 CRUD 的落地场景主要是因为它足够简单、足够典型。用户实体通常就那几个字段ID、用户名、密码、邮箱、手机号、创建时间增删查改四个操作覆盖了 SQL 里最基本的 INSERT、DELETE、SELECT、UPDATE而且用户数据的校验逻辑比如用户名不能重复、邮箱格式要正确也能很好地演示“业务逻辑为什么必须放 Service 层”。这里顺便说一下设计中容易犯的一个错误把“增删查改”做成“删查改增”顺序乱来。正确顺序应该是先想清楚实体结构再确定数据访问接口再实现业务逻辑最后暴露 Web 接口。这种自下而上的顺序让你每一步都有依据而不是想到哪写到哪。再有就是边界界定。这里要提醒新手并不是所有方法都必须严格经过三层。比如用户列表和用户详情这两个查询操作很多团队会省略 Service 层让 Controller 直接调用 Mapper——因为查询逻辑确实简单到不需要额外处理。但我个人不建议初学者这么做。原因很简单你正在学的是分层思想连最简单的方法都跳过其中一层你就很难形成“所有业务都走三层”的肌肉记忆。等以后遇到真正复杂的业务逻辑你很容易又图省事直接绕过 Service架构就慢慢腐化了。2. 环境准备与项目骨架搭建2.1 开发环境与依赖选型既然要实操先把环境理清楚。我这里用的是 JDK 1.8 和 Spring Boot 2.7.x——这两个版本搭配是当前中小型项目里最常见也最稳妥的组合。如果你用 JDK 17 及以上Spring Boot 直接上 3.x 也没问题但依赖坐标会稍有不同下文代码以 Spring Boot 2.7.x 为准。pom.xml 里引入的依赖不多核心是四个spring-boot-starter-web提供 Web 容器内置 Tomcat和 Spring MVC 能力是 Controller 层的基础。mybatis-spring-boot-starter整合 MyBatis 框架让 Mapper 接口能自动代理生成实现。mysql-connector-java注意 Spring Boot 2.7.x 需要用mysql-connector-j新坐标MySQL 驱动负责底层数据库通信。lombok通过注解自动生成 getter/setter、构造器、日志对象等样板代码。有同学会问为什么用 MyBatis 而不是 Spring Data JPA这其实是个老话题了。MyBatis 的优势在于 SQL 完全可控写复杂查询时心里有底JPA 的优势在于开发效率高、单表 CRUD 几乎不用写 SQL。对于“用户增删查改”这种既想演示原生 SQL 又想体现分层思想的场景MyBatis 更合适因为你能清楚看到每个数据操作是怎么办的而不是被框架包装成一黑盒。2.2 项目结构包名与目录规划目录结构是三层设计模式的外在体现建议按照“技术分层 业务分包”的方式来组织。技术分层就是 controller、service、mapper 这些按技术角色分包业务分包就是 user、order、product 这些按业务模块分包。两者结合通常是这样的com.example.userdemo ├── controller │ └── UserController.java ├── service │ ├── UserService.java │ └── impl │ └── UserServiceImpl.java ├── mapper │ └── UserMapper.java ├── entity或 pojo/domain/model │ └── User.java ├── common或 config │ └── Result.java └── UserDemoApplication.java启动类这里有两个细节值得注意。第一Service 层为什么还要拆出接口和实现两个文件很多小项目里 Service 就一个实现类没必要套接口——我同意项目简单时直接用一个类就行。但为了让你在面试时不被问住也为了以后可能引入多实现比如一个实现走 MySQL、一个实现走缓存我这里保留了接口分离的写法这也是目前主流的规范。第二实体类放在 entity 包还是 pojo 包不同团队习惯不同。entity 强调与数据库表一一对应pojo 则更强调普通 Java 对象的含义。没有对错之分重要的是统一。我这里用 entity因为用户实体就是纯数据库映射没有任何多余逻辑。2.3 数据库设计与 application.yml 配置用户表的建表语句我习惯这样写CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码, email varchar(100) DEFAULT NULL COMMENT 邮箱, phone varchar(20) DEFAULT NULL COMMENT 手机号, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;注意几个设计上的考量用户名加了唯一索引这是业务规则“用户名不能重复”在数据库层的兜底create_time 和 update_time 用数据库默认值自动维护省去应用层手动赋值字符集用 utf8mb4避免 emoji 表情存储乱码。这些细节不是多余的它们是真实项目中常见的坑。application.yml 里最核心的数据源配置如下spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/user_demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.userdemo.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个配置强烈建议打开——它能把数据库里的create_time自动映射为 Java 里的createTime省掉一大波手动映射的代码。很多新手没开这个配置然后在 XML 里写一堆resultMap完全没有必要。3. 核心细节解析三层代码怎么写才规范3.1 实体层与返回结果封装先写实体类 User。用 Lombok 简化样板代码Data public class User { private Long id; private String username; private String password; private String email; private String phone; private LocalDateTime createTime; private LocalDateTime updateTime; }这里有个容易被忽视的点password字段的数据类型。很多初学者把它设成String就直接存明文了——这在真实项目里是重大的安全隐患但在这个入门教程里我们先聚焦 CRUD 和分层所以密码字段先保留不做加密处理。从第二行代码开始往数据访问层传输的应该是加密后的密文这个后面有机会单独写一篇讲。接着是统一返回结果封装类 Result。这个类的作用是让所有接口返回统一的 JSON 结构前端解析起来才省心Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }可能你会觉得这是多余的封装直接返回实体不就行了但设想一下如果前端要区分“操作成功”和“数据校验失败”没有统一的 code 字段前端就得靠 HTTP 状态码或者字符串去猜这是非常脆弱的约定。有了 Result 之后所有接口返回结构一致前端处理逻辑就非常简单了。3.2 Mapper 层数据访问与 SQL 映射Mapper 层是三层架构的底层只负责跟数据库打交道。接口定义如下Mapper public interface UserMapper { int insert(User user); int deleteById(Long id); int update(User user); User selectById(Long id); ListUser selectAll(); }注意这里的方法命名是有讲究的select 开头表示查询、insert 表示插入、update 表示修改、delete 表示删除一目了然团队协作时大家不看实现都能猜个大概。SQL 写在 resources/mapper/UserMapper.xml 里?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.userdemo.mapper.UserMapper insert idinsert parameterTypeUser useGeneratedKeystrue keyPropertyid INSERT INTO user (username, password, email, phone) VALUES (#{username}, #{password}, #{email}, #{phone}) /insert delete iddeleteById parameterTypeLong DELETE FROM user WHERE id #{id} /delete update idupdate parameterTypeUser UPDATE user SET username #{username}, password #{password}, email #{email}, phone #{phone} WHERE id #{id} /update select idselectById parameterTypeLong resultTypeUser SELECT id, username, password, email, phone, create_time, update_time FROM user WHERE id #{id} /select select idselectAll resultTypeUser SELECT id, username, password, email, phone, create_time, update_time FROM user /select /mapper想聊一个很多人会踩的坑update 语句里的 SET 子句。上面这段代码是全量更新——所有字段都必须有值否则对应的列会被更新成 NULL。但真实业务中很多场景是“只更新某一个字段”比如只改手机号全量更新会把其他字段也覆盖掉。如果你想做部分更新更合理的策略是动态 SQL用if标签判断字段是否为 null只更新非空字段。update idupdate parameterTypeUser UPDATE user set if testusername ! nullusername #{username},/if if testpassword ! nullpassword #{password},/if if testemail ! nullemail #{email},/if if testphone ! nullphone #{phone},/if /set WHERE id #{id} /update这就是 MyBatis 动态 SQL 的典型用法也是面试中经常被追问的点。它解决了“更新时到底哪些字段需要更新”的灵活性需求但要小心set标签的末尾逗号问题——MyBatis 的set会自动去掉最后多余的逗号所以不用自己处理。3.3 Service 层业务逻辑与事务控制Service 层是整个架构里最“厚”的一层所有业务规则、流程控制、数据校验都在这里完成。接口定义public interface UserService { void addUser(User user); void deleteUser(Long id); void updateUser(User user); User getUserById(Long id); ListUser getAllUsers(); }实现类Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Override public void addUser(User user) { // 业务规则用户名不能为空 if (user.getUsername() null || user.getUsername().trim().isEmpty()) { throw new IllegalArgumentException(用户名不能为空); } // 业务规则用户名不能重复 User existing userMapper.selectByUsername(user.getUsername()); if (existing ! null) { throw new IllegalArgumentException(用户名已存在); } // 业务规则邮箱格式校验 if (user.getEmail() ! null !user.getEmail().contains()) { throw new IllegalArgumentException(邮箱格式不正确); } // 密码加密与数据落库 userMapper.insert(user); } // 其他方法省略 }在这个例子中你能清楚看到“业务逻辑放在 Service 层”的含义——这些校验规则如果用 Controller 去写那么每增加一个入口比如后台管理端、移动端、定时任务都要重复写一遍而放在 Service 层所有入口共用同一个校验逻辑改了这里全局生效。Service 层还有一个重要职责是事务控制。一个业务操作中往往涉及多次数据库操作比如“创建用户后发送欢迎通知”这种场景如果通知发送失败你希望用户数据也能回滚——那这个多步操作就应该在同一个事务里。MyBatis-spring-boot-starter 默认已经开启了事务管理你只需要在方法上加上Transactional注解Transactional(rollbackFor Exception.class) public void addUser(User user) { userMapper.insert(user); // 模拟其他业务步骤如果这里抛异常上面的 insert 也会回滚 userMapper.insertRole(user.getId(), 2L); }rollbackFor Exception.class很关键。Spring 默认只在遇到 RuntimeException 时回滚遇到受检异常不会回滚。如果你不加这个参数业务代码里抛出受检异常时事务不会回滚数据就脏了——这是很多隐藏 Bug 的来源。3.4 Controller 层参数接收与结果包装Controller 层是全链路最“薄”的一层它的职责只有两个把前端传来的参数接收下来转交给 Service把 Service 返回的结果包装成统一结构还给前端。RestController RequestMapping(/api/users) public class UserController { Autowired private UserService userService; GetMapping public ResultListUser list() { return Result.success(userService.getAllUsers()); } GetMapping(/{id}) public ResultUser detail(PathVariable Long id) { return Result.success(userService.getUserById(id)); } PostMapping public ResultVoid add(RequestBody User user) { userService.addUser(user); return Result.success(null); } PutMapping(/{id}) public ResultVoid update(PathVariable Long id, RequestBody User user) { user.setId(id); userService.updateUser(user); return Result.success(null); } DeleteMapping(/{id}) public ResultVoid delete(PathVariable Long id) { userService.deleteUser(id); return Result.success(null); } }这段代码有几个细节值得琢磨。一个是 RESTful 风格的接口设计。列表是 GET /api/users详情是 GET /api/users/{id}新增是 POST修改是 PUT删除是 DELETE——这符合资源导向的设计思想前端对接起来非常直观。很多老项目还在用 GET /queryUserList?actionadd 这种“动词式”接口虽然也能跑但现在的新项目已经很少这么设计了。另一个是参数接收方式。RequestBody接收 JSON 体PathVariable接收路径参数。初学者容易把这两个搞混记住一个口诀“JSON 用 RequestBodyURL 用 PathVariable查询参数用 RequestParam”。再有一个是 Controller 里没有任何业务逻辑。注意我哪怕是校验用户名是否为空也放进 Service 里了。Controller 的长度应该控制在“一眼能看完所有方法”的程度超过 100 行就要反思是不是业务逻辑写多了。4. 实操过程与核心环节实现4.1 完整操作流程从创建项目到启动验证实操这一节我带你把整个流程完整走一遍确保照着敲能跑起来。第一步创建项目。有几种方式用 IDEA 的 Spring Initializr 直接生成、从 Spring 官网 start.spring.io 下载、或者 Maven 手动构建。最推荐的还是 IDEA 里直接选 Spring Initializr勾上 Web、MySQL Driver、MyBatis Framework 三个依赖项目骨架就出来了。第二步准备好数据库。先建库CREATE DATABASE user_demo DEFAULT CHARSET utf8mb4;再建表。表结构见 2.3 节不重复写。第三步按 2.2 节规划好的包结构依次创建 entity、mapper、service、controller 四个包和对应的类。这里有个 IDE 使用小技巧创建包时一定要写完整路径比如com.example.userdemo.entityIDEA 会自动生成多级目录不要手动一层层建那样容易拼错包名。第四步编写配置文件。application.yml 里的数据源和 MyBatis 配置见 2.3 节。注意 MySQL 8.x 驱动类的写法Spring Boot 2.7.x 用com.mysql.cj.jdbc.Driver老项目里的com.mysql.jdbc.Driver已经过时了别复制旧代码。第五步逐层编写代码。顺序建议是 entity → mapper → service → controller因为上一层依赖下一层。写 service 接口时可以按住 CtrlMac 是 Command点击userMapper跳转到 Mapper 接口确认方法签名一致避免编译报错。第六步启动应用。找到UserDemoApplication.java直接 Run main 方法。看到控制台输出Tomcat started on port(s): 8080就说明启动成功了。然后用浏览器访问http://localhost:8080/api/users如果返回{code:200,data:[],message:操作成功}这种 JSON整条链路就通了。第七步用接口测试工具验证 CRUD。我这边用的是 Apifox功能类似 Postman。先 POST 新增一个用户再 GET 查列表确认数据进去了再 PUT 修改数据最后 DELETE 删除一环扣一环检查。4.2 参数设计与前后端交互约定前后端交互的约定往往比代码本身更重要。前端最怕的不是接口太少而是接口规则不统一。这里基于上面的代码说一下与前端对接时应该明确的规则一是数据格式。所有请求和响应默认都是 JSON。要求前端请求头带Content-Type: application/json后端统一用RequestBody接收。如果前端传的是表单格式application/x-www-form-urlencoded那后端就要改成RequestParam——两边的格式不匹配是联调时最常见的低级错误。二是响应结构。所有接口返回 Result 包装后的结构即{ code, message, data }。前端拿到后先判断 code 是否为 200是则取 data不是则弹出 message。这种约定一旦定下来前端封装的 axios 拦截器就能统一处理错误提示非常方便。三是分页参数。虽然本教程没做分页但真实项目里列表接口肯定要支持分页。常见约定是前端传pageNum页码从 1 开始和pageSize每页条数后端返回{ total, list }。这里用的 MyBatis 可以通过 PageHelper 插件简化分页以后可以专门讲。四是错误处理。上面的 Result.error 手动抛出异常的方式显得粗暴。更优雅的做法是用全局异常处理器RestControllerAdvice统一捕获异常把异常信息转成 Result 结构返回。这样可以避免 Controller/Service 里塞满 try-catch。4.3 代码走查一条请求完整贯穿三层为了帮你把三层架构串成一条线我拿“新增用户”举例把一次请求从发出到落库的全过程走一遍。前端点击“保存”按钮发出 HTTP POST 请求POST /api/users { username: zhangsan, password: 123456, email: zhangsanexample.com, phone: 13800138000 }第一步Tomcat 收到请求后Spring MVC 的 DispatcherServlet 根据 URL 映射找到UserController.add()方法。Spring 用 Jackson 把请求体 JSON 反序列化成User对象传入方法。第二步Controller 里只有一行代码userService.addUser(user)。它把数据转交给 Service 层自己不做任何处理。第三步进入UserServiceImpl.addUser()先执行三个校验逻辑用户名非空、用户名唯一、邮箱格式。校验通过后调用userMapper.insert(user)。第四步MyBatis 根据UserMapper接口的insert方法绑定 XML 里的 SQL把 User 对象的属性填充到#{username}等占位符里通过 JDBC 执行 INSERT 语句。第五步数据库插入成功返回自增主键。MyBatis 会把主键回填到 User 对象的 id 属性上这得益于useGeneratedKeystrue。第六步一层层返回。UserServiceImpl 返回 voidController 把Result.success(null)返回给前端。前端拿到{code:200,message:操作成功}提示用户“新增成功”。这条链路虽然只有六步但每一步各司其职任何一步出问题都能快速定位到具体层级——这是没有分层的代码永远做不到的。5. 常见问题与排查技巧实录5.1 新手最容易踩的 7 个坑这个部分是想认真给你避坑都是我实际带新人时见过的高频问题。第一个坑拉了依赖但没加Mapper注解。MyBatis 扫描不到 Mapper 接口启动报错“Invalid bound statement (not found)”。解决方式是在启动类上加MapperScan(com.example.userdemo.mapper)或者在每个 Mapper 接口上单独加Mapper二选一即可。第二个坑selectAll查出来是 null。大概率是 MyBatis 实体类属性和数据库列名对不上。比如数据库列create_timeJava 属性createTime如果没有开启map-underscore-to-camel-case查询结果就是 null。看到 null 别慌先看配置。第三个坑POST 请求报 415 Unsupported Media Type。原因几乎都是前端请求头里没带Content-Type: application/json。后端RequestBody要求请求体是 JSON前端没按要求传自然报错。第四个坑id 回显问题。新增用户后前端想立即使用返回的用户 ID但查不到。原因是没有配置useGeneratedKeystrue keyPropertyid。有这两个属性后MyBatis 才会把自增主键回填到实体对象里。第五个坑update 时把不想修改的字段更新成了 null。原因就是用全量 UPDATE 语句没做if动态判断。要修复就把 update 改成动态 SQL加上set和if。第六个坑LocalDateTime 字段返回 JSON 时格式怪异。如果不出意外你会看到类似createTime:2024-01-01T12:00:00的字符串不是前端想要的2024-01-01 12:00:00。解决办法是加配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第七个坑循环依赖报错。Service 里注入了另一个 Service另一个 Service 又注入回来启动时 Spring 直接抛BeanCurrentlyInCreationException。解决方案不是加Lazy糊弄过去而是重构代码把相互依赖的逻辑抽出来放到第三个组件里。5.2 排查思路接口报错了怎么办新手遇到接口报错第一反应通常是看前端控制台或者浏览器里的错误信息——这没错但往往信息不完整。我更推荐先看后端控制台日志因为异常堆栈才是问题根源。排查逻辑我总结成“三段法”。第一段看请求是否到达了后端。如果后端日志里连Mapping [POST] /api/users都没打印说明请求压根没被框架处理——先查路由、端口、网络别浪费时间去读代码。第二段看 Controller 是否执行成功。在后端日志里加一行log.info(进入 addUser, 参数: {}, user)如果日志打了但业务还是失败问题在 Service 层。第三段看 SQL 是否执行成功。如果异常堆栈里有BadSqlGrammarException或SQLSyntaxErrorException几乎可以直接定位到 XML 里的 SQL 语句——把 SQL 复制到 Navicat 里手动执行一遍看是不是 SQL 本身的问题。还有一个非常实用的技巧给 Mapper 接口的方法加上日志输出。在 application.yml 里配置logging: level: com.example.userdemo.mapper: debug这样 MyBatis 会把每条 SQL 和参数打到日志里你就能看到实际执行的 SQL 是什么、参数值是什么很多问题一眼就破了。5.3 性能优化与后续扩展方向写完一个能跑的 CRUD 只是开始还有很多可以优化和扩展的方向这里挑几个跟三层架构关系较大的来说。用户列表接口如果数据量上来了第一个瓶颈往往是全表扫描。没有条件的SELECT * FROM user在数据量到十万级时就会明显变慢。优化方向是分页查询加上索引。分页查询建议用 MyBatis 的 PageHelperPageHelper.startPage(pageNum, pageSize); ListUser users userMapper.selectPage(); PageInfoUser pageInfo new PageInfo(users);查询条件方面username字段已经有唯一索引按用户名查询没问题。如果经常按 email 查询可以给 email 加普通索引。索引不是越多越好插入、更新时要维护索引所以要结合真实查询场景来定。如果用户量继续增长单库的写入成为瓶颈这时就要考虑分库分表或者引入缓存。缓存这一块最常见的做法是用 Redis 缓存热点用户信息查询先走 Redis没命中再走 MySQL并把结果回填到 Redis。注意缓存与数据库的一致性——修改用户信息后要把 Redis 里对应的 key 删掉否则读到旧数据。这些方向不是靠一个教程就能讲完的但理解了三层架构之后你会发现所有这些扩展都是在你熟悉的 Controller-Service-Mapper 骨架上加东西——这就是分层设计最大的红利架构骨架稳定业务扩展永远是在骨架上添加新模块而不是推翻重来。6. 尾声一点实际操作之外的体会写到这里想分享一点题外话。用户增删查改几乎每个人都会写但把它写出质量、写出规范靠的不是敲代码的手速而是对架构思想的理解深度。我见过不少工作两三年的开发CRUD 写得很溜但一遇到跨表操作、缓存一致、事务回滚这些复杂场景就抓瞎——核心原因就是基础没打牢没有真正理解每一层的边界在哪、职责是什么。如果你现在刚学完这个教程建议做两件事第一把原生的 MyBatis 写法全部摸熟之后再去学 MyBatis-Plus——很多人直接跳过了原生 SQL导致 SQL 能力很虚排查问题非常吃力第二把这段代码自己敲三遍第一遍照抄第二遍闭卷第三遍加上异常处理和全局异常捕获器能独立完成第三遍这一关才算真正过了。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →