☰
服务端增删改查实战:从建表到接口测试的完整指南
2026/10/1 21:35:56 网站建设 项目流程

很多刚接触后端开发的读者,都遇到过同一个困惑:看了不少框架教程,也能看懂代码,但真要自己动手做一个“用户管理”“订单管理”这类功能时,却不知道从哪里下手。尤其是现在AI编程工具越来越普及,让AI写一段增删改查代码很容易,可写完以后连接口地址是什么、参数怎么传、数据库表怎么建都说不清楚。

这篇文章想解决的,正是这个问题。我们不讲高大上的分布式架构,也不聊复杂的中间件,而是聚焦服务端开发最基础、最核心的一组操作:增删改查(CRUD)。你会理解这四个操作在服务端意味着什么,会用一个真实可运行的项目把它们串联起来,还会掌握“零基础状态下如何用AI工具辅助完成这类开发”的正确姿势。

先给一个明确判断:AI编程降低了“写代码”的门槛,但没有降低“想清楚需求”的门槛。对服务端开发来说,增删改查只是最后落地的那一层,真正决定项目能不能跑通的,是你对数据结构、请求流程和调试方法的理解。这篇文章就按这条线索展开:先讲清楚概念,再搭环境,再写代码,最后验证和排错。读完之后,你不仅能自己写出一个完整的增删改查接口,还能在别人用AI生成的代码出问题时,知道该从哪个环节开始排查。

1. 这篇文章真正要解决的问题

很多零基础读者对“服务端增删改查”有误解,觉得它就是几个 SQL 语句的问题:INSERT、SELECT、UPDATE、DELETE。这个理解方向没错,但离实际开发还差得很远。

真实项目里的增删改查,包含至少三层内容:

  • 数据库层:表结构怎么设计,字段类型怎么定,主键是自增还是分布式 ID。
  • 服务端接口层:客户端通过 HTTP 请求调用接口,参数怎么校验,返回结果怎么统一,异常怎么处理。
  • 工程流程层:项目怎么启动,接口怎么测试,日志怎么打,代码怎么部署。

零基础读者最难的地方,不是记不住 SQL 语法,而是不知道这些层面之间的关系。客户端发来的请求到了服务端以后,服务端怎么取出数据?怎么告诉数据库要执行什么操作?操作结果怎么变成接口返回值?每一环都有约定,每一环都有坑。

这篇文章会以“用户信息管理”这个经典业务为例,完整走一遍从建表到写接口再到测试的过程。同时,我们在每个环节都会穿插说明:如果这段工作让 AI 来做,哪些可以放心交给它,哪些必须自己把关。这是零基础时代的新课题,也是本篇文章与其他 CRUD 教程最大的不同。

2. 服务端、数据库与增删改查:先建立整体认知

在动手写代码之前,先用一个生活化的类比把概念讲清楚。

想象一家餐厅:

  • 客户端是顾客,负责提需求、看结果。
  • 服务端是服务员,负责接收需求、传达指令、反馈结果。
  • 数据库是后厨的食材仓库,存放着所有原材料和成品。

顾客不会直接走进仓库搬东西,他们只能跟服务员说“我要一份红烧肉”。服务员也不会自己做菜,他负责把需求写成一张单子递给后厨,后厨按单子取食材、烹饪,再把菜端出来。整个过程里,顾客和仓库之间永远隔着一层“服务端”。

对应到技术世界,就是这样的关系:

  • 客户端(浏览器、手机 App、小程序、其他服务)发起 HTTP 请求。
  • 服务端接收请求,解析参数,调用业务逻辑。
  • 业务逻辑通过数据访问层操作数据库。
  • 数据库执行 SQL,把结果返回给服务端。
  • 服务端把结果加工成固定格式,响应给客户端。

在这个链路里,增删改查就是四种最基本的“指令类型”:

