
先直接说结论MybatisPlus这玩意在Java后端项目里的普及程度已经高到“新项目不开它反而会被人问一句为什么”的地步。我最早是抱着“MyBatis已经够用了加个Plus多此一举”的心态去看的结果在小团队里带了三四个项目之后发现它确实把日常CRUD的开发效率拉高了不只一个档次尤其是配合Spring Boot使用几乎不需要写XML单表操作全被BaseMapper接管。这篇就按我实际项目的使用路径把整合配置、条件构造器、核心插件、踩坑实录完整拆一遍。适合刚接触MybatisPlus的初级开发也适合已经在用但想系统排查一遍使用姿势的中级选手。1. 先弄清楚MybatisPlus到底解决什么问题1.1 原生MyBatis的三大痛点原生MyBatis足够灵活但灵活是有代价的。我实际感受最深的三个点第一单表CRUD的模板代码太多。每个表都要配一个Mapper接口、一个XML文件里面写一堆insert、update、selectById、selectList字段一多几十行就下去了。表少还好业务一多几十张表光这些模板就够写一阵子。第二分页不统一。手写分页最常见的就是用PageHelper或者自己拼LIMIT。一旦多个项目并行你会发现每个项目分页的返回结构、参数命名都不一样联调的时候前后端经常因为字段对不上扯皮。第三条件查询的SQL拼接非常疲惫。用户筛选项一多你就要在XML里堆whereif test...等条件多了以后那个标签嵌套复杂程度基本就接近“屎山施工现场”了。改一个条件经常要连带改XML、改接口、改SQL。MybatisPlus解决的就是这三个痛点单表CRUD零SQL、分页插件统一分页、Wrapper条件构造器动态拼条件。它的设计定位也很明确——不取代MyBatis而是在MyBatis之上做增强复杂SQL你仍然可以写XML框架不会拦你。1.2 它适合什么项目不适合什么项目以我个人的判断标准适不适合上MybatisPlus要看项目里SQL的复杂度分布。如果你的项目里80%以上是单表操作条件查询就是等值、范围、模糊、排序这几种那MybatisPlus就是性价比非常高的选择。尤其是团队里有初级开发上手快出错率低代码审查也轻松。如果项目是报表类、复杂关联查询非常多、需要深度调优SQL、对数据库方言有强依赖那我不建议你把核心查询也交给MybatisPlus。不是说它做不到而是这种场景用原生MyBatis或者更直白的工具控制力更强。MybatisPlus适合当主力CRUD工具但复杂统计SQL仍然要走自定义。我自己的经验是“混合使用”——单表和轻度关联用MybatisPlus复杂报表用XML手写双方不冲突。这也是大多数项目里最稳的组合方式。1.3 原生MyBatis和MybatisPlus的直观对比对比项原生MyBatisMybatisPlus单表CRUD手写SQL/模板BaseMapper内置方法零SQL动态条件XMLif标签Wrapper链式构造分页依赖第三方插件内置分页插件字段映射需手动配resultMap驼峰映射自动处理主键策略手工指定或注解内置多种主键策略逻辑删除自行实现注解全局配置即可这个表格只是给个直观感觉真正的差异会在后面几个章节里展开。2. Spring Boot整合第一篇基础配置不能踩坑2.1 依赖引入和关键配置我用的是Spring Boot 2.7.xMybatisPlus用的3.5.x版本。引入依赖时有个常见分歧到底引mybatis-plus-boot-starter还是mybatis-plus这里必须说明白Spring Boot项目引前者就对了它会自动装配省去一堆手动Bean配置。dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency配置参数里我实际项目里用的是这几个核心项mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: assign_id logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0log-impl这个配置强烈建议开发环境打开能看到每一条自动生成的SQL日志排查问题非常有用。生产环境再关掉不然日志量太大。2.2 实体映射的三个注解实体类上的注解不多但每个都值得注意。最常用的是TableName、TableId、TableField三个。TableName用于实体类对应表名。当表名和类名不一致时比如类名叫UserInfo表名叫sys_user_info就必须显式指定。TableId标注主键。这里有一个我踩过的坑如果不标框架默认把名为id的字段当主键当你的主键叫uid或者order_no分页插件和selectById就会出问题。主键类型我习惯用IdType.ASSIGN_ID返回的是雪花ID分布式环境下不会撞。数据库自增的场景用IdType.AUTO。TableField主要用于两种情况。一是字段名和列名不一致时指定映射关系二是非表字段要标注exist false比如类里有个remarkExtra属性只是用来看的但数据库里没有对应列不标会直接映射报错。2.3 BaseMapper和Service层的快速路Mapper接口继承BaseMapperT就拥有了17个内置方法基本覆盖单表CRUD。但这只是开胃菜真正舒服的是Service层继承IServiceT实现类继承ServiceImplT, M, E。我举个例子传统写法你写一个分页查询用户列表可能要自己组装Page对象、手动设置总条数。用IService之后直接PageUser page userService.page( new Page(1, 10), new LambdaQueryWrapperUser().eq(User::getStatus, 1) );注意这里的page方法第一个参数是分页对象第二个是条件构造器。返回的Page对象里records是当前页数据total是总条数getCurrent()是当前页码。这套结构用顺手之后再回到手写分页会觉得非常返祖。2.4 仍然可以自定义SQL不冲突经常有人担心用了MybatisPlus以后是不是就写不了复杂SQL了。不是。你完全可以在Mapper接口里定义方法然后在XML或注解里写自己的SQL。我在项目里常见的做法是单表查询、简单关联查询走MybatisPlus内置方法或Wrapper多表关联、子查询、报表统计走Mapper自定义方法。这样两层分工代码好维护SQL也好排查。自定义SQL结合MybatisPlus有个非常实用的点在XML里你依然可以使用${ew.customSqlSegment}来引用Wrapper条件。比如你想写一个自定义JOIN查询但条件部分又希望调用方通过Wrapper传入就可以这样。不过这个用法有个前提Wrapper里的条件字段必须是实体类的属性跨表字段你还需要用apply来手拼条件。这个进阶玩法建议等项目用到了再细抠没必要一上来就搞。3. 条件构造器实战QueryWrapper、LambdaQueryWrapper、UpdateWrapper3.1 QueryWrapper最常用方法清单条件构造器是MybatisPlus的灵魂也是很多人第一次用的时候觉得“这玩意怎么这么多方法”的原因。其实核心就是下面这张表记住以后日常开发足够了。方法对应SQL片段说明eq ?等值匹配ne ?不等匹配gt/ge ?/ ?大于/大于等于lt/le ?/ ?小于/小于等于likeLIKE %?%模糊匹配betweenBETWEEN ? AND ?范围匹配inIN (?,?,?)集合匹配isNull/isNotNullIS NULL/IS NOT NULL空值判断orderByDesc/orderByAscORDER BY排序last直接拼到SQL末尾慎用有SQL注入风险实际项目中条件构造器最常见的用法是在列表页查询。前端传来一堆筛选条件后端用Wrapper链式拼接。3.2 为什么我强烈建议优先用LambdaQueryWrapperQueryWrapper用字符串指定字段名比如wrapper.eq(user_name, name)。看起来没毛病但其实有个隐患字段名写错不会在编译期报错运行时报错或者查出错误数据你还不一定能立刻发现。更糟的是如果数据库字段重构、改名你需要在所有字符串里搜一遍。LambdaQueryWrapper用方法引用来指定字段wrapper.eq(User::getUserName, name)。这样做的直接好处是——字段名是被编译器检查过的类里有没有这个getter一目了然重构时代码跟着变。这属于“用时间换安全”的典型我实际改项目的时候深有体会字符串SQL在批量调整字段命名时就是灾难源。Lambda还有个隐藏优势配合IDEA的插件代码提示非常友好。你输入User::get的时候IDE会直接把所有字段getter列出来省去翻表结构的时间。3.3 动态条件拼接判空写法的细节动态条件的核心就是“有值才拼接条件”。MybatisPlus的条件方法普遍重载了一个boolean参数版本比如eq(boolean condition, R column, Object val)。我习惯在Service层参数为空的时候统一走这个重载。先看一段反面示例很多初学者会这样写LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); if (StringUtils.isNotBlank(name)) { wrapper.eq(User::getName, name); } if (age ! null) { wrapper.eq(User::getAge, age); }这样写没错但条件一多满屏if看得人牙疼。更优雅的写法LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.isNotBlank(name), User::getName, name) .eq(age ! null, User::getAge, age) .like(StringUtils.isNotBlank(company), User::getCompany, company);一行一个条件体积小逻辑清晰。而且这样写还有个好处空值条件自动不拼接SQL相当于帮你做了一层过滤。3.4 and/or嵌套的实际坑复杂查询里最容易翻车的是and和or的嵌套。SQL本身就存在优先级问题AND优先级高于OR你用Wrapper拼接的时候也很容易拼出跟预期不符的SQL。假设你要查询状态等于1且名称包含“张”或备注包含“VIP”。SQL应该是WHERE status 1 AND (name LIKE %张% OR remark LIKE %VIP%)用Wrapper写的时候一定要用and(Consumer)做括号包裹wrapper.eq(User::getStatus, 1) .and(w - w.like(User::getName, 张) .or() .like(User::getRemark, VIP));如果图省事不嵌套直接.eq(...).like(...).or().like(...)拼出来的SQL就会变成WHERE status 1 AND name LIKE %张% OR remark LIKE %VIP%这结果完全跑偏。我的经验是只要条件和或同时出现永远先考虑用and(消费者Lambda)或or(消费者Lambda)包一层宁可多写括号不要省这个步骤。3.5 UpdateWrapper做局部更新UpdateWrapper相对冷门但实际项目中更新某几条记录的特定字段时特别好用。比如批量更新用户状态按某个条件置为禁用LambdaUpdateWrapperUser wrapper new LambdaUpdateWrapper(); wrapper.eq(User::getCompanyId, companyId) .set(User::getStatus, 0) .set(User::getUpdateTime, new Date()); userMapper.update(null, wrapper);注意这里update的第一个参数传null表示完全用UpdateWrapper的set来指定更新内容。如果你传了实体对象实体里的非空字段也会参与更新两种策略叠在一起容易出现“想更新A结果B也被改了”的意外。所以明确用Wrapper做更新时第一参数建议传null。4. 五类高价值插件分页、代码生成、逻辑删除、自动填充、乐观锁4.1 分页插件配置和方法调用MybatisPlus的分页插件官方叫PaginationInnerInterceptor配置一个Bean即可生效。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这里有个非常关键的细节只配置了拦截器还不够你还必须用它自带的分页方法比如Page对象作为参数传给selectPage或page。如果你自己传了LIMIT参数进去拼SQL那分页插件是不生效的。实际项目里最常犯的错误就是把Page参数放在Mapper方法的非最后一位参数或者手动翻了页但忘了传Page对象。分页性能上我建议大表场景一定要配合索引设计。MybatisPlus分页插件生成的count语句是自动优化的比如它会去掉order by来计算总数所以性能一般可控。但如果你用了很多apply拼接子查询count语句可能性能下降这种复杂分页我建议还是走自定义SQL。4.2 代码生成器项目的启动加速度说句实在话代码生成器这个东西用好了是神器用不好就是项目里一大堆冗余代码的元凶。MybatisPlus的AutoGenerator可以一次性生成Entity、Mapper、Service、ServiceImpl、Controller但我的建议是——不要让Controller也自动生成。原因很简单前端对接风格每个项目都不同自动生成的Controller经常大改一通不如只生成到Service层。生成器的核心配置我摘一段实际用过的配置骨架FastAutoGenerator.create(jdbc:mysql://localhost:3306/db_demo, root, password) .globalConfig(builder - builder.author(dev).outputDir(D://code)) .packageConfig(builder - builder.parent(com.demo).moduleName(user)) .strategyConfig(builder - builder.addInclude(sys_user)) .execute();注意几个关键点addInclude用于指定要生成的表名不指定就是所有表作者名在生成后如果调试报错、同事看到会被问“为什么代码里都写你的名字”个人项目无所谓团队项目建议写团队代号。生成完以后务必把Entity里的字段对照真实表结构过一遍因为自动生成只认数据库的注释和类型有些字段的TableField映射不一定符合你的业务语义。4.3 逻辑删除配置简单但设计要谨慎逻辑删除是我在MybatisPlus里最“又爱又怕”的功能。爱它是因为一条注解就能实现“删除数据不消失”怕它是因为一旦配置错全表数据可能被意外过滤。配置方式分两步。第一步全局配置里声明逻辑删除字段名global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0第二步在实体类字段上加TableLogic注解TableLogic private Integer deleted;这样所有deleteById、deleteByWrapper都会自动改成UPDATE ... SET deleted 1所有普通查询都会自动追加AND deleted 0。这个功能有三类坑必须提醒第一唯一索引容易废。业务上经常有个场景用户名唯一。你逻辑删除了一条记录再插入同名的用户如果数据库里用户名列有唯一索引插入会直接报错因为那条记录物理上还在表里。解决办法是设计唯一索引时把deleted也放进去或者在插入前先做物理清理这些都属于业务层面的取舍。第二批量更新/插入时要注意deleted字段。你的实体类如果没有显式设置deleted且数据库列本身有默认值0那没问题。但如果列没有默认值插入时就会被空值顶掉导致逻辑删除字段非0新数据一进库里就查不出来。第三逻辑删除只对MybatisPlus内置方法生效你自己写的XML SQL不会自动追加条件。所以团队里如果有同学习惯手写SQL很容易绕过逻辑删除把数据物理删了。4.4 自动填充别再手动给createTime赋值很多项目每张表都有create_time、update_time两个字段传统写法是在每个Service里手动setCreateTime(new Date())。这个操作重复不说还极其容易被遗漏。MybatisPlus的自动填充可以把这个交给框架。实现分两步第一步实体类字段标注填充策略TableField(fill FieldFill.INSERT) private Date createTime; TableField(fill FieldFill.INSERT_UPDATE) private Date updateTime;第二步定义一个MetaObjectHandler的BeanComponent public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, Date.class, new Date()); this.strictInsertFill(metaObject, updateTime, Date.class, new Date()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, Date.class, new Date()); } }这里有个小细节值得说strictInsertFill的字段名是实体类属性名不是数据库列名。实际上如果字段名和列名的映射是通过TableField自定义的这里填的是属性名。另外strict前缀的意思是只在字段为空时填充如果业务里确实需要手动指定时间它不会被覆盖这个设计很合理。4.5 乐观锁并发场景下的保底方案乐观锁在MybatisPlus里实现极其轻量。实体类上用一个Version注解的字段然后配置乐观锁拦截器更新时框架自动做SET version version 1 WHERE version ?。Version private Integer version;配置方法还是在MybatisPlusInterceptor里追加一个interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());这个功能我用到的场景主要是“防止老数据覆盖新数据”。比如两个管理员同时编辑同一个商品A先提交B后提交没有乐观锁时B会把A的修改覆盖掉。加了版本号后B提交时版本号已经不一样了更新影响行数为0业务层就可以感知到并发冲突并给出提示。有一点必须提醒乐观锁拦截器只对updateById(entity)和update(entity, wrapper)生效对直接用UpdateWrapper的set不生效。如果你想用LambdaUpdateWrapper做乐观锁并发保护框架是拦不住的。这也是很多同事踩过坑的地方更新字段用Wrapper、版本校验没生效数据就被静默覆盖了。5. 常见问题与排查实录5.1 高频报错速查表先放一张问题速查表都是我在实际项目中遇到或协助排查过的高频问题对照着看能快速定位方向。现象可能原因解决方向Invalid bound statementMapper接口方法在XML里没找到检查XML路径、namespace、方法名是否一致查询返回null但数据库有数据驼峰映射未开启检查map-underscore-to-camel-case配置updateById影响行数为0数据未变更 / 乐观锁版本不一致打印日志确认字段检查Version分页总数一直是0分页拦截器未配置检查是否添加PaginationInnerInterceptor逻辑删除后无法插入同唯一键数据数据库唯一索引包含已删除记录从索引维度重新设计自动填充时间没有生效实体未标注fill或 Handler未注册检查TableField(fill...)和Bean自定义SQL查不出逻辑删除过滤数据手写SQL不会自动加deleted0手写SQL自行追加条件实体属性在SQL里找不到属性标注了existfalse确认是否确实需要排除映射5.2 三个排查实录讲透思路第一个是分页总数不准。有个项目统计用户列表查询条件里有一个apply(date_format(create_time, %Y-%m) 2024-08)。一开始分页正常后来人为改了数据总数开始不对。排查后发现apply里拼的是原生SQL片段分页插件统计总数时不会解析apply内部的逻辑它只是简单把外层查询包了一层COUNT。如果apply里有子查询或关联依赖总数统计很容易出偏差。解决办法是换一种写法把日期范围转换成语义化条件用between或ge/le替代。第二个是字段映射不上。实体类里有个userName数据库列是user_name但查询出来始终是null。检查配置map-underscore-to-camel-case是true按理说能映上。最后发现是实体类上多写了一个TableField(username)把映射关系写死成了单字段驼峰转换被覆盖了。这个属于典型的低级错误——但低级错误最坑人因为不好查。第三个是逻辑删除加唯一索引的冲突。这个在前面已经提到过这里补一个实际解法把deleted字段改成一个可变的随机值。逻辑删除时不是固定设1而是设为当前时间戳或随机数。这样一条记录被删除后deleted变成了一个唯一的值同名字段的新记录插入时只要新记录deleted0唯一索引就不会冲突。这个方案用下来确实解决了业务矛盾代价是逻辑删除字段的类型得改成长整型且全局配置要调整。5.3 我的几条使用建议第一简单CRUD和无脑单表操作用MybatisPlus但复杂SQL一定要手写。判断标准很简单如果一条查询要JOIN三张以上表或者里面有GROUP BY和HAVING我建议直接在XML里写不要硬用Wrapper拼。Wrapper拼复杂SQL不是不能而是代码可读性会很差后面交接的时候别人看半天看不懂。第二Service层的粒度控制。IService自带的方法特别多但不代表你每个Controller都能直接调。我在项目里通常会做一层薄薄的Service封装把业务校验、事务控制放在里面Controller只调用Service暴露的方法不直接操作Mapper。这样MybatisPlus的便利性保留住了业务边界也没有被冲散。第三实体类不要什么都往里塞。很多人会把查询参数、排序条件、甚至前端传过来的分页字段都塞进实体最后实体膨胀得几十个字段。MybatisPlus有内置的Page对象查询条件可以用一个专门的DTO类来接收这样职责明确实体只跟表结构对应。这个做法维护下来非常清爽。第四日志一定要在开发环境打开。SQL日志是排查MybatisPlus问题的最好工具很多时候发现问题就是多看一眼输出的SQL语句。你看到实际执行了什么样的SQL就知道框架到底给你拼了什么。等确认没问题了生产环境再关闭日志也不迟。6. 关于扩展方向的一点个人体会最后再分享一个我这几年的体会。很多人把MybatisPlus单纯当成一个CRUD框架其实它的插件体系才是真正的宝藏。除了前面讲的分页、逻辑删除、乐观锁它还有多租户插件、数据权限插件、动态表名插件这些能力在某些项目中能省掉一整层AOP开发量。我印象最深的是数据权限的场景。业务上需要做“用户只能看自己部门的数据”这种过滤传统做法是在每个查询方法里手动拼接数据范围条件非常容易漏。而MybatisPlus的多租户插件本身就是基于拦截器自动拼接SQL条件稍微改造一下就能用作数据权限过滤团队里讨论下来至少节省了几天的开发时间。不过我的建议是插件的引入要克制。一个项目里装饰器式地堆插件看起来功能很全但出问题时排查链路会非常长。好的使用方式是先打牢基础——Wrappers熟练、分页配置正确、逻辑删除设计过一遍再去考虑插件层面的增强。顺序反了很容易被框架的便利性迷惑最后陷入“为什么这里结果不对”的泥潭里。如果你正准备在下一个项目里引入MybatisPlus我的建议是先小范围试用把BaseMapper和LambdaQueryWrapper跑通再逐步加上分页、逻辑删除、自动填充这三个最常见的功能跑过一两个迭代之后你自然会对它的边界有更真切的理解。这套路线我用过也带人走过效果比一开始就全量铺开要稳得多。