首页 / 资讯中心 / 文章详情

MyBatis-Plus实战:从CRUD增强到性能优化的完整指南

MyBatis-Plus实战:从CRUD增强到性能优化的完整指南 ★ FEATURED ARTICLE
1. 为什么用了MyBatis-Plus之后团队修Bug的效率反而更高了1.1 从一个典型场景说起大概半年前我接手了一个内部老项目。项目用的还是原生MyBatis数据访问层清一色的手写XML。每张业务表对应的Mapper XML里都堆着selectById、selectList、insert、updateById这类模板代码少说也要几百行。表一多复制粘贴就非常容易出问题要么字段名抄错要么漏掉一个where条件要么返回值映射配错。最让人头疼的是查出来的bug还不一定当场暴露经常是上线跑了一两天才在某个边界条件里炸出来。后来我花了一个周末把项目切到了MyBatis-Plus。这个决策让团队里几个同事一开始挺怀疑的觉得多引入一个重度封装的框架会不会反而让问题更隐蔽结果用了大概三周我们明显感觉到一个变化修Bug的效率高了。原因很简单80%的单表CRUD代码从几百行XML缩成了零行SQL错误率跟着降了一大截。出问题之后代码一眼就能看完不用再对着冗长的XML找是不是哪里少了个trim标签。1.2 MyBatis-Plus到底解决了什么MyBatis-Plus本质上是MyBatis的增强工具它没有改变MyBatis的运行机制也不影响你手写XML的能力。它做的核心事情是把那些每张表都一样的单表CRUD逻辑收敛到统一的BaseMapper里。举个例子。在没有MyBatis-Plus之前新建一张订单表你要写一个OrderMapper接口再写一个OrderMapper.xml里面至少要有六个标签insert、deleteById、updateById、selectById、selectList、selectCount。每个标签里的字段列表都要和实体类对一遍少一个字段就是线上事故。而用MyBatis-Plus之后你的Mapper接口只需要继承BaseMapperOrder上面这些事情全部自动搞定。这带来的价值不只是省代码量更重要的是一致性。手写SQL时每个人写的风格都不太一样A同事喜欢用判断空B同事习惯is not nullC同事写delete语句时经常忘了加条件。这些风格和习惯差异在查问题的时候都会变成额外的认知负担。MP把所有单表操作统一成一套API之后团队里的每个人看别人的代码成本都低了一大截。1.3 边界它不是什么都能干MP的定位是非常明确的它就是单表CRUD的增强工具。很多刚接触MP的同事容易走两个极端要么觉得它是银弹所有SQL都想用MP拼出来要么觉得它是个玩具复杂查询还是不敢用。我实际用下来的感觉是这两个极端都要避免。MP适合处理的场景包括单表的增删改查、批量插入、简单的条件筛选、分页查询、逻辑删除、乐观锁更新。这些场景大约覆盖了业务系统里70%的数据访问代码。MP不适合硬扛的场景包括多表关联查询、复杂子查询、报表统计、需要数据库特定函数深度参与的SQL。这些场景老老实实写原生SQL反而更快、更清晰。我见过有人强行用QueryWrapper拼一个三表Join最后SQL又长又丑性能还不好调。MP的设计也考虑到了这一点它支持你在XML或者注解里继续写自定义SQL和MP的自动CRUD能力共存并不冲突。一句话总结边界能用MP解决的用MP不能用MP解决的让它帮你把事务和分页处理好SQL本身还是自己写。2. 环境接入与版本选择比你想的更讲究2.1 依赖引入与Spring Boot的版本对应关系这个坑我一开始就踩了。项目原本是Spring Boot 2.3我直接引入了当时最新的mybatis-plus-boot-starter3.5.x结果启动时报了一堆ClassNotFoundException仔细一看是javax.servlet和jakarta.servlet的差异。后来查了才知道MyBatis-Plus从某个版本开始分出了基于jakarta命名空间的适配Spring Boot 3.x的用户要选对应版本不能随便引入。具体来说如果你的项目还是Spring Boot 2.x就用mybatis-plus-boot-starter版本在3.4.x到3.5.x之间基本都能稳定运行。如果是Spring Boot 3.x或者更新的版本要引入的是mybatis-plus-spring-boot3-starter两者的包路径和部分类名有差异不能混用。我目前的实践经验是Spring Boot 版本MyBatis-Plus 依赖备注2.xmybatis-plus-boot-starter稳定使用3.4.x、3.5.x3.xmybatis-plus-spring-boot3-starter注意javax/jakarta切换3.x JDK17同上确认字节码版本兼容另外还要注意一个细节MP本身依赖MyBatis但它传递引入的MyBatis版本不一定和你项目里其他组件兼容。我曾在一个项目中同时依赖了别的ORM框架结果它传递了一个旧版MyBatisMP启动时直接报Invalid bound statement。解决办法是显式指定MyBatis版本或者在引入其他依赖时排查依赖树把冲突的MyBatis排除掉。2.2 配置文件的三个必备项MP接入Spring Boot之后有三个配置项我是每次都会先检查的。第一个是mapper-locations。如果你项目里还有自定义XML一定要在application.yml里配置扫描路径否则自定义方法全部找不到报的错还是Mapped Statements collection already contains value这种让人一头雾水的提示。mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml第二个是实体类与表名的驼峰映射。MP默认开启map-underscore-to-camel-case也就是说数据库字段create_time会自动映射到实体属性createTime。大多数人会用到这个但我见过有团队数据库字段就是下划线实体类属性也是下划线风格配置又开着驼峰映射结果所有字段都映射不上查出来的全是null。这种情况要么统一风格要么显式关闭这个配置。第三个是逻辑删除配置。MP推荐在application.yml里做全局逻辑删除配置而不只是在实体上加TableLogicmybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这样配置之后所有实体类里如果存在deleted字段都会自动按逻辑删除处理不需要每个实体都去加注解。这个配置对项目新老表都适用能少写很多注解。2.3 代码生成器建议配置化而不是每次重启MP自带的代码生成器AutoGenerator能根据数据库表结构生成实体类、Mapper接口、Service、ServiceImpl和Controller。这个工具用好了能省大量体力活。但我观察到很多人的用法有隐患每次改表结构就重新跑一次生成器把所有文件全量覆盖一遍。这样做的后果是你在Service或Controller里手工写的业务逻辑频繁被覆盖甚至会出现同一个类一会儿带自定义代码一会儿被清空的诡异状态。我的建议是把生成器的输出分成两部分一部分是可以反复覆盖的——实体类、Mapper接口、XML文件另一部分是生成之后就不再动的——Service、ServiceImpl、Controller。或者更稳妥的做法是只生成实体和MapperService层手写。原因是MP的Service层其实只有很少的通用逻辑大部分业务逻辑都会超出模板范畴生成器生成的Service代码很快就变成负担。配置生成器的时候有几个关键点值得注意。数据库驱动、URL、用户名密码要配准这是废话但经常有人在这里翻车。更需要注意的是strategy配置比如entity是否开启Lombok模式、是否覆盖已有文件、是否启用TableLogic逻辑删除。这些配置直接影响到生成出来的实体类长什么样出错的话后面改起来很麻烦。3. 核心能力拆解条件构造器与几种典型的写活方式3.1 QueryWrapper与LambdaQueryWrapper类型安全不是玄学MP最核心、最高频使用的功能就是条件构造器。它允许你用Java代码来构建SQL的WHERE子句替代手写SQL的条件拼接。实际开发中我几乎不用原始的QueryWrapper而是用LambdaQueryWrapper或者LambdaQueryChainWrapper。为什么不用QueryWrapper因为它需要写字符串形式的字段名。比如// QueryWrapper方式 QueryWrapperUser wrapper new QueryWrapper(); wrapper.eq(user_name, 张三).like(nick_name, 小);这里的user_name是字符串编译器不会检查它是否存在。一旦你改过数据库字段名、把user_name改成了name这里不会报错运行时报SQL异常运气不好还会查出错误数据。而LambdaQueryWrapper通过方法引用直接指向实体字段// LambdaQueryWrapper方式 LambdaQueryWrapperUser wrapper Wrappers.lambdaQuery(); wrapper.eq(User::getName, 张三).like(User::getNickName, 小);字段名在编译期就能被检查重构字段名时IDE会同步更新所有引用这个优势在大型项目里非常明显。我见过不止一次因为字符串字段名导致的线上查询查错列的问题所以团队里强制统一使用LambdaQueryWrapper。条件构造器在使用时有几个容易出问题的地方。第一个是and和or的优先级问题。在原生SQL里AND比OR的优先级要高比如where a 1 or b 2 and c 3实际执行的是a 1 or (b 2 and c 3)。MP的QueryWrapper拼接也存在同样的优先级问题如果不加括号很容易出现预期之外的结果。正确写法是// 正确查询a1且(b2或c3) wrapper.eq(a, 1).and(w - w.eq(b, 2).or().eq(c, 3));第二个是like的占位符问题。MP的like默认是在值两侧加%如果你希望只加一侧应该用likeLeft或likeRight。在模糊查询里%的位置直接决定了能否命中索引前导%会让索引失效这一点在做大表查询时尤其重要。第三个是null值的处理。使用条件构造器时如果查询参数是可选的不要用eq把null值直接拼进去否则会生成column null这样永远查不到数据的SQL。MP有一个Condition参数重载可以在传入null时自动忽略该条件// 参数为null时该条件不加入SQL wrapper.eq(StringUtils.isNotBlank(name), User::getName, name);这个写法在分页筛选场景里非常实用可以把多个可空条件的组合处理得非常干净。3.2 LambdaUpdateWrapper用代码替代两行updateXML除了查询条件MP的更新构造器也是我从原生MyBatis切换过来后觉得特别顺手的地方。以前写一个按条件更新字段的SQL需要在XML里写set、where、if而且每加一个可选更新字段就要在if里面加一行。现在直接boolean updated userService.lambdaUpdate() .eq(User::getUserId, userId) .set(User::getStatus, 1) .set(User::getUpdateTime, new Date()) .update();这个写法好在哪里set操作是不需要关心实体类其他字段的写起来很直接。但这里有一个很容易犯的错误不要使用LambdaUpdateWrapper去更新字段为null。举个例子如果你的业务需求是清空用户的昵称用set(User::getNickName, null)去执行MP默认会把null当成未设置字段不生成对应的SQL。解决方案是使用setSql方法// 明确指定SQL片段 wrapper.setSql(nick_name null);这个细节经常有人踩坑我在一个项目里排查过类似问题调用方传参为空结果数据库里的字段没清空导致前端显示旧数据。后来定位到问题就在这个set和setSql的区别上。3.3 AbstractService与IService事务和链式调用的最佳位置MP提供一个ServiceImplM, T基类和IServiceT接口继承之后Service层就自带批量插入、链式查询、链式更新等方法。我个人的实践经验是所有用MP的项目Service层都继承ServiceImpl而不是直接在Mapper层操作。原因有两个。第一个是事务边界的控制。Service层是事务切面生效的地方。如果你在Controller里直接调Mapper事务注解往往无效而通过Service层调用可以统一用Transactional管理事务。MP的ServiceImpl内部所有方法都声明了事务继承之后事务处理会规范很多。第二个是链式调用非常顺手。比如一次性处理某个用户的所有未读消息boolean mark messageService.lambdaUpdate() .eq(Message::getReceiverId, receiverId) .eq(Message::getStatus, 0) .set(Message::getStatus, 1) .update();如果走Mapper则要写一个自定义XML方法而MP的Service层原生支持这套写法大大减少了自定义方法数量。有一点必须说清楚IService只适合单表操作。如果你需要跨多张表做一个复杂的业务聚合还是要自己写Service业务方法然后在里面组合调用多个Mapper的MP方法或者自定义SQL。3.4 逻辑删除与乐观锁让软状态管理规范化逻辑删除几乎是企业级项目的标配因为业务数据不能随便物理删掉否则审计记录、统计数据全乱套了。MP对逻辑删除的支持体现在TableLogic注解和全局配置上。物理删除和逻辑删除是透明的你调deleteById方法实际执行的SQL是UPDATE语句把deleted字段置为1查询的时候MP自动在where后追加deleted 0条件。这里有一个很重要的细节逻辑删除字段的类型建议用Integer或Long不要用boolean因为boolean类型在Java和数据库之间的映射容易出问题而且在扩展场景比如需要区分正常/删除/待清理时boolean不够用。乐观锁的场景同样常见。MP提供了Version注解在实体类上标注版本号字段配合乐观锁拦截器MP在执行updateById时会自动把version字段作为一个等值条件加入WHERE同时把version加一。最关键的收益是两个并发请求同时更新同一条记录时只有一个能成功另一个的更新行数为0开发者可以在业务代码里捕获这个结果并做提示。这个机制一定要配合拦截器才能生效。截至3.x版本常见的配置是这样的Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; }注意乐观锁拦截器只管updateById和update(entity, wrapper)这种按主键更新或者明确带wrapper的更新如果你用UpdateWrapper直接set版本号它不会自动帮你做版本管理。4. 分页插件与性能优化参数配置里的门道4.1 分页插件是如何工作以及怎么正确注册MP的分页功能和原生MyBatis的PageHelper思路完全不同。PageHelper通过线程本地变量保存分页参数在下一个查询执行时自动拼接LIMITMP的分页则要求你显式把IPage作为Mapper方法的第一个参数传入。从设计上看MP的方式更安全因为分页和查询绑定在同一个语句上不存在多线程环境下分页参数错乱的隐患。注册分页插件时很多教程只写了一个PaginationInnerInterceptor其实这远远不够。如果你的项目里还同时使用了乐观锁、多租户、防全表更新等插件那么这些拦截器的顺序是有讲究的。MP官方推荐分页拦截器放在倒数第二个紧挨着SQL解析而其他业务拦截器放在更前面。我的理解是分页插件实际上修改的是SQL的结尾部分所以它需要在SQL被业务拦截器处理完之后再去拼LIMIT。在实际项目里我的注册顺序是这样的Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new DataPermissionInterceptor()); // 数据权限如果有 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 乐观锁 interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 防全表更新删除 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 分页 return interceptor; }4.2 自定义SQL如何与MP分页协同项目开发到一定阶段总会遇到分页列表里需要关联查询的场景。比如订单分页列表要带上用户昵称和商品名称这种情况下只用单表MP方法是搞不定的需要手写Join SQL。但在MP里做这件事也不难Mapper接口方法第一个参数传IPageT然后在XML里写普通查询语句MP会自动帮你拼上LIMIT。IPageOrderVO page new Page(current, size); IPageOrderVO result orderMapper.selectOrderDetailPage(page, query);对应的XML写法和平时完全一样select idselectOrderDetailPage resultTypecom.example.vo.OrderVO SELECT o.order_id, o.order_no, u.nick_name, g.goods_name FROM t_order o LEFT JOIN t_user u ON o.user_id u.user_id LEFT JOIN t_goods g ON o.goods_id g.goods_id WHERE o.deleted 0 if testquery.status ! null AND o.status #{query.status} /if /select这里最容易被忽略的是count的优化。MP默认会为每个分页查询生成一条SELECT COUNT(*)的语句但在复杂Join场景下这条count查询往往包含多余的LEFT JOIN导致性能变差。解决办法是在Mapper接口里额外提供一个selectCount方法或者把分页参数里的optimizeCountSql设为false。我实际遇到过一个案例一张订单表关联五张表做分页时数据量只有三百万但列表页加载需要四秒多。通过慢SQL日志定位后发现MP生成的count语句把五个Join全部带上EXPLAIN一看全表扫描了几张表。后来我在XML里单独写了一个精简版count查询过滤掉不需要参与计数的Inner Join查询时间瞬间降到200毫秒。4.3 超大分页中最容易忽视的深翻页问题MP帮你处理了LIMIT拼接但没有帮你解决深翻页的性能问题。当用户翻到第1000页时SQL变成LIMIT 19980, 20数据库仍然要把前19980行扫一遍然后丢弃掉。解决深翻页问题业界常见做法有三种游标分页、子查询分页、延迟关联分页。在MP框架下我推荐在自定义SQL中使用延迟关联SELECT t.* FROM t_order t INNER JOIN ( SELECT id FROM t_order WHERE deleted 0 ORDER BY id DESC LIMIT 19980, 20 ) tmp ON t.id tmp.id ORDER BY t.id DESC;这种写法让内层子查询只查主键即使深度翻页索引命中效率也比全行数据扫描高很多。实际测试中同样的深翻页场景普通写法和延迟关联写法在千万级数据量下性能差距能从五秒以上降到百毫秒级别。这个优化技巧和MP并不冲突你完全可以在XML里手写这条SQL然后照样把IPage参数传进去。5. 实战里最有价值的三个自定义自动填充、防全表更新、主键策略5.1 MetaObjectHandler让每一行数据都带着审计字段新项目建表时我们几乎都会带create_time、update_time、creator、updater之类的审计字段。之前用原生MyBatis时每次insert都要手动往实体里set这些值漏写一个就是没有审计日志。用MP之后可以通过MetaObjectHandler实现自动填充。实现方式很简单写一个类实现MetaObjectHandler接口重写insertFill和updateFill方法Component 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()); } }同时实体类里对应的字段要加TableField注解TableField(fill FieldFill.INSERT) private Date createTime; TableField(fill FieldFill.INSERT_UPDATE) private Date updateTime;使用自动填充有两件事要注意。第一个是只有实体类作为参数传入时自动填充才会生效。如果你用LambdaUpdateWrapper的set方法去更新字段是不会触发MetaObjectHandler的。第二个是strictInsertFill要求在实体类中已经存在该字段而且字段的默认值不能是null时的显式逻辑。如果实体类的createTime已经被手动设置为某个值strict模式不会覆盖它如果希望强制覆盖需要改成setFieldValByName。我观察到很多团队在初期会写大量的设置createTime代码用自动填充之后这些代码全部消失了。这不是简单的减少几行代码的问题而是固定的审计逻辑被收敛到了一个统一入口以后如果审计规则变化比如需要加上操作人默认值只需要改一个类。5.2 BlockAttackInnerInterceptor拦截那些不带where的灾难SQL生产环境里最可怕的SQL事故不是查询慢而是误执行了全表更新或全表删除。比如运营同学手滑写了个不带任何条件的DELETE FROM t_order或者代码里因为某个if分支判断错误拼接出的UPDATE语句把整张表的数据都改掉了。MP提供了一个防全表更新删除插件BlockAttackInnerInterceptor注册之后它会拦截所有不带WHERE条件的UPDATE和DELETE语句直接抛出异常。interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());这个拦截器实际价值非常大。我见过不止一个项目因为没有这个保护出现了全表数据被误清空的线下事故。虽然可以通过数据库层面做权限限制但从代码层面加上这一道防线成本极低收益极高。生产环境强烈建议默认开启。有一点需要注意这个拦截器拦截的是MP生成的SQL对于你自己在XML里手写的SQL它也能拦到吗实测下来只要SQL最终经过MP的拦截器链都会被执行拦截但如果你在系统里绕过了MP的SqlSession比如直接通过原生MyBatis的SqlSessionTemplate执行这个拦截器就拦不了。所以还是要双保险数据库账号权限也要保持最小化。5.3 主键策略雪花ID、自增还是自定义输入MP对主键的支持非常灵活通过TableId的type属性可以指定主键生成策略。多年的实际项目经验告诉我分布式环境下请使用IdType.ASSIGN_ID雪花算法或IdType.INPUT尽量避免依赖AUTO自增。原因有几方面。首先是数据库自增主键在分库分表后容易冲突不多做处理的话多个库的主键会撞号。其次是自增主键容易被爬虫抓取后做遍历曾经有业务就因为这个被刷接口。雪花ID则天然是趋势递增的可以全局唯一利于分布式场景。MP中设置雪花ID的方式TableId(type IdType.ASSIGN_ID) private Long id;对应的数据库字段类型建议使用BIGINT(20)而不是INT。这一点非常重要很多人建表时用了INT结果插入雪花ID直接溢出报错。MP默认的雪花ID是Long型如果数据库是INT很快就会超过2^31 - 1的范围。如果你因为历史原因必须用数据库自增主键MP也支持IdType.AUTO插入后主键会自动回填到实体类的id字段。但这个策略在分布式环境下需要额外评估个人建议新项目直接走ASSIGN_ID。还有一个细节如果使用了ASSIGN_ID在插入一批数据时MP会自动为每个实体生成不同的ID。如果你用saveBatch批量插入一万条数据ID生成不会有问题但这依赖MP内部对主键策略的处理如果你自定义了主键生成类要确保它是线程安全的。6. 折腾半年之后想提醒你的几件事6.1 建表规范从源头决定MP体验MP的体验高度依赖数据库表结构设计。如果表设计规范比如所有主键都叫id所有字段都是驼峰命名对应那么MP几乎零配置就能跑起来。如果表字段命名混乱比如同一张表里既有create_time又有userName或者有的表主键叫uid有的表叫order_id你就得给每个实体类手动加大量TableName和TableId注解使用效率大打折扣。我现在的习惯是新项目建表时直接同步建好MP的约定。主键统一叫id类型为BIGINT审计字段统一叫create_time、update_time、deleted、version。这样实体类基本连注解都不用写。这个约定带来的隐形成本降低是只有经历过混乱项目的人才能体会到的。6.2 saveBatch大批量插入时的性能隐忧MP提供了saveBatch批量插入方法测试数据不多时速度很快。但一旦单批数据量超过一万就会发现插入速度骤降。原因在于MP的批量插入是基于ExecutorType.BATCH的但它没有自动开启MySQL的JDBC批量写入参数。如果你用的是MySQL在JDBC连接字符串里务必加上rewriteBatchedStatementstrue这个参数会让JDBC驱动把多条INSERT语句合并成多VALUES的组合语句批量插入效率能提升3到5倍。我第一次在一个数据初始化任务里导300万条数据时没开这个参数跑了十几分钟还没结束加上之后两分钟跑完了。这个优化不起眼但谁用谁知道。6.3 团队里出现QueryWrapper满天飞怎么办MP用多了很容易出现LambdaQueryWrapper在每个业务方法里满天飞的情况。业务少还好业务一多你会发现同样一个查询条件在三个Service里被拼了三次。一旦需求变更为用户筛选条件里增加一个状态你就要改三个地方。我的解决办法是为常用的复杂查询单独建查询对象Query Object把条件构造器的组装收敛到一个地方。比如public class OrderQuery { private String orderNo; private Integer status; private Date createTimeStart; private Date createTimeEnd; // getter/setter... } public LambdaQueryWrapperOrder buildWrapper() { return Wrappers.OrderlambdaQuery() .eq(StringUtils.isNotBlank(this.orderNo), Order::getOrderNo, this.orderNo) .eq(this.status ! null, Order::getStatus, this.status) .ge(this.createTimeStart ! null, Order::getCreateTime, this.createTimeStart) .le(this.createTimeEnd ! null, Order::getCreateTime, this.createTimeEnd) .orderByDesc(Order::getCreateTime); }这样做有几个好处条件本身可复用单元测试好写业务代码里不再出现又臭又长的条件构造逻辑。代码可读性提升之后别人在review时可以一眼看出查询意图。6.4 升级MP版本时最容易忽略的破坏性变更MP从3.4.x到3.5.x有过几次比较大的API调整。最典型的是把原先的PaginationInterceptor改成了MybatisPlusInterceptor把PerformanceInterceptor改成了ISqlParser相关实现很多老项目在升级后发现分页失效或者慢SQL日志没了就是因为没有跟上这个变化。另外3.5.x版本以后主键默认策略有所调整从ASSIGN_ID变成了INPUT还是别的需要仔细看官方发布说明。这里我的建议是不要频繁追新版本但是当项目准备升级时先看一遍官方文档中的升级指南确认影响点之后再动手。我个人的经验是MP这种框架它的价值在于把繁琐重复的CRUD工作标准化但它并没有降低你对SQL和数据库本身的理解要求。相反正因为MP把简单的东西变简单了复杂的东西在使用时反而更要求你深刻理解底层。分页为什么慢、乐观锁为什么失效、批量插入为什么快不了这些问题最终还是要回到SQL原理、索引原理、连接池原理去解答。所以学MP不能只停留在会用API要带着为什么这么设计的问题去用这样才算真正掌握。如果你也在项目里刚刚引入MP或者正犹豫要不要从原生MyBatis切换过来我的建议是先挑一个非核心模块试点把单表CRUD切过去跑一跑看看排查问题的效率有没有提升。等团队都适应了这套API风格再把影响面扩大。用MP不是目的让数据访问代码稳定、可维护、不成为业务开发的瓶颈才是目的。
阅读完成 · 觉得有帮助?
咨询建站