操作数据库动作HTTP 方法业务场景
增INSERTPOST创建一个新用户
删DELETEDELETE删除一个用户
改UPDATEPUT / PATCH修改用户资料
查SELECTGET获取用户列表或详情

注意这里有一个常见误区:HTTP 方法和数据库 SQL 不是一一对应的。比如你想“查询一个用户”,用GET /api/user/1;你“删除一个用户”,用DELETE /api/user/1。这两个接口在 URL 上看起来很像,但语义完全不同。很多零基础读者第一次看接口文档会糊涂,就是因为没把这个映射关系理清楚。

另外还要解释一个概念:RESTful 风格。它是一套约定,主张用 HTTP 方法来表达操作意图,用 URL 来定位资源。遵循这套约定,别人看你的接口就能秒懂:GET /api/user是查用户列表,POST /api/user是新增用户,PUT /api/user/{id}是修改指定用户,DELETE /api/user/{id}是删除指定用户。虽然不是所有项目都严格遵循 RESTful,但它是目前最常见的接口设计规范,零基础学习时按这个思路起步最稳妥。

3. 零基础如何正确使用 AI 编程工具写 CRUD

在进入代码之前,必须用一整节把 AI 编程这件事说透。因为零基础读者最容易犯的错误,就是让 AI “直接生成整个项目”,然后对着一堆看不懂的文件发呆。

3.1 先拆解需求,再让 AI 写代码

正确的顺序是:你先有需求,再有设计,最后才是代码。

比如“做一个用户管理模块”,这句话在 AI 眼里太模糊了。你需要进一步问自己:

  • 用户有哪些字段?用户名、手机号、邮箱、状态?
  • 新增用户时哪些字段必填?
  • 用户可以改哪些信息?
  • 删除用户是物理删除还是逻辑删除?
  • 查询列表需要分页吗?按什么字段排序?
  • 接口返回格式是什么?成功和失败怎么区分?

这些问题自己没想清楚,AI 给出来的代码大概率只是一份“看起来完整、实际上未必符合需求”的半成品。这不能怪 AI,因为模糊的问题必然带来模糊的答案。

3.2 一个可复用的 AI 提示词模板

等你想清楚了需求,可以这样向 AI 提问:

你是一名 Java 后端工程师。我要做一个用户管理模块,技术栈是 Spring Boot 3.x + MyBatis-Plus + MySQL。 业务需求: 1. 新增加用户,字段为 username、email、phone、status,其中 username 和 email 必填,phone 选填,status 默认 1(启用)。 2. 修改用户信息,只能修改 email、phone、status,不允许修改 username。 3. 删除用户,使用逻辑删除,字段 deleted 默认 0。 4. 分页查询用户列表,支持按 username 模糊搜索,按 create_time 倒序。 数据库表结构如下(MySQL 方言): (在这里贴上你的建表 SQL) 要求: - 遵守 RESTful 风格,统一返回 Result<T> 结构。 - 使用 MyBatis-Plus 的 BaseMapper 和 IService。 - 先给出你的接口设计思路,再给出完整代码。

这个提示词有三个关键点:说明技术栈、给出表结构、提出具体约束。尤其是“给出表结构”这一点,很多初学者会忽略,结果 AI 自己凭空设计一张表,跟你期望的字段对不上。一定要记住:谁提供信息,谁就掌握主动权。

3.3 AI 生成代码后必须做的三件事

AI 给出代码不等于工作结束,你至少要检查三点:

第一,依赖是否完整。AI 生成的代码往往默认你已经有了完整的工程依赖,但实际项目中可能根本没引入 MyBatis-Plus、没配置 MySQL 驱动。所以生成代码后,第一件事是核对 pom.xml 或 build.gradle。

第二,注解和配置是否正确。Spring Boot 版本不同,注解的包名可能不同。最典型的是javax.*和jakarta.*的区别,如果你的 Spring Boot 是 3.x,还用javax.annotation.*,运行时会直接报类找不到。

