Day22 MyBatis-Plus深度实践:动态SQL、分页、乐观锁、逻辑删除

发布时间:2026/9/28 20:17:52

Day22 MyBatis-Plus深度实践:动态SQL、分页、乐观锁、逻辑删除
专栏《Java后端工程师进阶之路》从CRUD到AI工程师的完整跃迁路径Day 22/ 90MyBatis-Plus 的Wrapper链式 API就是为这种场景而生的。它不能替代所有 XML但能把 80% 的 CRUD 从“拼字符串”变成“写 Java 代码”让你的 SQL 在编译期就能被检查到一部分。一、环境准备Spring Boot 3 集成 MyBatis-Plusspring: datasource: url: jdbc:mysql://127.0.0.1:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: auto logic-delete-field: deleted # 全局逻辑删除字段 logic-delete-value: 1 logic-not-delete-value: 0 mapper-locations: classpath*:/mapper/**/*.xml二、动态 SQL从 XML 中解放出来MyBatis-Plus 的动态 SQL 核心就是Wrapper。它分为两类基于字符串的QueryWrapper/UpdateWrapper基于 Lambda 的LambdaQueryWrapper/LambdaUpdateWrapper我强烈建议用 Lambda 版本。原因就一个字段名一改编译器立刻报错不会把user_name拼成userName留到运行时才暴露。实体类与 Mapper复杂条件查询Service public class UserService extends ServiceImplUserMapper, User { public PageUser searchUser(UserSearchDTO dto, int current, int size) { LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); // 1. 模糊匹配用户名仅当参数不为空 wrapper.like(StringUtils.isNotBlank(dto.getUserName()), User::getUserName, dto.getUserName()); // 2. 精确匹配手机号 wrapper.eq(StringUtils.isNotBlank(dto.getMobile()), User::getMobile, dto.getMobile()); // 3. 状态必须明确传值才过滤不传就不过滤 wrapper.eq(dto.getStatus() ! null, User::getStatus, dto.getStatus()); // 4. VIP 等级范围 wrapper.ge(dto.getMinVipLevel() ! null, User::getVipLevel, dto.getMinVipLevel()) .le(dto.getMaxVipLevel() ! null, User::getVipLevel, dto.getMaxVipLevel()); // 5. 注册时间范围 wrapper.between(dto.getStartTime() ! null dto.getEndTime() ! null, User::getRegisterTime, dto.getStartTime(), dto.getEndTime()); // 6. 排序最后登录时间倒序再按注册时间倒序 wrapper.orderByDesc(User::getLastLoginTime) .orderByDesc(User::getRegisterTime); return baseMapper.selectPage(new Page(current, size), wrapper); } }这段代码比 XML 直观在哪里每个条件前面第一个参数就是“是否生效”的开关null/空字符串自动跳过字段是User::getUserName这种 Lambda 引用重构时 IDE 能追踪不会忘记11前缀也不会因为漏写AND导致 SQL 语法错误默认已经帮你处理好参数注入不存在字符串拼接的 SQL 注入风险。生成的 SQL 类似这样SELECT id, user_name, mobile, status, vip_level, register_time, last_login_time FROM t_user WHERE user_name LIKE %张% AND status 1 AND vip_level 1 AND vip_level 5 AND register_time BETWEEN 2025-01-01 AND 2025-12-31 ORDER BY last_login_time DESC, register_time DESC LIMIT 0, 20三、分页别再手写LIMIT和COUNTMyBatis-Plus 的分页是我使用频率第二高的功能。它自动帮你处理两件事先执行COUNT(*)查总数再执行带LIMIT的查询把结果封装成PageT返回。配置分页插件高版本已经不需要PaginationInterceptor了统一用MybatisPlusInterceptorConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 添加分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 添加乐观锁插件后面讲 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }分页查询实战返回的Page对象包含records当前页数据total总记录数pages总页数current/size当前页和页大小optimizeJoinOfCountSql如果你联表查询分页可以调优 count SQL分页前一定要记得加ORDER BY否则 MySQL 返回的顺序是不可预期的分页结果会“乱跳”。四、乐观锁解决并发更新覆盖问题并发下最常见的坑A 和 B 同时读取同一条记录A 修改后保存B 也修改后保存结果 A 的修改被 B 覆盖。乐观锁的思路是给表加一个版本号字段version每次更新时version 1并且WHERE version 旧值。如果已经被别人更新过这次更新就返回 0 行影响业务层可以重试或提示。启用乐观锁实体类加VersionData TableName(t_account) public class Account { TableId(type IdType.AUTO) private Long id; private String userName; private BigDecimal balance; Version private Integer version; }配置插件前面已加new OptimisticLockerInnerInterceptor()。乐观锁使用示例Service public class AccountService extends ServiceImplAccountMapper, Account { public boolean deductBalance(Long accountId, BigDecimal amount) { // 1. 先读取当前记录和版本号 Account account baseMapper.selectById(accountId); if (account null || account.getBalance().compareTo(amount) 0) { throw new RuntimeException(账户不存在或余额不足); } // 2. 业务上扣减余额 account.setBalance(account.getBalance().subtract(amount)); // 3. MyBatis-Plus 会自动 WHERE version 旧值 int affected baseMapper.updateById(account); if (affected 0) { // 版本冲突可重试或抛异常 throw new RuntimeException(并发冲突请稍后重试); } return true; } }执行时生成的 SQLUPDATE t_account SET balance ?, version version 1 WHERE id ? AND version ?关键提醒只有updateById和update(wrapper, entity)这种标准更新会被插件拦截手写updateBatchSomeColumn或自定义 XML 不会自动加版本控制version字段不要手动 set 新值让插件自动加 1。五、逻辑删除别让数据真的“消失”生产环境真正的删除很少见。MyBatis-Plus 的逻辑删除机制是给表加一个deleted字段删除时只更新该字段查询时自动过滤deleted 0。配置与实体全局配置已经在 yaml 中写了logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0实体上也可以单独标注Data TableName(t_user) public class User { // ... 其他字段 TableLogic private Integer deleted; }使用方式// 调用删除时MyBatis-Plus 会自动改成 UPDATE userService.removeById(1001L); // 对应 SQL // UPDATE t_user SET deleted 1 WHERE id 1001 AND deleted 0 // 查询时自动过滤 User user userService.getById(1001L); // 对应 SQL // SELECT ... FROM t_user WHERE id 1001 AND deleted 0两个容易踩的坑1. 唯一索引冲突如果user_name有唯一索引删除后再注册同名用户会报错。解决思路联合唯一索引改成(user_name, deleted)允许同名用户一个被删除、一个未删除或者删除时把用户名后面拼接逻辑删除标识例如user_name _deleted_ id。2. 级联/外键场景逻辑删除只是更新一个字段关联表的数据不会自动被处理。如果子表有外键你需要在业务层自行维护级联逻辑删除或者干脆不用外键、用应用层约束。六、自动填充create_time / update_time 不需要手动 set几乎每个表都有create_time和update_timeMyBatis-Plus 提供MetaObjectHandler自动填充Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, deleted, Integer.class, 0); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }实体字段TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;这样你写 Service 的时候就不用再手动 set 创建时间代码干净很多。七、三条能直接落地的经验1. Wrapper 用 Lambda 版不要用字符串版字符串版QueryWrapper在字段重命名时不会报错极容易留下运行时才暴露的隐患。团队规模越大越要坚持 Lambda 版本。如果有人非要用字符串写复杂条件建议写到自定义 XML 里至少 SQL 一目了然。2. 分页查询一定要做“慢查询兜底”分页再方便也不代表查询本身不会慢。LIKE %xxx%不走索引、ORDER BY用错字段、连表分页都会产生大偏移量问题。生产环境建议单表分页超过 10 万行时考虑用延迟关联或上次查询的最大 ID 游标给分页接口加超时控制避免前端一直转圈用Page的setSearchCount(false)跳过 count当只展示“下一页”按钮时特别有用。3. 乐观锁和逻辑删除不是银弹要看业务场景乐观锁适合“读多写少、冲突概率低”的场景比如账户余额、库存扣减逻辑删除适合审计要求高的业务但会让统计报表查询变复杂需要同时看deleted 0和deleted 1如果业务对实时性要求极高悲观锁SELECT FOR UPDATE或 Redis 分布式锁反而更合适。MyBatis-Plus 是我最近几年用得最顺手的持久层工具之一。它不是要取代 MyBatis而是把 80% 的重复劳动接管过来让你把注意力放在那 20% 真正复杂的 SQL 上。用好Wrapper、分页、乐观锁、逻辑删除、自动填充这几张牌你的持久层代码会少一半维护起来也会轻松很多。工具选得好CRUD 写得少基础打得牢AI 才接得稳。下一篇 Day 23我们进入持久层选型的深水区《JPA vs MyBatis从架构哲学到性能数据的全面对比》。我会用同一套业务场景分别用 JPA 和 MyBatis 写一遍给你看看到底该怎么选。参考资料MyBatis-Plus 官方文档Spring Boot 3.2 官方文档MySQL 8.0 性能优化官方指南

