简介本资源是一份面向Java初学者与SpringBoot入门开发者的增删改查实例Demo聚焦于使用SpringBoot框架与MySQL数据库交互解决数据表自动生成及CRUD操作的学习需求。压缩包共103个文件以79个xml配置文件、8个java源码、8个class编译文件、4个yml配置及properties、jar等为主涵盖项目依赖、实体类、控制器、服务层与仓库接口等模块整体约92KB结构紧凑便于快速导入运行。目前已有8680人学习下载适合需要动手实践Spring Data JPA的开发者参考。实例中完整演示了通过JpaRepository的save()方法实现新增与更新、findAll()与findById()完成查询、deleteById()执行删除并对比原生SQL与JdbcTemplate操作方式同时涉及Transactional声明式事务管理与ddl-auto自动建表配置可帮助读者理解SpringBoot项目分层结构与数据库交互流程为后续构建RESTful接口或微服务应用打下基础。1. 从一份 SpringBoot 增删改查实例 Demo 说起它到底能帮你省掉哪些重复劳动如果你写过几个 SpringBoot 项目大概率会发现一个规律不管业务怎么变底层那套增删改查的骨架几乎一模一样。Controller 接参数、Service 转逻辑、Mapper 落库、实体类映射字段来来回回就是这几层。真正让人头疼的不是写不出来而是每次新建一个模块都要重新搭一遍稍不留神就漏了分页、忘了事务、参数校验没加、返回格式不统一。这份 SpringBoot 增删改查实例 Demo 解决的正是这个问题——它把一套完整的 CRUD 流程从建表到接口测试全部串起来包括实体定义、Mapper 映射、Service 事务控制、Controller 参数接收与统一响应封装。适合刚接触 SpringBoot 的开发者照着跑通第一遍也适合有经验的工程师拿来当脚手架模板改改包名和表名就能直接用在模拟项目里。下面我按实际拆包的顺序把这份资源的结构、跑法、参数配置和容易翻车的地方逐一讲清楚。2. 拆开这份 Demo分层结构、依赖版本与建表脚本2.1 工程目录与分层逻辑拿到一份 SpringBoot 增删改查实例第一件事不是急着跑而是先看它的包结构。常见做法是按controller、service、service.impl、mapper、entity、common这几个包来分。这份 Demo 也不例外整体遵循典型的 MVC 三层架构但有几个细节值得注意。entity包下放的是与数据库表一一对应的实体类字段名和表字段通常用驼峰与下划线做映射。mapper包下是 MyBatis 的接口方法名对应 SQL 的语义比如selectById、insert、updateById、deleteById。service层负责业务组合和事务边界controller层只做参数接收和结果返回不写业务逻辑。common包里一般会有统一返回体Result、分页对象PageResult、全局异常处理GlobalExceptionHandler这几样东西。我一般会先确认一件事这个 Demo 用的是 MyBatis 还是 MyBatis-Plus。两者在增删改查的写法上差别很大。如果是原生 MyBatisXML 文件里会写完整的 SQL如果是 MyBatis-Plus很多基础方法直接继承BaseMapper就有了不需要手写。从这份实例的定位来看它更偏向原生 MyBatis 的写法因为这样更能看清 SQL 映射的完整链路对新手理解底层更有帮助。2.2 依赖版本与关键配置打开pom.xml核心依赖通常包括这几项!-- SpringBoot Web 起步依赖提供内嵌 Tomcat 和 MVC 支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis 与 SpringBoot 整合包 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency !-- MySQL 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- Lombok用来省掉 getter/setter -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency这里有几个参数需要留意。mybatis-spring-boot-starter的版本要和 SpringBoot 版本匹配如果 SpringBoot 用的是 3.xMyBatis starter 也要用对应的 3.x 版本否则启动时会报NoSuchMethodError。MySQL 驱动在 8.x 之后包名从mysql-connector-java变成了mysql-connector-j这个细节很多人会忽略导致依赖拉不下来。application.yml里的配置同样关键spring: datasource: url: jdbc:mysql://localhost:3306/demo_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case这个参数建议打开它能让数据库的user_name自动映射到实体类的userName省掉大量手动映射。serverTimezone必须指定否则 MySQL 8 会报时区错误。mapper-locations的路径要和实际 XML 文件存放位置一致放在resources/mapper下是最常见的做法。2.3 建表脚本与初始化数据一份能跑通的增删改查 Demo必然附带建表 SQL。通常是一张用户表或商品表字段不会太多但覆盖了自增主键、字符串、数字、时间这几类常见类型。CREATE TABLE t_user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, user_name varchar(64) NOT NULL COMMENT 用户名, age int DEFAULT NULL COMMENT 年龄, email varchar(128) DEFAULT NULL COMMENT 邮箱, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; INSERT INTO t_user (user_name, age, email) VALUES (张三, 25, zhangsanexample.com), (李四, 30, lisiexample.com);建表时用utf8mb4而不是utf8这是血泪经验——后者在存储某些特殊字符时会出问题。create_time和update_time交给数据库自动维护比在代码里手动 set 更可靠。初始化两条数据是为了后面测试查询和更新时有现成的记录可用。3. 跑通增删改查从 Mapper 到 Controller 的完整链路3.1 实体类与 Mapper 接口定义实体类用 Lombok 的Data注解省掉样板代码字段与表字段通过驼峰映射对应。Data public class User { private Long id; private String userName; private Integer age; private String email; private Date createTime; private Date updateTime; }Mapper 接口定义四个基础方法对应增删改查Mapper public interface UserMapper { // 根据 ID 查询 User selectById(Long id); // 查询全部 ListUser selectAll(); // 新增 int insert(User user); // 更新 int updateById(User user); // 删除 int deleteById(Long id); }Mapper注解让 MyBatis 在启动时扫描到这个接口并生成代理实现。如果不想在每个接口上都加可以在启动类上用MapperScan(com.example.demo.mapper)统一指定。对应的 XML 映射文件放在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.demo.mapper.UserMapper select idselectById resultTypeUser SELECT * FROM t_user WHERE id #{id} /select select idselectAll resultTypeUser SELECT * FROM t_user ORDER BY id DESC /select insert idinsert useGeneratedKeystrue keyPropertyid INSERT INTO t_user (user_name, age, email) VALUES (#{userName}, #{age}, #{email}) /insert update idupdateById UPDATE t_user set if testuserName ! nulluser_name #{userName},/if if testage ! nullage #{age},/if if testemail ! nullemail #{email},/if /set WHERE id #{id} /update delete iddeleteById DELETE FROM t_user WHERE id #{id} /delete /mapperuseGeneratedKeystrue和keyPropertyid配合使用插入后能把数据库生成的自增 ID 回填到实体对象里后面如果需要用这个 ID 就不用再查一次。set标签配合if做动态更新只更新非空字段这是实际项目里最常用的写法。#{}是预编译占位符能防止 SQL 注入不要用${}拼接字符串。3.2 Service 层的事务控制Service 层是事务的边界。增删改查里单条 insert 或 update 本身是原子操作但如果业务逻辑涉及多步写操作就必须加Transactional。Service public class UserService { Autowired private UserMapper userMapper; public User getById(Long id) { return userMapper.selectById(id); } public ListUser listAll() { return userMapper.selectAll(); } Transactional(rollbackFor Exception.class) public void create(User user) { userMapper.insert(user); } Transactional(rollbackFor Exception.class) public void update(User user) { userMapper.updateById(user); } Transactional(rollbackFor Exception.class) public void delete(Long id) { userMapper.deleteById(id); } }rollbackFor Exception.class这个参数建议显式加上。默认情况下 Spring 只对RuntimeException回滚如果抛的是受检异常事务不会回滚这是一个非常隐蔽的坑。加上这个参数后任何异常都会触发回滚。3.3 Controller 层与统一返回格式Controller 层只做参数接收和结果封装不写业务逻辑。RestController RequestMapping(/api/user) public class UserController { Autowired private UserService userService; GetMapping(/{id}) public ResultUser getById(PathVariable Long id) { return Result.success(userService.getById(id)); } GetMapping(/list) public ResultListUser list() { return Result.success(userService.listAll()); } PostMapping(/create) public ResultVoid create(RequestBody Valid User user) { userService.create(user); return Result.success(); } PutMapping(/update) public ResultVoid update(RequestBody User user) { userService.update(user); return Result.success(); } DeleteMapping(/{id}) public ResultVoid delete(PathVariable Long id) { userService.delete(id); return Result.success(); } }Result是一个统一返回体通常包含code、message、data三个字段。这样前端拿到响应后只需要判断code就能知道成功还是失败不用去解析 HTTP 状态码。Valid注解配合实体类上的校验注解如NotBlank、Min可以在进入业务逻辑之前就拦截非法参数。启动项目后用 curl 或 Postman 测试各个接口# 查询单条 curl http://localhost:8080/api/user/1 # 查询列表 curl http://localhost:8080/api/user/list # 新增 curl -X POST http://localhost:8080/api/user/create \ -H Content-Type: application/json \ -d {userName:王五,age:28,email:wangwuexample.com} # 更新 curl -X PUT http://localhost:8080/api/user/update \ -H Content-Type: application/json \ -d {id:1,userName:张三丰,age:26} # 删除 curl -X DELETE http://localhost:8080/api/user/1每个接口返回的 JSON 结构应该一致code为 200 表示成功data里携带具体数据。如果返回的code不是预期值先看控制台有没有异常堆栈再去检查 SQL 是否执行成功。4. 避坑与排查这份 Demo 跑不起来时先看这几条4.1 启动报数据源配置错误现象项目启动时抛出Failed to configure a DataSource: url attribute is not specified。原因application.yml没有被正确加载或者数据源配置的缩进层级写错了。YAML 对缩进非常敏感spring.datasource下面的url、username、password必须对齐。解决检查application.yml是否在src/main/resources根目录下确认spring:下面datasource:的缩进是两格url等字段再缩进两格。如果用的是application.properties确认 key 的拼写是spring.datasource.url而不是spring.datasource.jdbc-url。4.2 Mapper 接口找不到 XML 映射现象启动时报Invalid bound statement (not found)或者调用接口时提示找不到对应的 SQL。原因mapper-locations配置的路径和 XML 实际存放位置不一致或者 XML 的namespace写错了。解决确认application.yml里mybatis.mapper-locations的值是classpath:mapper/*.xml并且 XML 文件确实放在resources/mapper目录下。再检查 XML 的namespace是否完整对应 Mapper 接口的全限定名比如com.example.demo.mapper.UserMapper少一段都会导致绑定失败。4.3 插入后返回的主键为 null现象调用新增接口后返回的data里id字段是 null但数据库里确实插入成功了。原因insert标签上没有配置useGeneratedKeys和keyProperty或者配置了但keyProperty的值和实体类字段名不一致。解决在insert标签上加上useGeneratedKeystrue keyPropertyid确保keyProperty的值与实体类中主键字段名完全一致。如果主键字段叫userId那keyProperty就要写userId而不是id。4.4 更新操作把未传字段覆盖为 null现象只想更新用户名结果年龄和邮箱被更新成了 null。原因用的是全量更新 SQLUPDATE t_user SET user_name?, age?, email? WHERE id?没有传的字段就被 set 成了 null。解决改用动态 SQL用set配合if testfield ! null只更新非空字段。或者改用 MyBatis-Plus 的updateById它默认就是只更新非空字段。如果业务上确实需要把某个字段置空那就要单独处理不能依赖动态 SQL。4.5 事务不生效导致数据不一致现象Service 方法里先插入一条记录再更新另一条记录更新失败了但插入的记录没有回滚。原因Transactional注解没有生效。常见原因是同类内部方法直接调用绕过了 Spring 的代理或者异常类型不在默认回滚范围内。解决确认Transactional加在 public 方法上且调用方是通过 Spring 注入的 Bean 来调用的不是new出来的对象。同时显式指定rollbackFor Exception.class避免受检异常不回滚。如果确实需要在同类内部调用可以通过AopContext.currentProxy()获取代理对象再调用。5. 进阶用法把这份 Demo 改造成分页查询与批量操作基础增删改查跑通之后实际项目里最先遇到的需求就是分页和批量操作。这份 Demo 的骨架可以直接扩展不需要改动原有结构。分页查询的核心是在 Mapper 里加两个方法一个查总数一个查当前页数据。参数用offset和limit控制。// Mapper 接口新增 Long countAll(); ListUser selectPage(Param(offset) int offset, Param(limit) int limit);select idcountAll resultTypelong SELECT COUNT(*) FROM t_user /select select idselectPage resultTypeUser SELECT * FROM t_user ORDER BY id DESC LIMIT #{offset}, #{limit} /selectService 层封装分页逻辑传入页码和每页条数计算出 offsetpublic PageResultUser page(int pageNum, int pageSize) { long total userMapper.countAll(); int offset (pageNum - 1) * pageSize; ListUser list userMapper.selectPage(offset, pageSize); PageResultUser result new PageResult(); result.setTotal(total); result.setList(list); result.setPageNum(pageNum); result.setPageSize(pageSize); return result; }批量插入用foreach标签拼接 valuesinsert idbatchInsert INSERT INTO t_user (user_name, age, email) VALUES foreach collectionlist itemitem separator, (#{item.userName}, #{item.age}, #{item.email}) /foreach /insert这里有个参数需要留意批量插入的数据量如果超过几千条建议分批执行每批 500 到 1000 条。一次性拼接太长的 SQL 会撞上 MySQL 的max_allowed_packet限制报Packet for query is too large。我一般会在 Service 层用Lists.partition把大列表切分成小批次循环调用 Mapper 的批量插入方法。验证分页是否正确除了看返回的total和list大小还要测边界情况页码为 1、页码为最后一页、页码超出总页数、每页条数为 0 或负数。这些边界不测上线后迟早会被前端反馈回来。从那以后我每次写完分页接口都会强制走一遍这四种边界用例确认返回结构一致、不报错、不返回脏数据。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?