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

发布时间:2026/7/15 20:49:22

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/7/15 20:49:22

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

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

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

2026/7/15 20:49:22

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

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

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

2026/7/15 20:39:22

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

Pixhawk PosHold模式原理与4.1.7固件实操指南

Pixhawk PosHold模式原理与4.1.7固件实操指南

2026/7/15 22:29:27

1. 项目概述:为什么PosHold模式是飞手从“能飞”迈向“会控”的第一道分水岭你刚拿到Pixhawk飞控的无人机,调参软件里密密麻麻的飞行模式图标看得眼花——Stabilize、AltHold、Loiter、RTL、Auto……其中那个标着“PosHold”的选项,既不像Sta…

Pixhawk飘移模式原理与实操:精准横向位移控制技术解析

Pixhawk飘移模式原理与实操:精准横向位移控制技术解析

2026/7/15 22:29:27

1. 什么是飘移模式?它不是“失控”,而是有目的的横向运动控制“Pixhawk无人机教程-4.1.5 飘移模式 Drift Mode”这个标题里,“Drift Mode”常被新手误读为“漂移”“打滑”甚至“失控”,其实完全相反——它是Pixhawk飞控在保持高度…

【Springboot毕设全套源码+文档】基于Java的星海书店管理系统的设计与实现(丰富项目+远程调试+讲解+定制)

【Springboot毕设全套源码+文档】基于Java的星海书店管理系统的设计与实现(丰富项目+远程调试+讲解+定制)

2026/7/15 22:29:27

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围:&am…

【Springboot毕设全套源码+文档】基于java的校园闲置物品交易平台设计与实现(丰富项目+远程调试+讲解+定制)

【Springboot毕设全套源码+文档】基于java的校园闲置物品交易平台设计与实现(丰富项目+远程调试+讲解+定制)

2026/7/15 22:29:27

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围:&am…

模板驱动型文档自动化:从Word手工到可审计流水线

模板驱动型文档自动化:从Word手工到可审计流水线

2026/7/15 22:29:27

1. 这不是“点几下就出PDF”的玩具,而是把文档生产从手工作坊升级成流水线的底层逻辑 你有没有过这种经历:客户要一份产品说明书,你翻出去年的Word模板,改标题、换图片、删掉过时参数,再手动调整页眉页脚——3小时过去…

Rasa Action Server异步实践:从async/await到任务队列

Rasa Action Server异步实践:从async/await到任务队列

2026/7/15 22:19:26

1. 项目概述:Rasa Action Server 的异步能力到底能走多远?在构建真实业务场景下的对话机器人时,我遇到过太多次“用户等得不耐烦”的反馈。比如,当用户问“查一下我上个月的账单”,后端要调用财务系统、生成PDF、再上传…

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践

2026/7/14 10:03:09

Unity游戏文本翻译架构深度解析:XUnity.AutoTranslator的技术实现与工程实践 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator XUnity.AutoTranslator作为Unity游戏社区中最成熟的文本翻译解决方…

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持

2026/7/13 20:43:19

openEuler Raspberry Pi Kernel设备驱动开发指南:为树莓派硬件添加支持 【免费下载链接】raspberrypi-kernel It provides openEuler kernel source for Raspberry Pi 项目地址: https://gitcode.com/openeuler/raspberrypi-kernel 前往项目官网免费下载&…

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧

2026/7/15 0:26:43

openEuler系统集成测试实战:基于smoke-test套件的环境验证技巧 【免费下载链接】integration-test The repo contains test suits for system integration test 项目地址: https://gitcode.com/openeuler/integration-test 前往项目官网免费下载:…

【LINUX】驱动

【LINUX】驱动

2026/7/15 0:08:14

【LINUX驱动】【字符设备】【中断】【Platform】【网课 设备树】【GPIO】【PINCTRL】【INPUT】【IIC】【SPI】【网络驱动】【屏幕驱动】【一 设备树】【二 内核模块编译】【三 基本驱动框架】【四 Platform总线设备驱动框架】【五 驱动子系统】【六 综合】

【1982-2026】全国高精度建筑轮廓|村级精度|SHP矢量

【1982-2026】全国高精度建筑轮廓|村级精度|SHP矢量

2026/7/15 0:08:14

🔍 数据简介 本次分享1982-2026年全国村级精度建筑轮廓矢量数据,覆盖全国各省市区县,到村级别精细,为2026年最新实时采集成果,非网传仅60/77个城市的老旧数据。 数据含带高度/不带高度双版本,单体建筑边界精…

【1975-2026】全国水系水路数据|河流/水库/运河|SHP矢量

【1975-2026】全国水系水路数据|河流/水库/运河|SHP矢量

2026/7/15 0:08:14

🔍 数据简介 本次分享1975-2026年全国高精度水系水路矢量数据,覆盖全国全域,包含河流、水系、水库、运河、湿地、冰川、沟渠等全类别水文要素。 数据集包含双层矢量图层,字段分类清晰、要素齐全,支持2013-2026逐年完整…