第二章: [核心] 通用 CRUD 与 Service 接口
第二章: [核心] 通用 CRUD 与 Service 接口
Prorise第二章. [核心] 通用 CRUD 与 Service 接口
摘要: 本章将揭开 MyBatis-Plus (MP) 效率革命的核心面纱——通用 CRUD。我们将不再编写重复的 SQL,而是通过继承 BaseMapper 和 ServiceImpl,在分钟级内完成标准的数据访问层与业务层构建。同时,我们将验证 MP 的“无侵入”特性,演示如何在享受便捷的同时,依然能够灵活编写复杂的自定义 SQL。
本章学习路径
- Mapper 革命:掌握
BaseMapper的核心原理,通过一行代码解锁 17 个数据库操作方法。 - CRUD 实战:深度测试增删改查,理解 MP 如何处理逻辑删除(
@TableLogic)与动态更新。 - 架构升级:从 Dao 层迈向 Service 层,利用
IService实现事务封装与批量操作优化。 - 边界突破:当通用方法无法满足需求时,如何优雅地回退到原生 MyBatis 编写自定义 SQL。
2.1. BaseMapper:零 SQL 的奥秘
在上一章,我们创建了一个空的 UserMapper 接口。
1 | public interface UserMapper extends BaseMapper<UserDO> {} |
这行看似简单的代码背后,MP 利用 Java 泛型 和 反射机制,在启动时自动解析 UserDO 的字节码,分析 @TableName 和 @TableId 等注解,动态生成了对应的 SQL 语句(如 INSERT INTO tb_user...),并注入到 MyBatis 的容器中。
这就是 BaseMapper 的核心魔法:以类为表,以属性为字段,自动映射。
2.1.1. [新增] 插入与主键回填
首先,我们测试最基础的插入操作。
场景描述:向数据库插入一条新用户,并验证 ID 是否自动生成,以及插入后是否能直接获取到该 ID。
文件路径: src/test/java/com/example/mpstudy/mapper/UserMapperTest.java
1 |
|
1
2
3
4
5
6
----- [1] 开始执行 Insert 测试 -----
==> Preparing: INSERT INTO tb_user ( name, age, email, version, is_deleted, gmt_create, gmt_modified ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
==> Parameters: Prorise(String), 30(Integer), prorise@example.com(String), 1(Integer), 0(Integer), 2025-12-14...(Timestamp), 2025-12-14...(Timestamp)
<== Updates: 1
受影响行数: 1
回填的主键 ID: 6 <-- 注意:这里获取到了数据库自增的 ID
深度解析:
- 字段自动填充:注意 SQL 中的
version、is_deleted、gmt_create等字段。虽然我们在 Java 代码中只设置了 name/age/email,但由于数据库设置了DEFAULT值(或者使用了 MP 的自动填充功能,将在后续章节讲解),数据完整性得到了保证。 - 主键回填:这是 JDBC 的特性,MP 完美封装了它。在执行
insert后,user.getId()会立即有值,无需再次查询。
2.1.2. [删除] 逻辑删除的“骗局”
在企业级开发中,物理删除(DELETE FROM)是非常危险的操作。我们在第一章中配置了 @TableLogic,现在是见证它生效的时刻。
场景描述:调用删除接口,观察实际执行的 SQL 语句是否变成了 UPDATE。
1 |
|
1
2
3
4
5
6
----- [2] 开始执行 Delete 测试 -----
-- 重点观察:SQL 动词是 UPDATE 而不是 DELETE
==> Preparing: UPDATE tb_user SET is_deleted=1 WHERE id=? AND is_deleted=0
==> Parameters: 1(Long)
<== Updates: 1
deleteById 受影响行数: 1
核心原理:由于我们在实体类 isDeleted 字段上添加了 @TableLogic,MP 的 deleteById 方法被“偷梁换柱”了。它执行的是更新操作,将 is_deleted 标记为 1。同时,后续所有的 select 操作都会自动加上 WHERE is_deleted=0,从而在业务层面实现数据的“隐身”。
2.1.3. [更新] 动态 SQL 的智慧
updateById 是一个智能方法,它只会更新那些“非 null”的字段。
场景描述:只修改用户的年龄,不修改邮箱和名字。
1 |
|
1
2
3
4
5
----- [3] 开始执行 Update 测试 -----
-- 重点观察:SET 子句中只有 age 字段,name 和 email 未出现在 SQL 中
==> Preparing: UPDATE tb_user SET age=? WHERE id=? AND is_deleted=0
==> Parameters: 99(Integer), 4(Long)
<== Updates: 1
注意:updateById 的默认策略是 忽略 null 值。如果你真的想把某个字段更新为数据库的 NULL(例如清除邮箱),通过 updateById 传入 null 是无效的。这种情况需要使用 UpdateWrapper(将在第三章讲解)或配置全局策略 update-strategy。
2.2. Service 层:架构的升维
在简单的 Demo 中,直接在 Controller 调用 Mapper 是可以的。但在真实项目中,我们需要 Service 层 来承载业务逻辑、事务控制和缓存处理。
MP 提供了 IService<T> 接口和 ServiceImpl<M, T> 实现类,它们是对 BaseMapper 的进一步封装,提供了更贴近业务语义的方法(如 save, saveBatch, saveOrUpdate)。
2.2.1. 定义 Service 接口与实现
这是一次标准化的代码结构搭建,请务必遵循。
步骤 1:定义接口
文件路径: src/main/java/com/example/mpstudy/service/UserService.java
1 | package com.example.mpstudy.service; |
步骤 2:实现接口
文件路径: src/main/java/com/example/mpstudy/service/impl/UserServiceImpl.java
1 | package com.example.mpstudy.service.impl; |
2.2.2. 批量操作的性能优化 (saveBatch)
在原生 MyBatis 中,批量插入通常需要手写 <foreach> 标签,容易出错且性能受限于 SQL 长度。MP 的 saveBatch 方法在底层通过 JDBC 的 Rewrite Batched Statements 进行了优化。
场景描述:模拟导入 1000 个用户,对比循环插入与批量插入。
文件路径: src/test/java/com/example/mpstudy/service/UserServiceTest.java
1 |
|
2.2.3. 智能存储 (saveOrUpdate)
这是 ETL(数据抽取转换加载)任务中的神器。
场景描述:尝试保存一个用户对象,如果它有 ID 且数据库存在,则更新;否则插入。
1 |
|
2.3. 混合开发:自定义 SQL
MP 再强大,也不可能覆盖所有复杂的 SQL 场景(如多表关联查询、复杂的报表统计)。MP 的设计哲学是 开放 的,它允许你随时切回原生 MyBatis。
2.3.1. [实战] 定义 XML Mapper
需求:通过用户名称查询用户(虽然这个可以用 MP Wrapper 实现,但为了演示流程,我们假设这是一个非常复杂的 SQL)。
步骤 1:定义接口方法
在 UserMapper.java 中添加:
1 | public interface UserMapper extends BaseMapper<UserDO> { |
步骤 2:创建 XML 文件
文件路径: src/main/resources/mapper/UserMapper.xml
重要:确保 application.yml 中配置了 mybatis-plus.mapper-locations,否则 MP 找不到这个 XML。
1 |
|
步骤 3:配置路径 (如果尚未配置)
1 | mybatis-plus: |
测试验证:
1 |
|
2.4. 本章小结
本章我们实现了从“手写 SQL”到“方法调用”的跨越。
核心要点:
- BaseMapper:提供了
insert,deleteById,updateById,selectById等原子操作,且自动处理主键回填。 - 逻辑删除:
deleteById实际执行的是UPDATE,前提是配置了@TableLogic。 - IService:
saveBatch和saveOrUpdate是业务层的利器,能大幅减少代码逻辑。 - 混合模式:不要被 MP 限制住,复杂的查询依然推荐使用 XML 手写 SQL,两者可以完美共存。
2.5. 本章总结与 CRUD 速查
2.5.1. 场景一:标准 Service 层 CRUD
需求:快速实现对单表的用户管理业务。
方案:继承 ServiceImpl。
代码模版:
1 | // 1. Controller 调用 |
2.5.2. 场景二:批量数据处理
需求:从 Excel 导入 5000 条用户数据。
方案:使用 IService.saveBatch,避免在 for 循环中调用 save。
代码模版:
1 | public void importUsers(List<UserDO> excelData) { |
2.5.3. 核心避坑指南
updateById 不更新 null 值
- 现象:想把
email字段清空,传入email=null,但数据库未发生变化。 - 原因:MP 默认更新策略是
NOT_NULL(忽略 null 字段)。 - 对策:使用
UpdateWrapper强制更新,或者在字段上加@TableField(updateStrategy = FieldStrategy.IGNORED)(不推荐全局开启)。
- 现象:想把
自定义 XML 报错
Invalid bound statement- 现象:调用自定义的
selectByName方法时抛出异常。 - 原因:Mapper 接口的方法名与 XML 中的
id不一致,或者mapper-locations路径配置错误导致 XML 没被加载。 - 对策:检查 YAML 配置路径,确保 XML 文件在 build 后的
target/classes目录下存在。
- 现象:调用自定义的
逻辑删除的大坑
- 现象:手写 XML SQL 时,查出了已删除的数据。
- 原因:MP 的自动逻辑删除过滤只对 MP 内置方法(
selectById等)有效,对自己手写的 SQL 无效。 - 对策:在手写 XML 时,必须手动添加
WHERE is_deleted = 0。
下章预告:掌握了 CRUD 只是第一步。在实际业务中,我们往往需要执行复杂的条件查询,比如“查询年龄大于 20 且 名字包含 ‘Jack’ 或 邮箱不为空的用户”。不仅如此,我们还需要处理分页。在下一章,我们将解锁 MP 最强大的武器——条件构造器 (Wrapper) 与 分页插件。