第三,代码逻辑是否符合你的业务约束。比如提示词里说了“不允许修改 username”,AI 生成的代码可能照样把 username 放进更新字段里。这种问题很隐蔽,不逐行看根本发现不了。所以零基础读者用 AI,不是把任务交给 AI 就完事了,而是要把它当成一个“写代码很快、但需要你认真验收”的队友。

4. 环境准备与前置条件

下面进入实操。我们要实现一个完整的用户管理接口,涉及的环境有这些:

组件用途说明
JDK 17+Java 运行环境不同版本以实际环境为准,Spring Boot 3.x 要求 JDK 17 以上
Maven 3.6+依赖管理也可以用 IDEA 内置的 Maven
MySQL 8.x关系型数据库版本以实际环境为准,本文的 SQL 在 MySQL 8.x 下测试通过
IDEA 或 VS Code开发工具推荐使用支持 Spring 插件的 IDE
Postman / Apifox / curl接口测试工具三者任选其一

如果你之前没有安装 Java 和 Maven,可以在命令行里先验证一下:

java -version mvn -version

如果提示“找不到命令”,说明环境变量还没配好。这一步是很多零基础读者的第一道坎,但也是最容易查资料解决的一步。安装完成的标准是:命令行里能正常输出版本号。

MySQL 准备一句提示:请确认本地 MySQL 服务已经启动。Windows 用户可以在“服务”里查看 MySQL 状态;macOS 和 Linux 用户可以用:

sudo systemctl status mysql

5. 数据库设计与会话建表

我们设计的业务场景是“用户信息管理”。先定义核心字段:

  • id:主键,自增。
  • username:用户名,唯一,不能为空。
  • email:邮箱,唯一。
  • phone:手机号,选填。
  • status:状态,1 启用,0 禁用。
  • deleted:逻辑删除标记,0 未删除,1 已删除。
  • create_time:创建时间,自动填充。
  • update_time:更新时间,自动填充。

建表 SQL 如下:

CREATE DATABASE IF NOT EXISTS demo_db DEFAULT CHARACTER SET utf8mb4; USE demo_db; CREATE TABLE IF NOT EXISTS `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `username` VARCHAR(64) NOT NULL COMMENT '用户名', `email` VARCHAR(128) NOT NULL COMMENT '邮箱', `phone` VARCHAR(32) DEFAULT NULL COMMENT '手机号', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1启用 0禁用', `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除:0未删除 1已删除', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`), UNIQUE KEY `uk_email` (`email`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '用户表';

几点说明:

第一,表名使用了反引号包裹,因为user在 MySQL 里虽然不是严格意义上的保留字,但为了避免歧义,写成`user`更安全。

第二,utf8mb4是必须的。MySQL 的utf8不是真正的完整 UTF-8,utf8mb4才能完整支持中文和 emoji 字符。

第三,逻辑删除字段deleted很关键。生产环境中,直接执行DELETE FROM会把数据彻底抹掉,一旦删错,恢复成本极高。用deleted字段做标记,查询时自动过滤“已删除”数据,是 CRUD 开发里的常见工程实践。

6. 项目初始化与核心代码实现

6.1 创建 Spring Boot 项目并添加依赖

你可以使用 Spring Initializr 生成项目,也可以直接在 IDEA 里创建。需要添加的依赖包括:Spring Web、MyBatis-Plus、MySQL 驱动、Lombok。

以 Maven 项目为例,核心依赖如下:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>

这里特别提醒:版本号请以 Maven 仓库实际可用版本为准。如果你用的是 Spring Boot 2.x,MyBatis-Plus 的 starter 坐标是mybatis-plus-boot-starter,不要和 Spring Boot 3.x 的坐标混用。这是 AI 生成代码时最容易埋坑的地方——它可能给你一个在旧项目里能跑、但新项目里根本不存在的依赖坐标。

6.2 配置文件

在src/main/resources/application.yml中配置数据源:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

