03-MyBatis-Plus全功能实战:CRUD、条件构造器、分页、排序
MyBatis-Plus全功能实战CRUD、条件构造器、分页、排序黒漂技术佬 · 2026年7月前言前一篇文章我们啃完了 MyBatis 的原生招式Mapper XML、动态 SQL、#{}防注入、ResultMap 嵌套映射。学完这些你写个无人售货柜商品管理的后台已经不在话下。但是——你是不是也感觉到了单表 CRUD 这种事翻来覆去就是那几句 SQLSELECT*FROMt_deviceWHEREid?INSERTINTOt_device...UPDATEt_deviceSET...DELETEFROMt_deviceWHEREid?每张表都写一遍你烦不烦反正我烦了。于是就有了这篇文章的主角MyBatis-Plus简称 MP。MP 不是替代 MyBatis而是在 MyBatis 之上做增强。你之前学的 Mapper XML、动态 SQL 原封不动还能用MP 只是在此基础上帮你把重复劳动省掉。听起来是不是很诱人开搞。一、什么是 MyBatis-PlusMyBatis-Plus 是 MyBatis 的增强工具包内置通用 Mapper、通用 Service提供分页插件、条件构造器、代码生成器等能力目标是只做增强不做改变。简单翻译一下你原来怎么写 MyBatis 还怎么写但 MP 帮你吃掉了一大堆 boilerplate样板代码。举个例子不用 MP 的时候每种单表查询你都要在 XML 里写select有了 MP 之后MapperpublicinterfaceDeviceMapperextendsBaseMapperDevice{// 什么都不用写CRUD 自动有了}继承了BaseMapperT你直接就能调用deviceMapper.selectById(1L);// 查一条deviceMapper.selectList(null);// 查全部deviceMapper.insert(device);// 插入deviceMapper.updateById(device);// 更新deviceMapper.deleteById(1L);// 删除单表操作从此再也不写 XML爽不爽二、快速开始pom.xml 引入dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactIdversion3.5.5/version/dependencyapplication.ymlmybatis-plus:mapper-locations:classpath*:/mapper/**/*.xml# XML 路径如果需要configuration:map-underscore-to-camel-case:true# 驼峰转换log-impl:org.apache.ibatis.logging.stdout.StdOutImpl# SQL 日志global-config:db-config:id-type:auto# 主键自增策略logic-delete-field:is_deleted# 逻辑删除字段logic-delete-value:1logic-not-delete-value:0配置项和原生 MyBatis 高度兼容只是换了个前缀mybatis-plus。三、实体类注解告诉 MP 你的表长什么样DataTableName(t_device)// 指定表名publicclassDevice{TableId(typeIdType.AUTO)// 主键自增privateLongid;TableField(device_sn)// 字段名和属性名不一致时指定privateStringdeviceSn;// 货柜设备编号TableField(device_name)privateStringdeviceName;// 设备名称如 A栋1F饮料柜privateStringlocation;// 安装位置TableField(valuestatus,existtrue)privateIntegerstatus;// 1在线 0离线TableField(fillFieldFill.INSERT)privateLocalDateTimecreateTime;TableField(fillFieldFill.INSERT_UPDATE)privateLocalDateTimeupdateTime;TableLogicprivateIntegerisDeleted;// 逻辑删除标记TableField(existfalse)// 数据库里没有这个字段privateStringstatusText;// 前端展示用在线/离线}逐一说明注解作用典型场景TableName指定实体对应的表名表名和类名不一致时必须加TableId标注主键字段指定生成策略IdType.AUTO自增IdType.ASSIGN_ID雪花算法TableField指定属性对应的列名数据库叫device_snJava 叫deviceSn时用TableField(exist false)标记非数据库字段临时计算结果、前端拼接文本等TableLogic逻辑删除标记delete 操作自动转为 updateTableField(fill ...)配合自动填充使用创建时间、更新时间自动注入TableId(type IdType.ASSIGN_ID)用的是雪花算法Snowflake生成的是一个 64 位的 Long 型 ID全局唯一。在分布式场景比如多台无人售货柜各自生成订单号下非常有用避免 ID 冲突。四、条件构造器没有 XML 的复杂查询这是 MyBatis-Plus 最核心也最强大的功能。如果只靠BaseMapper的默认方法只能做selectById、selectList这种简单操作。真正的业务查询——“查询所有在线的设备”、“按设备名称模糊搜索”、“按安装时间范围筛选”——需要条件构造器。MP 提供了两种条件构造器构造器特点推荐场景QueryWrapper字符串写列名拼错不报错简单查询、快速原型LambdaQueryWrapperLambda 表达式写列名编译期检查正式项目一律用这个4.1 LambdaQueryWrapper 常用 APIAutowiredprivateDeviceMapperdeviceMapper;// 1. 等值查询查询所有在线设备ListDeviceonlineDevicesdeviceMapper.selectList(newLambdaQueryWrapperDevice().eq(Device::getStatus,1));// 2. 模糊查询按设备名称搜索ListDevicedevicesdeviceMapper.selectList(newLambdaQueryWrapperDevice().like(Device::getDeviceName,饮料柜));// 3. 范围查询查找某个时间段内安装的设备ListDevicerecentDevicesdeviceMapper.selectList(newLambdaQueryWrapperDevice().between(Device::getCreateTime,LocalDateTime.of(2026,1,1,0,0),LocalDateTime.now()));// 4. IN 查询查多个指定设备ListDeviceselecteddeviceMapper.selectList(newLambdaQueryWrapperDevice().in(Device::getId,1L,3L,5L,7L));// 5. 组合条件在线 位置在北京 按创建时间倒序ListDeviceresultdeviceMapper.selectList(newLambdaQueryWrapperDevice().eq(Device::getStatus,1).likeRight(Device::getLocation,北京).orderByDesc(Device::getCreateTime).last(LIMIT 10)// 拼原生 SQL 片段);4.2 常用条件方法速查表方法含义SQL 等价eq等于column valuene不等于column valuegt / ge大于 / 大于等于column value/column valuelt / le小于 / 小于等于column value/column valuelike全模糊匹配column LIKE %value%likeLeft左模糊column LIKE %valuelikeRight右模糊column LIKE value%between范围column BETWEEN v1 AND v2in包含column IN (v1, v2, ...)isNull / isNotNull空值判断column IS NULLorderByAsc / orderByDesc排序ORDER BY column ASC/DESC用LambdaQueryWrapper有个巨大的好处你如果写Device::getDeivceName注意我故意写错了IDE 直接标红编译报错。而用QueryWrapper写字符串deivce_name拼错了要到运行时才发现——运维同学半夜又要爬起来。五、分页插件告别手算 SQL Limit业务中分页是刚需。手写分页 SQL 不复杂但每次都拼LIMIT offset, size着实无聊。而且除了数据还要查总条数两条 SQL 来回写。MP 的分页插件一步到位ConfigurationpublicclassMybatisPlusConfig{BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptornewMybatisPlusInterceptor();// 分页插件interceptor.addInnerInterceptor(newPaginationInnerInterceptor(DbType.MYSQL));returninterceptor;}}然后业务代码// 构建分页对象第1页每页10条PageDevicepagenewPage(1,10);// 加上条件LambdaQueryWrapperDevicewrappernewLambdaQueryWrapperDevice().eq(Device::getStatus,1)// 只要在线设备.orderByDesc(Device::getUpdateTime);// 执行分页查询PageDeviceresultdeviceMapper.selectPage(page,wrapper);System.out.println(当前页result.getCurrent());System.out.println(总页数result.getPages());System.out.println(总记录result.getTotal());System.out.println(数据列表result.getRecords());一条selectPage调用内部自动执行两条 SQLSELECT COUNT(*) FROM t_device WHERE status 1→ 查总数SELECT * FROM t_device WHERE status 1 ORDER BY update_time DESC LIMIT 0, 10→ 查分页数据你再也不用自己写两个查询方法了。六、自动填充createTime/updateTime 不用手动设后台管理系统里几乎所有表都有create_time和update_time每次新增或更新都要手动setCreateTime(now)麻烦且容易忘。MP 的自动填充功能专门解决这个问题ComponentpublicclassMyMetaObjectHandlerimplementsMetaObjectHandler{OverridepublicvoidinsertFill(MetaObjectmetaObject){this.strictInsertFill(metaObject,createTime,LocalDateTime.class,LocalDateTime.now());this.strictInsertFill(metaObject,updateTime,LocalDateTime.class,LocalDateTime.now());}OverridepublicvoidupdateFill(MetaObjectmetaObject){this.strictUpdateFill(metaObject,updateTime,LocalDateTime.class,LocalDateTime.now());}}配合实体类上的注解TableField(fillFieldFill.INSERT)privateLocalDateTimecreateTime;TableField(fillFieldFill.INSERT_UPDATE)privateLocalDateTimeupdateTime;之后你 insert/update 实体对象时根本不用管时间字段MP 在 SQL 执行前自动帮你填上。妈妈再也不用担心我看到createTime null的报错了。七、逻辑删除假删除真安全项目里几乎没有人敢直接DELETE FROM t_device WHERE id 1——数据丢了找不回来审计也过不了。常规操作是加一个is_deleted字段删的时候设1查的时候带WHERE is_deleted 0。但每个查询都要记得加这个条件烦不烦MP 帮你做了TableLogicprivateIntegerisDeleted;全局配置mybatis-plus:global-config:db-config:logic-delete-field:is_deletedlogic-delete-value:1# 已删除logic-not-delete-value:0# 未删除效果deviceMapper.deleteById(1L)→ 实际执行UPDATE t_device SET is_deleted 1 WHERE id 1所有select操作自动追加WHERE is_deleted 0需要查已删除数据用deviceMapper.selectList(...)加上.eq(Device::getIsDeleted, 1)即可注意逻辑删除和唯一索引有冲突。比如设备编号device_sn设了唯一键逻辑删除后新建一条相同编号的记录会失败。这时要么不用唯一索引要么把is_deleted也纳入唯一索引。八、乐观锁并发场景的数据安全保障无人售货柜的场景管理员 A 看到某设备信息在编辑管理员 B 也在同一时刻编辑同一台设备。A 点保存B 也点保存——后保存的把先保存的覆盖了这就是典型的丢失更新问题。乐观锁的解决思路在表中加一个version字段每次更新时带上版本号比对版本号匹配才更新同时版本号 1。VersionprivateIntegerversion;需要配置一个拦截器BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptornewMybatisPlusInterceptor();interceptor.addInnerInterceptor(newOptimisticLockerInnerInterceptor());returninterceptor;}实际执行的 SQL 会变成UPDATEt_deviceSETdevice_name新名称,version3WHEREid1ANDversion2如果version已经被别人更新成了 3这条 SQL 影响行数为 0MP 会抛出OptimisticLockException上游捕获后提示数据已被他人修改请刷新重试——完美。九、完整实战无人售货柜设备管理把上面的功能串成一份完整的设备管理代码。Service 层publicinterfaceDeviceServiceextendsIServiceDevice{PageDevicepageSearch(Stringkeyword,Integerstatus,intpageNum,intpageSize);booleanonline(Longid);// 设备上线booleanoffline(Longid);// 设备离线}ServicepublicclassDeviceServiceImplextendsServiceImplDeviceMapper,DeviceimplementsDeviceService{OverridepublicPageDevicepageSearch(Stringkeyword,Integerstatus,intpageNum,intpageSize){LambdaQueryWrapperDevicewrappernewLambdaQueryWrapper();if(StringUtils.hasText(keyword)){wrapper.and(w-w.like(Device::getDeviceName,keyword).or().like(Device::getDeviceSn,keyword));}if(status!null){wrapper.eq(Device::getStatus,status);}wrapper.orderByDesc(Device::getUpdateTime);returnthis.page(newPage(pageNum,pageSize),wrapper);}Overridepublicbooleanonline(Longid){DevicedevicenewDevice();device.setId(id);device.setStatus(1);returnthis.updateById(device);}Overridepublicbooleanoffline(Longid){DevicedevicenewDevice();device.setId(id);device.setStatus(0);returnthis.updateById(device);}}Controller 层RestControllerRequestMapping(/api/device)publicclassDeviceController{AutowiredprivateDeviceServicedeviceService;GetMapping(/page)publicResultPageDevicepage(RequestParam(requiredfalse)Stringkeyword,RequestParam(requiredfalse)Integerstatus,RequestParam(defaultValue1)intpageNum,RequestParam(defaultValue10)intpageSize){returnResult.success(deviceService.pageSearch(keyword,status,pageNum,pageSize));}PostMapping(/{id}/online)publicResultVoidonline(PathVariableLongid){returndeviceService.online(id)?Result.success():Result.fail(操作失败);}PostMapping(/{id}/offline)publicResultVoidoffline(PathVariableLongid){returndeviceService.offline(id)?Result.success():Result.fail(操作失败);}}ServiceImplT是 MP 提供的通用 Service 实现继承了它就有了全套save、remove、update、page、list等方法。你只需要在它上面补几行自定义逻辑就行。总结MyBatis-Plus 的正确使用姿势总结Mapper 继承BaseMapperT单表 CRUD 自由了不用写一行 SQL条件查询用LambdaQueryWrapper编译期保证字段名正确杜绝 runtime 翻车分页用PaginationInnerInterceptor一行selectPage搞定连总数都帮你算了自动填充用MetaObjectHandler时间字段从此无人值守逻辑删除 乐观锁数据安全的两大护法简单注解就搞定MP 不是替代 MyBatis而是让它更好用。复杂联表查询、存储过程这些XML 照样上但 80% 的单表操作场景MP 让你把开发效率拉满。