相关新闻

遗传算法实战:N皇后问题的Python可执行实现与调试指南

遗传算法实战:N皇后问题的Python可执行实现与调试指南

2026/8/23 0:15:53

1. 项目概述:从理论到可运行代码的遗传算法实战落地你有没有试过写完一个算法原理,回头一看——代码根本跑不起来?或者明明理解了选择、交叉、变异这些概念,但一到写n_queen_solver.py就卡在“怎么把棋盘编码成染色体”这一步&…

【从循环累加到实时时钟】—— 汇编语言综合实验项目解析

【从循环累加到实时时钟】—— 汇编语言综合实验项目解析

2026/9/24 15:24:33

1. 汇编语言实验项目设计思路第一次接触汇编语言时,很多人会被寄存器、内存地址这些概念吓到。其实汇编就像乐高积木,虽然零件简单,但组合起来能创造无限可能。这个综合实验项目就是从最简单的累加运算开始,逐步搭建出一个完整的电…

华为MetaERP Oracle EBS 三种成本模式全流程详解(以生产 1 台重型卡车为例)前置统一业务背景(三种核算共用业务场景)业务基础数据原材料:底盘总成(PO 采购,订单单价 10 万

华为MetaERP Oracle EBS 三种成本模式全流程详解(以生产 1 台重型卡车为例)前置统一业务背景(三种核算共用业务场景)业务基础数据原材料:底盘总成(PO 采购,订单单价 10 万

2026/8/23 0:15:53

Oracle EBS 三种成本模式全流程详解(以生产 1 台重型卡车为例)前置统一业务背景(三种核算共用业务场景)业务基础数据原材料:底盘总成(PO 采购,订单单价 10 万 / 套,标准成本 9.5 万&…

CANN/GE ACL数据集缓冲区添加函数

CANN/GE ACL数据集缓冲区添加函数

2026/9/28 4:08:17

aclmdlAddDatasetBuffer 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、Te…

用ffmpeg高效批量调整图片尺寸的实战指南

用ffmpeg高效批量调整图片尺寸的实战指南

2026/9/28 16:01:49

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

Transformers 音频特征提取工具库 audio_utils 全解析:从 Mel 刻度换算到对数 Mel 频谱

Transformers 音频特征提取工具库 audio_utils 全解析:从 Mel 刻度换算到对数 Mel 频谱

2026/9/28 2:15:29

Transformers 音频特征提取工具库 audio_utils 全解析:从 Mel 刻度换算到对数 Mel 频谱 【免费下载链接】transformers 🤗 Transformers: the model-definition framework for state-of-the-art machine learning models in text, vision, audio, and mu…

RustFS 多节点集群重启与滚动升级实战:Readiness、Quorum 与 Degraded 模式完全指南

RustFS 多节点集群重启与滚动升级实战:Readiness、Quorum 与 Degraded 模式完全指南

2026/9/28 3:14:54

RustFS 多节点集群重启与滚动升级实战:Readiness、Quorum 与 Degraded 模式完全指南 【免费下载链接】rustfs 🚀2.3x faster than MinIO for 4KB object payloads. RustFS is an open-source, S3-compatible high-performance object storage system sup…

Java Integer缓存揭秘:128陷阱原理、避坑与面试全解

Java Integer缓存揭秘:128陷阱原理、避坑与面试全解

2026/9/28 3:58:00

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

RustFS Scanner 数据用量发布权威性决策:配额准入如何获得可用的权威依据

RustFS Scanner 数据用量发布权威性决策:配额准入如何获得可用的权威依据

2026/9/28 3:47:14

RustFS Scanner 数据用量发布权威性决策:配额准入如何获得可用的权威依据 【免费下载链接】rustfs 🚀2.3x faster than MinIO for 4KB object payloads. RustFS is an open-source, S3-compatible high-performance object storage system supporting mi…

远程协作的工作台整理

远程协作的工作台整理

2026/9/28 16:01:48

远程协作的工作台整理远程协作的核心不是再加一个工具,而是让交接信息足够完整。异步任务要写明目标、输入位置、完成标准和需要决策的人。 工作台的最小配置 将日程、待办、代码和沟通入口收拢到少数固定位置;通知按紧急程度分层。工作台不需要模仿办公…

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

2026/9/28 5:05:21

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

2026/9/28 16:01:48

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…