说明三件事:

一是map-underscore-to-camel-case: true。数据库字段是create_time,Java 实体类是createTime,这个配置开启后,MyBatis-Plus 能自动完成下划线到驼峰命名的映射。

二是logic-delete-field: deleted。这就是逻辑删除的全局配置。配置之后,你调用deleteById时,MyBatis-Plus 不会执行真正的DELETE,而是自动执行UPDATE user SET deleted = 1 WHERE id = ? AND deleted = 0。这对业务数据是很重要的一层保护。

三是log-impl: StdOutImpl。启动后会打印 SQL 日志,零基础阶段建议打开,方便你观察 MyBatis-Plus 到底帮你执行了什么语句。

6.3 启动类与实体类

启动类加@MapperScan,告诉 Spring 到哪里找 Mapper 接口:

package com.example.demo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.example.demo.mapper") public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }

实体类:

package com.example.demo.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("`user`") public class User { @TableId(type = IdType.AUTO) private Long id; private String username; private String email; private String phone; private Integer status; @TableLogic private Integer deleted; private LocalDateTime createTime; private LocalDateTime updateTime; }

注意几个细节:

  • @TableName("user")对应数据库表名。因为表名是user,加上反引号避免与关键字冲突。
  • @TableId(type = IdType.AUTO)表示主键自增。
  • @TableLogic标记逻辑删除字段,注意这个注解加在“逻辑删除标记字段”上,而不是加在业务字段上。新手很容易把@TableLogic和@TableField搞混。

6.4 Mapper 与 Service

Mapper 接口:

package com.example.demo.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demo.entity.User; public interface UserMapper extends BaseMapper<User> { }

Service 接口:

package com.example.demo.service; import com.baomidou.mybatisplus.extension.service.IService; import com.example.demo.entity.User; public interface UserService extends IService<User> { }

Service 实现类:

package com.example.demo.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.demo.entity.User; import com.example.demo.mapper.UserMapper; import com.example.demo.service.UserService; import org.springframework.stereotype.Service; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }

看到这里,你可能觉得代码“太少了”。这是对的,因为 MyBatis-Plus 把大量通用 SQL 封装掉了。只要你继承BaseMapper,就有了insert、deleteById、updateById、selectById、selectPage这些基础方法。这就是“通用 CRUD 服务”的思路——不需要手写 SQL,也能完成大部分单表操作。

6.5 统一返回结构

实际项目中,接口返回值一般要统一格式。简单定义一个Result<T>:

package com.example.demo.common; import lombok.Data; @Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }

这个类的作用是约定一种“沟通语言”。客户端不管调用哪个接口,拿到的结构都是一样的:状态码code、提示信息message、业务数据data。如果每个接口返回格式都不一样,前端工程师对接起来会非常痛苦。

6.6 Controller 增删改查接口

这是整篇文章的核心代码区。我们按 RESTful 风格实现五个接口:

package com.example.demo.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.demo.common.Result; import com.example.demo.entity.User; import com.example.demo.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; // 新增用户 @PostMapping public Result<User> createUser(@RequestBody User user) { if (user.getUsername() == null || user.getUsername().trim().isEmpty()) { return Result.error(400, "username 不能为空"); } if (user.getEmail() == null || user.getEmail().trim().isEmpty()) { return Result.error(400, "email 不能为空"); } user.setId(null); userService.save(user); return Result.success(user); } // 查询用户列表(分页 + 模糊搜索) @GetMapping("/page") public Result<Page<User>> pageUser( @RequestParam(defaultValue = "1") Integer current, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String username) { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); if (username != null && !username.trim().isEmpty()) { wrapper.like(User::getUsername, username); } wrapper.orderByDesc(User::getCreateTime); Page<User> page = userService.page(new Page<>(current, size), wrapper); return Result.success(page); } // 查询用户详情 @GetMapping("/{id}") public Result<User> getUserById(@PathVariable Long id) { User user = userService.getById(id); if (user == null) { return Result.error(404, "用户不存在"); } return Result.success(user); } // 修改用户 @PutMapping("/{id}") public Result<User> updateUser(@PathVariable Long id, @RequestBody User user) { User exist = userService.getById(id); if (exist == null) { return Result.error(404, "用户不存在"); } user.setId(id); user.setUsername(null); user.setCreateTime(null); user.setUpdateTime(null); userService.updateById(user); return Result.success(userService.getById(id)); } // 删除用户(逻辑删除) @DeleteMapping("/{id}") public Result<Void> deleteUser(@PathVariable Long id) { User exist = userService.getById(id); if (exist == null) { return Result.error(404, "用户不存在"); } boolean removed = userService.removeById(id); if (!removed) { return Result.error(500, "删除失败"); } return Result.success(null); } }

