资讯动态

MybatisPlus从入门到实战:条件构造器、分页与乐观锁详解

发布时间:2026/10/13 18:26:59 来源:尧图企业网站定制
1. 实训任务概览与核心思路1.1 第六天实训到底要解决什么问题先说说这次实训的背景。到了Day06前面的课程基本已经把Spring Boot的基础用法、Controller层开发、Service层事务处理这些常规套路过了一遍。这个阶段最常见的痛点是单表CRUD的代码重复度实在太高了几乎每个实体都要写一套Mapper接口、XML文件、基础增删改查的SQL而且大部分SQL还只是换个表名而已。第六天的实训目标很直接——引入MybatisPlus用一套通用机制把这些机械劳动干掉把精力腾出来处理真正有业务逻辑的部分。实训对应的模拟场景是一个典型的电商后台系统雏形比如商品管理、订单管理这类业务模块。在Day05的时候这些模块的DAO层还在用传统MyBatis的方式Mapper XML里堆了几十个标签写完一个模块再复制到下一个模块改表名、改字段名枯燥不说还容易漏改。Day06的任务就是把这些模块逐一改造为MybatisPlus实现并且新增两个需要用条件查询、分页查询的新模块作为练习。1.2 为什么选MybatisPlus而不是别的方案其实在实训选型时JPA、MyBatis通用Mapper、MybatisPlus这几个方案都对比过。JPA的自动建表能力确实方便但对于Java开发者群体来说MyBatis的学习曲线更平滑而且国内大量业务项目就是MyBatis栈实训选型要贴近实际生产环境。MyBatis通用Mapper和MybatisPlus相比后者在条件构造器、乐观锁插件、逻辑删除、代码生成器这些生态组件上更完整社区活跃度也更高出了问题能找到的参考案例更多。从团队协作的角度来说MybatisPlus没有侵入性它不会强制你重构所有代码而是“想用哪块用哪块”。你可以在老项目里只引入分页插件而不必把所有Mapper都换成BaseMapper也可以在新模块里全面使用LambdaQueryWrapper把条件拼接从XML字符串切换到Java代码。这种渐进式改造的特性对实训课这种“既有历史代码要兼容又要快速出新功能”的项目场景非常友好。一句话总结我的选型逻辑上手门槛低改造风险小写出来的代码还好维护。2. 环境准备与配置要点2.1 引入依赖时的版本坑Day06实训的第一步是在pom.xml里引入MybatisPlus的依赖。这个步骤看起来简单却是第一个容易埋雷的地方核心是版本兼容问题。我当时的配置是这样的dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency这里我特意选了3.5.3.1这个版本而不是最新的3.5.4或更早的3.4.x原因有两个。第一3.5.3.x系列对Spring Boot 2.6.x兼容性测试做得比较充分实训环境用的就是Spring Boot 2.6.13第二3.5.x之后的分页插件用法和旧版有细微差别旧版的PaginationInterceptor已经在3.5.0里移除了网上很多教程还在用旧写法盲目照抄会出现启动报错。如果你用的是Spring Boot 3.x那就需要选mybatis-plus-spring-boot3-starter这个专门的依赖不要用普通的boot-starter否则会直接启动失败。另外有一点容易忽略MybatisPlus的starter自带MyBatis依赖但如果你原来pom里手动引入了mybatis-spring-boot-starter就会产生依赖冲突启动时经常出现莫名其妙的mapper扫描异常。实训中我的处理方式是先移除旧的mybatis和mybatis-spring依赖再引入mybatis-plus-boot-starter这样最干净。2.2 核心配置文件的参数选择依赖引入后需要在application.yml里做几个关键配置。这是实训第六天项目的一个核心环节配置不对后面所有功能都会“看起来能用但实际有问题”。下面是我在实际项目中总结出来的标准配置方案mybatis-plus: # 实体类扫描路径 type-aliases-package: com.example.jingtao.entity # 驼峰命名自动转换开关默认就是true但建议显式写出来 configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: # 主键策略ID_WORKER是雪花算法适合分布式场景 id-type: assign_id # 逻辑删除字段名 logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 # 乐观锁字段名 version-field: version这个配置里有几个细节值得展开说说。map-underscore-to-camel-case这行配置虽然MybatisPlus默认是true但如果你同时还在用原生MyBatis的XML和ResultMap务必确认ResultMap里没有手动映射了带下划线的字段不然会出现重复映射。log-impl这里设置成StdOutImpl是为了实训调试方便可以直接在控制台看到SQL执行的具体语句和参数但生产环境一定要去掉这个配置否则日志量会大到让你怀疑人生。关于主键策略我用的是assign_id对应MybatisPlus的雪花算法。常见的课程作业里很多人图省事直接设成auto让数据库自增但在这个电商模拟场景里订单号和商品ID在分库分表场景下自增ID会有冲突风险雪花算法生成的Long型ID更贴近真实业务。当然如果你用的是MySQL并明确没有分库分表需求auto确实更简单这个看实训要求没有绝对的对错。3. Mapper层的通用化改造3.1 继承BaseMapper带来的变化Day06的核心任务之一是把原来手写的Mapper接口改造为继承MybatisPlus的BaseMapper。改造前的代码长这样public interface ProductMapper { int insertProduct(Product product); int deleteProduct(Long id); int updateProduct(Product product); Product selectProductById(Long id); ListProduct selectProductList(); }每一行方法都要配一个XML标签虽然SQL都简单但写多了确实烦。改造后整个接口可以精简成一行public interface ProductMapper extends BaseMapperProduct { // 只需在此添加自定义复杂SQL方法 }这里我想特别说明一下继承BaseMapper并不意味着从此用不上XML了。项目里总有些复杂查询比如多表联查、动态条件特别多的统计报表这种场景还是要老老实实写XML或者用注解SQL。实训第六天的教训是不要为了“炫技”把所有查询都强行用BaseMapper自带方法实现遇到多表关联查询时条件构造器写起来比XML复杂得多反而得不偿失。合理的方式是“通用CRUD用BaseMapper复杂查询继续写XML”两者混用完全不冲突。3.2 实体类注解的正确姿势Mapper接口精简后实体类就要承担更多职责。MybatisPlus通过注解来映射表名、主键策略、字段填充规则等元信息。下面是我们实训项目中商品实体经过改造后的核心片段Data TableName(tb_product) public class Product { TableId(value id, type IdType.ASSIGN_ID) private Long id; TableField(product_name) private String productName; TableField(price) private BigDecimal price; TableField(value create_time, fill FieldFill.INSERT) private LocalDateTime createTime; TableField(value update_time, fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; TableLogic TableField(deleted) private Integer deleted; Version TableField(version) private Integer version; }每个注解都不是随便加的它们分别解决不同问题。TableName用于指定数据库表名Java类名和表名不一致时尤其重要比如这里的Product对应tb_product。TableField上的fill属性是自动填充功能配合MetaObjectHandler处理器可以在插入或更新时自动写入时间字段比代码里手动set创建时间要干净得多这个后面实操环节我会详细展开。TableLogic注解是逻辑删除的开关。加上这个注解后调用BaseMapper的deleteById方法时MybatisPlus不会执行DELETE语句而是自动转化为UPDATE ... SET deleted 1。这个设计在电商项目里特别实用尤其是订单、用户这类敏感数据物理删除会让后续对账、审计无从下手。需要注意的一点是逻辑删除字段在查询时会自动追加条件WHERE deleted 0这既是优点也是“坑”。如果某天你确实需要查询包含已删除数据的全量记录需要自己写SQL绕过这个自动条件。Version是乐观锁的核心配合分页插件一起使用可以在更新时自动拼接version ?条件并做CAS校验。关于乐观锁的机制细节和实际踩坑在下一个部分展开讲。4. 条件构造器把SQL搬进Java代码4.1 QueryWrapper与LambdaQueryWrapper怎么选条件构造器是MybatisPlus最让人“用了就回不去”的功能。它用Java方法链代替了XML里的动态SQL拼接让条件判断、字符串拼接、参数传递都在类型安全的前提下完成。实训第六天主要用了两种WrapperQueryWrapper和LambdaQueryWrapper。前者直接传字段名后者用方法引用。举个例子按商品名称模糊查询、价格区间过滤、按销量倒序排列三者的写法对比// 方式一QueryWrapper字段名硬编码 QueryWrapperProduct wrapper new QueryWrapper(); wrapper.like(product_name, queryParam.getKeyword()) .between(price, queryParam.getMinPrice(), queryParam.getMaxPrice()) .orderByDesc(sales_count); // 方式二LambdaQueryWrapper方法引用编译期检查 LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.like(Product::getProductName, queryParam.getKeyword()) .between(Product::getPrice, queryParam.getMinPrice(), queryParam.getMaxPrice()) .orderByDesc(Product::getSalesCount);我的习惯是新项目一律用LambdaQueryWrapper因为字段名一旦重构硬编码字符串不会报编译错误只会留到运行时才暴露问题。这种“编译期错误”比“运行时错误”好处理一个数量级。但要注意LambdaQueryWrapper也有个性能上的小知识每次方法引用都会生成一个SerializedLambda如果你在一个超大循环里反复创建Wrapper会有一点性能损耗不过对于实训项目的并发量来说这个损耗完全可以忽略不必为此过度设计。4.2 复杂条件的链式写法实战条件构造器最爽的地方在于“按需拼接”。比如商品列表页的筛选条件是可选的用户不传某个参数时SQL里就不应该出现这个条件。传统写法用XML的if标签或者用StringBuilder手动拼SQL过程既繁琐又容易出SQL注入的漏洞。用Wrapper的话逻辑就变成了LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.isNotBlank(param.getCategoryId()), Product::getCategoryId, param.getCategoryId()) .like(StringUtils.isNotBlank(param.getProductName()), Product::getProductName, param.getProductName()) .ge(param.getMinPrice() ! null, Product::getPrice, param.getMinPrice()) .le(param.getMaxPrice() ! null, Product::getPrice, param.getMaxPrice()) .apply(DATE(create_time) DATE({0}), param.getCreateDate());第一个参数是布尔类型condition为true时后续条件才会拼接进SQL。这个设计的好处是代码的可读性比一堆if语句包裹setSqlSegment的方法调用要直观得多。而且因为用的是参数的占位符形式不会有人手动拼接字符串导致SQL注入的问题。这里我想提醒一个容易忽略的点apply方法虽然可以传任意的原生SQL片段但尽量只在万不得已时使用比如需要调用数据库特定函数、日期函数这类Wrapper不支持的能力时才用。参数一定要用{0}这种占位符传进去不要直接进行字符串拼接。我在实训过程中见过有同学图省事写成.apply(DATE(create_time) DATE( dateStr ))这就是典型的SQL注入漏洞哪怕在实训项目里代码规范也必须要做到位。4.3 条件构造器使用中的边界情况Wrapper用多了你会碰到几个边界情况这是实训容易忽略的细节。第一个是and和or的优先级问题。and优先于or是数据库SQL的天然规则但写Wrapper时很多人会误以为链式调用的顺序就是执行顺序。举个例子要查询“分类A或分类B且价格大于100”的商品如果这样写wrapper.eq(Product::getCategoryId, A) .or() .eq(Product::getCategoryId, B) .gt(Product::getPrice, 100);实际生成的SQL是你绝对不想要的WHERE category_id A OR (category_id B AND price 100)。正确的写法是要有用and(消费者)方法实现嵌套分组wrapper.and(w - w.eq(Product::getCategoryId, A).or().eq(Product::getCategoryId, B)) .gt(Product::getPrice, 100);这样生成的SQL是WHERE (category_id A OR category_id B) AND price 100逻辑才正确。第二个是in条件超出数据库限制。如果传给in的参数集合元素特别多比如几千个ID一些数据库会报错或者性能骤降。在实际项目里大批量ID的in查询一般会改成分批处理每批500个。MybatisPlus没有对in的数量做内置限制这个逻辑得自己在Service层控制。第三个是last方法。它的作用是直接拼接一段SQL到语句末尾比如.last(LIMIT 5)。这个方法是把双刃剑它的优先级最高会无视你之前设置的所有其他条件而且一旦使用不当比如拼接了; DROP TABLE这类内容后果会很严重。所以在实训项目中我规定了一个铁律last方法的使用必须经过评审而且只能拼接静态常量或白名单参数绝不接受外部输入。5. 内置插件分页、乐观锁与逻辑删除5.1 分页插件的配置与使用细节分页是后台管理系统里的高频需求。MybatisPlus的分页插件配置非常简单但有几个细节体验不好容易踩坑这里一起说明。分页插件需要在配置类里注册一个BeanConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 分页插件放在第一位 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 乐观锁插件放在其后 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }这里有个容易忽略的点PaginationInnerInterceptor构造器里最好显式传入DbType.MYSQL而不是用无参构造器让MybatisPlus自动判断。原因是在某些连接池配置下数据库类型探测会走一条比较复杂的逻辑链路如果探测失败分页SQL的方言拼接就会出错典型症状是SQL最后拼接了LIMIT但没拼上OFFSET或者LIMIT和OFFSET的顺序不对。分页查询的Service层标准写法是这样的public PageProduct pageProducts(ProductPageQuery query) { PageProduct page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.isNotBlank(query.getCategoryId()), Product::getCategoryId, query.getCategoryId()) .orderByDesc(Product::getCreateTime); return productMapper.selectPage(page, wrapper); }这里有一个从旧版MybatisPlus过渡来的旧写法需要特别提醒3.4.0之前的分页用法是直接配置PaginationInterceptor而3.5.x之后统一用MybatisPlusInterceptor容器来管理。网上很多历史帖子用的还是旧写法如果你版本比较新还按旧写法配置代码能启动但分页不生效执行SELECT时不会自动拼LIMIT排查起来很让人头疼。分页结果的获取也有讲究。很多人用page.getPages()来算总页数用page.getTotal()获取总数这是没问题的。但要注意的是selectPage方法会自动执行一条COUNT查询来统计总数。如果你的列表查询条件特别复杂且联表很多COUNT查询的性能可能会很慢。这时候可以使用page.setSearchCount(false)来禁用自动COUNT然后手动构造一条更轻量的COUNT语句。这是实训课程里没有讲到的细节但在实际生产里经常会遇到属于加分项。5.2 乐观锁的机制与实战踩坑先解释一下为什么需要乐观锁。电商后台里运营人员A和B同时打开同一个商品的编辑页面A把价格从100改成120B把库存从50改成30。如果没有任何并发控制A的提交可能会覆盖B的修改或者反过来。悲观锁适合并发冲突多的场景但性能开销大而乐观锁利用版本号机制在读多写少的场景下有天然优势。MybatisPlus的乐观锁实现原理很简单更新时自动在SQL的WHERE子句里带上version 当前版本号同时执行SET version version 1。如果更新影响行数为0说明版本号不匹配本次更新失败。这个过程完全由插件自动完成不需要在Mapper里写任何特殊代码。实训第六天我专门设计了几个并发场景来验证这个机制。先说结论95%的情况都能正确拦截并发冲突但有一个细节坑了很多同学——乐观锁字段没有在实体类上加Version注解或者加了这个注解但在前端Form表单里没有把version值跟数据一起提交。当更新时version是nullSQL就会变成WHERE version null可以想象这种情况下条件永远不成立更新影响行数永远是0导致所有更新操作都会失败。官方文档建议在实体类上加上Version注解后每次更新前从数据库重新查询一次并带上新的version这是标准做法。但在实际开发中更常见的做法是从前端提交的JSON数据里带上原versionService层校验非空后再执行updateById。这样既能减少一次数据库查询又不影响乐观锁效果属于一个实用优化技巧。还有一个小问题需要注意updateById这个方法执行时MybatisPlus默认会把实体的所有非null字段都crafted进SET子句。如果你只想更新部分字段比如只改价格但实体的stock字段不小心被赋值了也会被一并更新。这就是我在实训中经常强调的“Dirty Checking”风险。解决办法是使用UpdateWrapper来精确指定要更新的字段或者使用同一实体对象只给需要更新的字段赋值其他字段保持null。Transactional(rollbackFor Exception.class) public boolean updateProductPrice(Long id, BigDecimal newPrice, Integer version) { Product product new Product(); product.setId(id); product.setPrice(newPrice); product.setVersion(version); // 携带版本号 int rows productMapper.updateById(product); // rows为0说明版本冲突 return rows 0; }这里的version是从前端传来的原版本号更新时插件自动在WHERE里加上version 原版本号并set为version 1。如果版本号不对update返回0Service层可以根据返回值给前端提示“数据已被其他用户修改请刷新后重试”。5.3 逻辑删除的隐藏联系逻辑删除和查询的联动说过了这里再展开一个容易忽略的点MybatisPlus的逻辑删除默认是全局单字段名deleted你可以在配置里修改logic-delete-field来指定。如果想让不同的表用不同的逻辑删除字段名比如有的表用is_deleted有的用del_flag可以通过在每个实体类上用TableLogic注解来单独指定这样就覆盖了全局配置。在实际使用中逻辑删除最大的坑在于“唯一索引冲突”。比如用户在表里注册了一个手机号然后执行删除逻辑只是把deleted置为1但物理记录还在表里。此时如果同一个手机号再次注册插入操作会因为唯一索引而失败因为老记录虽然逻辑删除了但依然占着索引位置。解决办法有两种一是把唯一索引改成包含了deleted字段的联合索引比如UNIQUE KEY uk_phone_deleted(phone, deleted)二是注册前先物理删除或调整deleted字段的值。这个坑在实训项目里只会在删除后再新增时暴露如果没有提前做好预案排查起来很费时间。6. 核心实操从ServiceImpl到分页接口的落地6.1 Service层的封装套路Day06实训不只是让你把Mapper换成BaseMapperService层也有对应的简化套路。MybatisPlus提供了IService接口和ServiceImpl实现类它们把单表的增删改查、批量操作、分页等方法都封装好了Service类只要继承ServiceImpl再实现自定义接口即可。以我们的ProductService为例改造前后的对比挺明显的。改造前每个Service实现类都要注入对应Mapper然后手动写insert、update、delete、select的逐个调用。改造后Service public class ProductServiceImpl extends ServiceImplProductMapper, Product implements ProductService { Override public PageProduct pageProducts(ProductPageQuery query) { LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(query.getKeyword()), Product::getProductName, query.getKeyword()) .eq(query.getCategoryId() ! null, Product::getCategoryId, query.getCategoryId()) .orderByDesc(Product::getId); return baseMapper.selectPage(new Page(query.getPageNum(), query.getPageSize()), wrapper); } }ServiceImpl还提供了saveBatch、updateBatchById、listByIds这些批量方法它们内部对每批数据默认使用批量SQL注入的方式可以减少JDBC网络往返次数。使用的时候有两点要提醒一是saveBatch默认批处理大小是1000如果你的数据量特别大可以重载方法传batchSize二是批量更新方法在部分数据库方言下并不会真正生成批量UPDATE语句而是逐条执行这时候性能优化效果有限最好先做压测验证。6.2 生成接口完整流程演示为了便于理解我要完整走一遍Day06实训项目中一个典型接口的实现过程商品条件分页查询。第一步定义分页查询参数类。这里有个命名上的小技巧不要用Product本身来做接收参数尤其是带了id、createTime等字段时接收页面参数会污染实体语义。单独建一个ProductPageQueryData public class ProductPageQuery { private Integer pageNum 1; private Integer pageSize 10; private String keyword; private String categoryId; private BigDecimal minPrice; private BigDecimal maxPrice; private String sortField; private String sortOrder; }第二步Controller层直接调Service参数简单校验后可放置于Service内RestController RequestMapping(/api/product) public class ProductController { Resource private ProductService productService; GetMapping(/page) public ResultPageProduct page(ProductPageQuery query) { return Result.success(productService.pageProducts(query)); } }第三步Service实现里注意对sortField和sortOrder做白名单校验处理。排序字段不能直接拼接任意字符串进wrapper的orderBy方法否则就有注入风险这在前面的last方法里也提到过。安全做法是定义一个允许排序的字段白名单映射private static final MapString, SFunctionProduct, ? SORT_FIELD_MAP Map.of( price, Product::getPrice, salesCount, Product::getSalesCount, createTime, Product::getCreateTime, id, Product::getId ); if (StringUtils.isNotBlank(query.getSortField())) { SFunctionProduct, ? sortField SORT_FIELD_MAP.get(query.getSortField()); boolean asc asc.equalsIgnoreCase(query.getSortOrder()); if (sortField ! null) { wrapper.orderBy(true, asc, sortField); } }6.3 自动填充字段的处理器实现前面实体类上标注了fill FieldFill.INSERT和fill FieldFill.INSERT_UPDATE但光注解还不够需要提供一个MetaObjectHandler的Spring BeanMybatisPlus才能在插入和更新时自动填充这些字段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); this.strictInsertFill(metaObject, version, Integer.class, 1); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }这里有两个容易踩坑的细节。第一是strictInsertFill这个“strict”的含义如果实体类对应字段已经有值就不会覆盖如果实体类是null才会填充。这个特性很重要因为有时候业务需要传入自定义的createTime比如数据迁移、补录历史数据首次插入时不应被强制覆盖。第二个细节是MetaObjectHandler在填充时拿不到对应表的元信息它是通过实体类的TableField(fill FieldFill.INSERT)注解来感知哪些字段需要填充的。所以如果你实体类里没有对应注解MetaObjectHandler即使写了填充字段也不会生效。这两个配置“缺一不可”少任何一个自动填充都不会工作。7. 排查实录实训中高频报错速查7.1 常见问题对照表实训第六天过程中大家遇到的问题高度集中在几类几乎每个人都会至少碰到其中一个。这里整理成一张速查表方便你直接对照排查。现象根本原因解决方案启动报错“Invalid bound statement (not found)”Mapper接口没有扫描到或XML的namespace不对检查MapperScan路径是否覆盖到Mapper接口包检查XML namespace是否与接口全限定名一致分页不生效查询返回全量数据分页插件没有注册或者注册方式用了旧版PaginationInterceptor使用MybatisPlusInterceptor PaginationInnerInterceptor配置逻辑删除后相同记录插入失败数据库唯一索引与逻辑删除字段冲突将唯一索引改为包含deleted的联合索引或在插入前处理旧数据乐观锁更新总是返回0实体类缺少Version注解或version字段传值为null给版本字段加Version注解确保更新时传入非null版本号自动填充时间没有生效实体类缺少fill属性或MetaObjectHandler未注册两者需同时配置缺一不可LambdaQueryWrapper编译报错实体类的属性名和方法引用写错检查是否引用了不存在的getterIDEA对LambdaQueryWrapper有良好提示优先用IDE开发查询结果中逻辑删除字段被设置成nullTableField逻辑删除字段和fill填充顺序问题在insertFill中给deleted字段设置初始值并加上TableLogic后确认查询自动过滤批量插入数据量大时性能极差默认batchSize过大导致SQL语句超长调整saveBatch的batchSize参数或减少每批数据量7.2 三个魔鬼细节除了上面表格里那些一眼能看出的问题还有几个隐藏得比较深的“魔鬼细节”是实训现场花了很长时间才定位的写在这里帮大家提前避开。第一个是TableField(exist false)的使用。如果你的实体类里有数据库表中不存在的字段比如一个“确认密码”字段或者一个临时计算用的字段必须加上exist false注解否则MybatisPlus在生成SQL时会把该字段当成真实表字段然后报“字段不存在”的错误。这是新手极容易忽略的一步。第二个是Mapper XML和MybatisPlus的实体映射共存时的顺序。如果在XML里定义了一个selectProductPage查询但同时又在实体类上加了TableField那么XML里的resultType如果写的是实体类所有映射规则都会走MyBatis的原生映射逻辑不会自动走MybatisPlus的驼峰转换之外的规则。换句话说XML里的SQL要自己处理好字段别名和实体属性的映射关系驼峰转换可以帮一部分忙但不要100%依赖它。第三个是updateById方法的“选择性更新”容易被误解。MybatisPlus的官方文档说updateById会忽略null字段默认只更新非null字段。但注意如果某个字段被设置为空字符串“或者0它不算null会被照常更新。所以如果你不想把空串或0更新进数据库需要在Service层先做空值判断和过滤。我在实训中对这个问题提供了一个通用解法创建一个专门用于做部分字段更新的UpdateDTO并用UpdateWrapper的set方法精确控制更新范围。7.3 排查思路分享遇到报错时我的排查流程是这样的先看控制台打印的SQL语句看看MybatisPlus最终生成的SQL和参数是什么样的。这一步能解决80%的问题因为大部分问题本质上是“SQL不是你想要的”。第二步是检查实体类注解把TableName、TableField、TableLogic、Version这四类注解逐一核对。实体类注解是配置和代码之间的桥梁90%的“看起来没问题但运行不对”都出在这里。第三步是检查配置类。分页插件、乐观锁插件都有没有注册配置类有没有被Spring容器扫描到。如果启动时MybatisPlusConfig这个类都没加载那所有插件都不会生效。第四步才是上网搜索搜索时注意看报错信息和MybatisPlus版本号很多旧帖子的解决方案在新版本里已经不适用了需要结合版本甄别。8. 实训后的自我检验与进阶方向8.1 自测清单实训第六天结束后如果你不确定自己是否真正掌握了MybatisPlus的核心用法可以对照下面这份自测清单逐一检查。能不看文档完成80%以上说明这次实训已经达标了。能否在Spring Boot项目里正确引入MybatisPlus并且完成基础的单表CRUD能否熟练使用LambdaQueryWrapper写出包含模糊查询、范围查询、排序和逻辑分组条件的复杂查询能否配置并解释分页插件的工作原理能否说出分页插件自动生成的COUNT语句和分页SQL的结构能否解释逻辑删除的底层机制以及它对查询、唯一索引、数据恢复的影响能否解释乐观锁的原理并动手实现一个带版本控制的更新接口能否编写MetaObjectHandler实现自动字段填充能否在同一个项目中合理混用MybatisPlus的BaseMapper和自定义XML的Mapper方法8.2 往深了走的方向实训学完如果你觉得还想往更深的方向拓展我这里有几个方向值得探索。第一个是MybatisPlus的多租户插件它的原理是启动时在SQL末尾自动拼接租户条件实现一套数据库多租户隔离。电商后台系统里商家之间数据隔离很有可能是刚需如果你要做一个真正商业化的东西这个知识点逃不掉。第二个是MybatisPlus的SQL注入器它允许你自定义一些全局通用的SQL方法。比如我们实训里有一个“批量根据业务编码查询”的需求用BaseMapper自带方法会发现每个表都要重复写SQL注入器就是干这个事的把通用方法注册进去所有Mapper都能用。这个属于进阶玩法但面试聊起来很加分。第三个是MybatisPlus与MyBatis原生拦截器的集成。当你理解了MybatisPlus所有插件都是基于MyBatis的Interceptor机制实现的你就可以自己编写拦截器做一些统一的数据权限控制、SQL链路追踪等事情。这个机制才是MybatisPlus后续所有扩展能力的根基。8.3 最后分享一点个人体会实训第六天这个节点很有意思它几乎是整个MyBatis学习路线里的一个分水岭。前五天的CRUD练习虽然基础扎实但重复性太高人的精力被磨掉了一大半Day06把通用操作全部交给MybatisPlus之后后面几天的实训项目开发速度明显变快能把更多时间花在业务查询逻辑和接口设计上。我个人觉得MybatisPlus这种“约定优于配置”的框架学起来核心要抓两件事一是它的注解体系理解了每个注解作用在什么生命周期你就理解了它怎么帮你生成SQL二是它的插件机制理解了插件如何与MyBatis原生Executor协作你就能举一反三地组合运用分页、乐观锁、多租户这些能力。这两条主线把握住了以后无论MybatisPlus迭代到哪个版本你都能快速适应新变化。另外一个小建议是实训中遇到的问题一定要自己动手敲一遍解决方案不要光看文档或者光看同学的代码。像逻辑删除和唯一索引冲突那个问题课堂上讲一百遍不如自己删一次再插一次来得印象深刻。MybatisPlus是个工具型的框架只有亲手踩过坑、亲手填上坑才能真正把它变成自己的熟练技能。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