公司动态
SpringBoot整合MyBatisPlus的实用配置指南
有人把MyBatis-Plus当作“进阶版MyBatis”装上就能跑但真正压榨出它的效率需要把配置细节抠到骨子里。SpringBoot整合MyBatis-Plus的门槛极低低到几乎不需要思考可一旦你的项目里冒出多租户、复杂分页、字段自动填充、逻辑删除混用这些需求那些默认配置就会变成隐形的雷。我们今天不聊基础CRUD只聊那些真正影响生产环境的配置逻辑以及为什么这么配、不这么配会怎样。依赖引入的版本陷阱很多新手直接复制官方文档的依赖却忽略了版本兼容性。SpringBoot 2.x与3.x对MyBatis-Plus的依赖坐标要求完全不同3.x下必须用mybatis-plus-spring-boot3-starter用旧坐标会直接启动失败且报错信息极度迷惑——不是类找不到而是DataSource初始化异常。更隐蔽的是当你的项目同时引入了mybatis-plus-boot-starter和mybatis-spring-boot-starter两个框架会争抢SqlSessionFactory的创建权最终触发“Invalid bound statement (not found)”之类的诡异异常。解决方法是彻底排除mybatis-spring的传递依赖只保留MP的starter。如果你用的是低版本MyBatis-Plus3.4.x以下还需要手动引入jsqlparser依赖否则分页插件和条件构造器会在解析SQL时抛异常。建议直接锁定MyBatis-Plus 3.5.5这个版本已经内置了jsqlparser解析模块省去大量版本冲突的调试时间。yml配置里的性能命门mybatis-plus.configuration.map-underscore-to-camel-case默认是true这没问题但配置mybatis-plus.global-config.db-config.id-typeassign_id时要注意分布式环境下雪花ID的时钟回拨问题。如果你的服务器通过NTP同步时间一旦发生时间回拨雪花ID生成器会抛异常或产生重复ID。此时需要自定义IdentifierGenerator类重写nextId方法加入序列号回退保护。别小看这个点很多支付系统的订单号重复事故就是这么来的。另一个配置重灾区是mybatis-plus.configuration.log-impl。生产环境千万别配StdOutImpl控制台会疯狂打印SQL和参数IO压力陡增。正确做法是配org.apache.ibatis.logging.slf4j.Slf4jImpl再结合日志框架设置logger级别为debug只针对某个mapper包。同时mybatis-plus.global-config.bannerfalse可以关掉启动时的漫天羊驼Logo不是为好看而是为了减少无意义的日志输出。分页插件配置——不止于一个Bean网上80%的分页插件配置都是错的。他们只写了Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }这段代码能跑但分页插件必须排在拦截器链的末尾如果同时配置了非法SQL拦截器、乐观锁插件顺序错了会导致分页count查询被其他拦截器截胡。另外分页插件内部封装的JsqlParserGlobal支持堆内存和缓存配置默认每1.5秒清理一次SQL解析缓存。当你的表字段超过30个、嵌套子查询特别多时解析耗时可能达到几十毫秒。推荐显式设置PaginationInnerInterceptor.setMaxLimit(500L)防止有人恶意传超大页码拖垮数据库同时开启optimizeCountSql和optimizeJoinOfCountSql两个优化参数虽然默认值已经开启但在古老版本中需要手动配置。一个更阴间的场景是多表联查时分页插件生成的count SQL会裁剪掉order by但会错误的保留left join产生的多余字段导致count结果异常。此时你可以使用自定义count方法或在SQL语句里用MP的InterceptorIgnore(tenantLine true)注解跳过某些逻辑。记住分页插件不是万能的复杂报表查询建议手动写count SQL。逻辑删除的陷阱——你以为删除就没了逻辑删除配置了global-config.db-config.logic-delete-field和logic-not-delete-value之后所有deleteById都会变成update操作这没问题。但逻辑删除与唯一索引会产生致命冲突假设你的用户表手机号有唯一索引用户删除后再次注册相同手机号因为旧记录逻辑上只变了deleted字段唯一索引仍然生效导致插入失败。解决办法有很多但最优雅的是将deleted字段保留同时增加一个deleted_bucket字段用于存放删除时间戳唯一索引改为(phone, deleted_bucket)删除时不只置为1而是将deleted_bucket设为当前时间戳。这种模式需要自定义MetaObjectHandler来实现后面我们会讲。同时逻辑删除字段必须用Integer或Long类型如果你用BooleanMP的logic-not-delete-value配置就要写成Boolean.FALSE并且数据库字段类型要对应。在编写自定义SQL时TableLogic的字段不会自动拼接条件你得手动加WHERE deleted 0。你可以在MP的配置里开启mybatis-plus.global-config.sql-parser-cachetrue或者利用逻辑删除的logic-delete-value配合SqlParser(filter false)强制忽略逻辑删除条件但请谨慎使用一旦忽略被删除的数据就会重见天日。自动填充——别偷懒用SQL默认值字段自动填充常被用来处理create_time、update_time。很多教程让你在实体类上写TableField(fill FieldFill.INSERT)然后自定义一个MetaObjectHandler在insertFill和updateFill里用setFieldValByName(createTime, new Date())。这种方式有个大坑如果你需要填充的值依赖当前登录用户ID、租户ID、会话上下文单纯在MetaObjectHandler里取不到Request对象因为MP的填充逻辑发生在MyBatis参数构建阶段此时你还可以通过RequestContextHolder获取请求但如果在异步线程、MQ消费者中执行插入RequestContextHolder会为null。更合理的做法是在填充器中优先从方法参数和实体字段获取值不要硬编码当前时间。例如Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime::now, LocalDateTime.class); this.strictInsertFill(metaObject, creator, UserContext::getUserId, Long.class); }这里strictInsertFill的妙处在于它只有在实体类对应字段为null时才填充不会覆盖你业务层手动设置的值。另外自动填充只对MP的CRUD方法生效当你使用Insert自定义SQL时填充逻辑完全失效需要在Mapper接口的方法上增加TableField对应的逻辑或者干脆不用自定义SQL插入。如果你的表有多个数据库默认值比如status字段默认1建议不要依赖MP自动填充而把默认值放在数据库层面因为INSERT语句不包含该字段时数据库会自行处理MP填充反而会增加不必要的代码分支。乐观锁——配置顺序错了等于没有悲观锁、乐观锁的选择已经不用多说了MP的乐观锁实现依赖Version注解和OptimisticLockerInnerInterceptor。这个拦截器参与MP的拦截器链顺序必须在分页插件之前因为如果分页插件先执行它会将原始SQL改写成分页SQL然后乐观锁插件再去识别version条件时会因为SQL结构改变而导致解析失败。正确的顺序是多租户插件 → 乐观锁插件 → 非法SQL拦截插件 → 分页插件如果有多个。这是官方文档没强调的细节。然而乐观锁插件不适用于批量更新updateBatchById也不适用于Wrapper自定义更新语句。它会正常把你的实体版本号加1但如果你用update(user, wrapper)这种形式且wrapper中没有version条件MP不会自动帮你拼接版本号需要手动eq(version, user.getVersion())。还有一点乐观锁的version字段推荐用Integer或Long不建议用Timestamp因为时间戳在毫秒级并发下仍会碰撞而递增数字更可靠。在配置时建议同时设置global-config.db-config.version-entity? 实际上没有这个配置你需要做的只是确保实体类字段注解里明确Version。性能分析插件——慢SQL的照妖镜MP自带的PerformanceInterceptor旧版或SqlExplainInterceptor可以输出每条SQL的执行耗时但在3.5.x版本中官方把这些插件移除了推荐使用MybatisPlusInterceptor配合P6Spy或者Druid的慢SQL日志功能。如果你还在用PerformanceInterceptor在MP 3.5.5中会直接报错ClassNotFound。我们推荐用Druid连接池自带的慢SQL监控功能在yml里配置spring: datasource: druid: filters: stat,wall connection-properties: druid.stat.slowSqlMillis1000这样超过1秒的SQL会输出到日志。如果你想拿到MP打印的预编译SQL和参数开启mybatis-plus.configuration.log-impl为Slf4jImpl后在日志配置中设置com.baomidou.mybatisplus.mapper为TRACE级别。慢SQL排查不仅看耗时还要看SQL解析计划MP的InterceptorIgnore注解可以跳过某些插件对特定SQL的干扰这在排查死锁场景时非常有用。注意性能分析插件对你没用但统计是评估优化效果的前提生产环境建议把慢SQL阈值设为500ms。代码生成器的正确打开方式很多人用AutoGenerator自动生成controller/service/entity生成的代码直接扔进项目但生成的实体类默认带TableId(type IdType.ASSIGN_ID)却忽略了逻辑删除字段需要加TableLogic、版本号字段需要加Version所以代码生成后还需要大量手动调整。正确做法是自定义模板或者至少配置GlobalConfig的setEntityName、setServiceName并在StrategyConfig里设置logicDeleteFieldName和versionFieldName这样生成出的代码才能直接用。更关键的是代码生成器需要和数据库表结构严格同步如果你用update自动同步表结构MP不会帮你加索引也不会为你处理字段类型映射。例如MySQL的datetime会被映射为LocalDateTime而Oracle的TIMESTAMP也会映射为LocalDateTime但是当字段类型是BIGINT UNSIGNED时JDBC驱动会把值映射为BigInteger此时实体类里如果你定义成了Long就会在查询时出现类型转换异常。代码生成器生成的实体类尽量保持简洁业务校验放到DTO层不要把MP实体的字段和数据库一一对应强绑定因为后期你添加一个冗余字段MP的selectById总会去查该字段可能导致SQL报错。多租户插件——隐藏的SQL屠杀如果你的系统是SaaS架构MP的TenantLineInnerInterceptor可以自动为每个SQL拼接租户条件省事是省事但它默认对每个表都强制拼接租户ID导致某些字典表不分租户被错误过滤数据直接查空。你需要用TenantLineHandler的ignoreTable方法精确指定哪些表不参与租户隔离。另外多租户插件和逻辑删除、乐观锁同时存在时SQL解析器会按顺序重写SQL一旦租户表名在JOIN中被你起了别名插件解析可能失败需要保证表名和实际库表名完全一致。更隐蔽的问题是当你在XML中自定义复杂SQL时如果SQL中包含了嵌套子查询、UNION这类结构多租户插件在3.5.5之前版本无法正确解析UNION会只给前半部分加租户条件导致数据泄露。如果你必须用UNION建议拆成两条SQL或者给对应的Mapper方法增加InterceptorIgnore(tenantLine true)然后手动在SQL里写上租户条件。只要涉及多租户配置顺序ignoreTable缺一不可且必须在测试环境构造跨租户数据验证否则上线后就是安全事故。自定义SQL与注解SQL的最佳实践道听途说总说MP不能写复杂SQL其实可以。你可以直接在Mapper接口的方法上写Select注解也可以选择在XML里写。但一旦使用了XML文件你要确保mybatis-plus.mapper-locations配置正确且XML的namespace与Mapper接口全限定名一致。MP对Select注解里的动态条件支持有限比如想要if这种标签只能写在XML里。最实用的一种玩法是在XML里写基础查询用MP的Wrapper作为参数通过${ew.customSqlSegment}来动态拼接条件例如select idselectMyPage resultTypeUserVO SELECT FROM user ${ew.customSqlSegment} /select然后调用时传入LambdaQueryWrapper这样既能利用MP的条件构造器又能自定义返回字段和Join逻辑。但要注意${ew.customSqlSegment}是直接字符串拼接wrapper中如果包含ORDER BY并注入字符串可能引起SQL注入MP的wrapper参数值经过预编译但customSqlSegment本身没有参数化建议对所有自定义查询排序字段做白名单校验。另外MP的BaseMapper已经提供了selectList、selectPage但当你需要返回DTO而非实体类时可以定义VO类用TableName标记映射关系或者简单点直接在Mapper里写一个返回ListMapString,Object的方法然后用MyBatis的MapKey处理。SQL性能优化的终点往往是把压力从数据库转移到索引上MP的QueryWrapper里的last(limit 10)可以帮助你快速测试分页查询但小心last里如果有; delete这种危险语句会被直接拼接所以last只适用于内部低风险操作生产环境切勿对用户输入使用last。事务与批量操作的终极协同SpringBoot中使用Transactional管理事务但MP的saveBatch、updateBatchById其实默认是不走事务的如果不加Transactional批量操作中途失败时已经插入的数据不会回滚。在批量操作时MP的分批插入默认每1000条提交一次如果你的事务传播级别是REQUIRES_NEW会导致部分提交无法回滚。推荐在批量操作时降低批大小或者干脆自己用for循环配合SqlSessionTemplate的batch模式。还有一点MP的saveBatch在遇到主键冲突时会直接抛异常不会像MySQL的insert ignore那样静默跳过所以你要么在service层提前判断要么使用自定义SQL插入时加ON DUPLICATE KEY UPDATE。如果真的需要批量插入同时更新冲突字段MP 3.5.5提供了一个insertOrUpdateBatch但它的实现是先查exists再决定insert或update性能极差高并发下建议手写SQL。记住最靠谱的批量操作方案是走XML使用foreach标签拼接INSERT配合Transactional保证原子性。这里的“最佳实践”很反直觉MP的默认能力适合单条CRUD批量能力反而是它的短板。配置的本义是把工具适配到你的场景而不是被工具牵着走。MyBatis-Plus的默认配置看起来很合理但每个默认值背后都牺牲了一部分灵活性和安全性。真正熟练的开发者会从业务出发反向定制MP的每个插件和策略而不是等线上出了数据泄露、分页超时、唯一索引冲突后再翻日志。你至少应该做到理解拦截器链的顺序熟悉逻辑删除和唯一索引的冲突模型掌握自动填充和上下文安全获取以及敢于放弃MP自带的批量方法而手写高性能SQL。这些配置不是零散的代码片段而是一张环环相扣的安全网它决定你的项目在并发攻击和脏数据面前是坚如磐石还是纸糊的墙。当你下次在一个SpringBoot项目里引入MyBatis-Plus时不要急着在pom里粘贴依赖。先花十分钟想清楚你的场景是否有多租户逻辑删除怎么处理唯一键冲突分页查询的极限在哪里代码由谁生成这些问题想透了配置自然就长在你的代码里而不是从网上复制粘贴。