逐个解释核心逻辑:

新增接口:用@RequestBody接收 JSON。为什么用 POST 而不是 GET?因为 POST 表示“向服务器提交资源”,也适合传递复杂数据。新增之前先做基本校验:用户名和邮箱不能为空。这属于“入参校验”,虽然是简单校验,但必须由服务端来做,不能只依赖前端。

分页查询:用LambdaQueryWrapper构建查询条件。like方法生成LIKE '%xxx%'模糊查询,orderByDesc按创建时间倒序。Page对象是 MyBatis-Plus 的分页模型,注意这里使用的是selectPage/page方法,MyBatis-Plus 3.x 默认内置分页插件,不需要额外配置拦截器。不过如果遇到分页失效问题,可以检查是否缺少PaginationInnerInterceptor配置,这个问题会在下一节排查清单里细说。

查询详情:@PathVariable表示从 URL 路径中取值。GET /api/user/1时,id=1会被绑定到方法参数上。这是 RESTful 风格的核心用法,和@RequestParam的GET /api/user?id=1是两种不同的传参方式,新手要重点区分。

修改接口:先用getById确认用户存在,再updateById。注意我们在传入对象上把username和createTime置为 null,原因是:MyBatis-Plus 的updateById默认只更新非空字段。如果前端传来的user对象里没有这两个字段,它们本来就是 null,不更新;但如果前端传了username,而我们的业务规则又不允许修改用户名,那就要主动把它置空,防止被误更新。这种“防止不该被更新的字段被更新”的写法,在实际项目里非常重要。

删除接口:removeById因为全局配置了逻辑删除,所以底层执行的是UPDATE而不是DELETE。这一点可以打开 SQL 日志验证,也能给你上一道保险。

7. 运行验证:用 curl 和 Postman 测试接口

7.1 启动项目

在项目根目录执行:

mvn spring-boot:run

看到类似下面的日志,说明启动成功:

Tomcat started on port 8080 (http) Started DemoApplication in 3.2 seconds

启动失败的情况后面统一排查。现在假设一切正常,我们开始验证。

7.2 测试新增用户

curl -X POST "http://localhost:8080/api/user" \ -H "Content-Type: application/json" \ -d '{"username": "zhangsan", "email": "zhangsan@example.com", "phone": "13800000000", "status": 1}'

预期返回:

{ "code": 200, "message": "success", "data": { "id": 1, "username": "zhangsan", "email": "zhangsan@example.com", "phone": "13800000000", "status": 1, "deleted": 0, "createTime": "2025-01-01T10:00:00", "updateTime": "2025-01-01T10:00:00" } }

如果没有返回id或返回500,优先去控制台看 SQL 日志。

7.3 测试分页查询

curl "http://localhost:8080/api/user/page?current=1&size=10&username=zhang"

预期返回一个分页结构,里面包含records数组、total总数、current当前页等信息。

7.4 测试修改用户

curl -X PUT "http://localhost:8080/api/user/1" \ -H "Content-Type: application/json" \ -d '{"email": "newemail@example.com", "phone": "13900000000", "status": 0}'

修改之后再用查询详情接口确认:

curl "http://localhost:8080/api/user/1"

如果username没有被改动,说明我们前面那个“主动置空”的保护逻辑生效了。

7.5 测试删除用户

curl -X DELETE "http://localhost:8080/api/user/1"

再次查询这个用户,正确的预期是返回“用户不存在”。注意这里并不是数据被删没了,而是逻辑删除后,默认查询条件自动加上了deleted = 0,所以正常接口查不到它。

如果你想验证数据还在,可以直接在 MySQL 里执行:

SELECT * FROM `user` WHERE id = 1;

你会看到deleted字段变成了1。这就是逻辑删除和物理删除的直观区别。

8. 常见问题与排查思路

零基础读者最需要的就是一张“出了错先往哪里看”的对照表。下面整理 CRUD 开发中最常见的几类问题:

问题现象可能原因排查方式解决方案
项目启动失败,报数据库连接错误MySQL 未启动、用户名密码错误、数据库不存在检查 MySQL 服务状态,检查 application.yml 中的 url/username/password启动 MySQL,创建 demo_db 数据库,修正配置
启动报端口被占用8080 端口已被其他进程占用`netstat -anofindstr 8080(Windows)或lsof -i:8080`(Linux/macOS)
新增用户时中文乱码数据库连接 URL 缺少编码参数查看数据库表字符集,查看请求编码URL 增加characterEncoding=utf8,表使用utf8mb4
调用 Mapper 方法报找不到 Bean启动类缺少@MapperScan检查启动类注解和 mapper 包路径加@MapperScan("com.example.demo.mapper")
username和createTime字段映射不上数据库下划线和实体类驼峰映射未开启开启日志观察 SQL 查询列名配置map-underscore-to-camel-case: true
分页查询结果一直是全量数据缺少分页插件拦截器检查 MyBatis-Plus 版本是否符合 3.x 默认行为;旧版本需要额外加MybatisPlusInterceptor新版可先确认代码版本;旧版参考官方文档补齐配置
Spring Boot 3.x 启动报javax包不存在依赖导入的是旧版注解,与 Jakarta API 冲突查看编译错误和依赖树使用 Spring Boot 3.x 配套的jakarta.*依赖坐标
AI 生成代码与本地 Maven 依赖不一致AI 默认的版本号与本地仓库不匹配查看mvn dependency:tree统一版本号,以 Maven 仓库已有版本为准

8.1 一个最隐蔽但最要命的坑:依赖坐标混用

这里单独提一下 Spring Boot 2.x 和 3.x 的区别。

Spring Boot 3.x 使用jakarta.*命名空间,MyBatis-Plus 对应的 starter 是mybatis-plus-spring-boot3-starter。而 Spring Boot 2.x 使用的是javax.*命名空间,MyBatis-Plus 的 starter 是mybatis-plus-boot-starter。

如果 AI 给你生成了mybatis-plus-boot-starter的依赖坐标,你又用着 Spring Boot 3.x,项目可能连编译都过不了;反过来,你在 Spring Boot 2.x 里用了 3.x 的坐标,运行时会报各种类找不到。排查思路很简单:先确认自己的 Spring Boot 版本,再核对 MyBatis-Plus 的依赖坐标。零基础阶段每走一步都去 Maven 仓库确认一下,能省掉大量调试时间。

9. 最佳实践与工程建议

CRUD 看起来简单,但真正写得好、上线稳定,需要养成几个习惯。这些习惯比多写几个接口更重要。

9.1 永远不要信任前端传参

新增接口要有必填校验,修改接口要防止非法字段更新,删除接口要确认资源存在。这些校验逻辑虽然简单,但每一层都能挡住一批线上事故。不要以为“前端已经校验过了”,HTTP 请求可以绕过前端直接打到服务端。服务端校验是最后一道防线。

9.2 删除操作多思考逻辑删除

能逻辑删除就不要物理删除。尤其是用户、订单、支付记录这类核心数据,物理删除等于把恢复数据的可能性杀掉了。逻辑删除的代码成本很低,但带来的安全感很高。当然,也要注意逻辑删除字段要加入查询条件,否则会出现“删除后依然能查询到”的尴尬。

9.3 接口返回结构必须统一

每个接口的返回结构应该一致,最好都包含 code、message、data。这能降低前后端联调成本,也能让接口文档更规范。这里不展开讲全局异常处理,但建议你在项目里引入@RestControllerAdvice,把业务异常统一包装成Result,避免异常堆栈直接暴露给客户端。

9.4 写操作要考虑事务

新增、修改、删除如果涉及多张表,必须加@Transactional。比如“创建订单”要同时扣库存、生成订单记录、写入日志,任何一个环节失败,前面已经提交的数据就会变成脏数据。单表的 CRUD 暂时用不到事务,但这是从“会写 CRUD”走向“写可靠的业务代码”的必经一步。

9.5 善用日志和慢查询排查

开发阶段打开 MyBatis-Plus 的 SQL 日志,能帮你确认框架到底执行了什么语句。上线后记得关闭 SQL 日志,改用专业的慢查询分析和监控工具。实际项目中,很多“接口变慢”的问题,都是从 SQL 开始排查的。

9.6 AI 生成代码后的审查清单

如果你是在 AI 的帮助下完成 CRUD,发布前可以按这个清单检查一遍:

  • 依赖坐标与当前 Spring Boot 版本是否匹配?
  • 数据库连接配置是否指向正确环境?
  • 逻辑删除配置是否开启?
  • 新增和修改接口是否有基础参数校验?
  • 修改接口是否存在“不允许修改的字段被更新”的风险?
  • 查询接口是否可能产生全表扫描?
  • 是否有 SQL 注入风险?(使用 MyBatis-Plus 的LambdaQueryWrapper和#{}能有效降低风险)

记住一句话:AI 编写代码,你负责判断。判断力来自对业务的理解,也来自对服务端基础概念的掌握。

9.7 一个可以快速上手的最小实践路径

如果你是零基础,建议不要直接照抄整篇文章的所有代码,而是按下面这个路径走:

第一步,先手工建表,执行建表 SQL,用 Navicat 或命令行确认表结构。

第二步,新建项目,配置数据源,启动一次空的 Spring Boot 工程。

第三步,加上 MyBatis-Plus 依赖,用 AI 生成实体类和 Mapper,手动写一个“查询用户列表”接口,先跑通一条链路。

第四步,在最短的链路上扩展新增、修改、删除接口,每写一个接口就用 curl 或 Postman 测试一个。

第五步,回过来把异常处理、参数校验、逻辑删除这些工程实践补齐。

这个过程可能只需要一个下午。跑通之后你会发现,所谓“服务端增删改查”,本质上就是客户端和服务端约定好一套规则,服务端再把规则翻译成数据库操作。掌握了这个闭环,再去学习 Spring Security、Redis、消息队列,就有了扎实的地基。

总结与后续学习方向

这篇博客真正想讲清楚的是三件事:增删改查在服务端开发中的完整链路是什么;零基础阶段如何借用 AI 工具提升效率而不是被 AI 带偏;以及一个最小可运行的用户管理项目从建表到测试的全过程。

下一步,你可以尝试把示例中的单表 CRUD 扩展到一个稍微复杂的业务场景,比如“用户 + 订单”两张表的联合查询,或者为接口增加 JWT 登录认证。这些方向都会让你越来越深入地理解服务端开发,但无论走多远,每天都会和今天我们讲的这四种操作打交道。把它们彻底弄懂,比盲目学习更多框架有意义得多。

建议你把这篇文章收藏备用,特别是“常见问题与排查思路”那张表,会在你第一次独立写接口报错时帮上大忙。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询