MyBatis Plus:快速上手指南
MyBatis Plus快速上手指南上一篇我们讲了 MyBatis 的架构和 Mapper 代理机制知道了 Mapper 接口通过动态代理把方法调用转成 SQL 执行。但在实际开发中大部分 Mapper 方法做的事情都很基础按 ID 查、按条件查、插入一条、更新一条、删一条。这些 SQL 写法几乎一模一样每个 Mapper 都要重复一遍。如果你写过几个 Mapper很快就会发现大部分代码其实都在重复增删改查、按 ID 查询、分页、条件查询……每个 Mapper 都要重新写一遍。MyBatis Plus 干的就是这件事——把这些重复劳动封装掉。目录MyBatis Plus 是什么快速接入 Spring BootBaseMapper内置的 CRUD 方法条件构造器Wrapper 怎么用Service 层封装自动填充时间字段不用手动 set分页查询逻辑删除代码生成器MP 和 MyBatis 的关系小结MyBatis Plus 是什么MyBatis Plus简称 MP是 MyBatis 的增强工具只做增强不做改变。引入 MP 后MyBatis 的所有功能照常使用XML 映射文件、动态 SQL、自定义方法都不受影响。日常开发最常用的也就五件事CRUD、条件构造、分页、自动填充和逻辑删除后面基本都会用到。快速接入 Spring BootSpring Boot 项目引入依赖dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactIdversion3.5.5/version/dependency启动类加上 MapperScanSpringBootApplicationMapperScan(com.example.mapper)publicclassApplication{publicstaticvoidmain(String[]args){SpringApplication.run(Application.class,args);}}定义实体类TableName(user)// 指定表名publicclassUser{TableId(typeIdType.AUTO)// 主键自增privateLongid;privateStringname;privateIntegerage;privateStringemail;TableField(fillFieldFill.INSERT)// 插入时自动填充privateLocalDateTimecreateTime;TableField(fillFieldFill.INSERT_UPDATE)// 插入和更新时自动填充privateLocalDateTimeupdateTime;// getter / setter 省略}定义 MapperpublicinterfaceUserMapperextendsBaseMapperUser{// 不需要写任何方法BaseMapper 已经提供了通用 CRUD}到这里就能用了。BaseMapperUser提供了 17 个内置方法直接调用就行。BaseMapper内置的 CRUD 方法BaseMapper是 MP 提供的核心接口里面定义了通用的 CRUD 方法。你的 Mapper 只要继承它就自动拥有这些能力。日常开发真正经常用的其实就下面这些方法说明示例insert插入一条记录mapper.insert(user)deleteById按主键删除mapper.deleteById(1L)deleteBatchIds批量删除mapper.deleteBatchIds(Arrays.asList(1L, 2L))delete条件删除mapper.delete(wrapper)updateById按主键更新null 字段不更新mapper.updateById(user)update条件更新mapper.update(user, wrapper)selectById按主键查询mapper.selectById(1L)selectBatchIds批量查询mapper.selectBatchIds(Arrays.asList(1L, 2L))selectOne查询一条mapper.selectOne(wrapper)selectList条件查询列表mapper.selectList(wrapper)selectCount条件计数mapper.selectCount(wrapper)selectPage分页查询mapper.selectPage(page, wrapper)光看方法名没什么感觉直接看几个最常用的。插入UserusernewUser();user.setName(张三);user.setAge(25);user.setEmail(zhangsanexample.com);// 返回值是受影响的行数introwsuserMapper.insert(user);// 插入后MP 会自动把生成的主键回填到对象里System.out.println(user.getId());// 自增主键的值MP 默认会把null字段也拼进 SQL 里。如果你想跳过null字段比如更新时只更新非空字段MP 的updateById方法已经做了这个处理为 null 的字段不会出现在 SET 子句里。查询// 按 ID 查UseruseruserMapper.selectById(1L);// 批量查ListUserusersuserMapper.selectBatchIds(Arrays.asList(1L,2L,3L));// 查全部ListUseralluserMapper.selectList(null);更新UserusernewUser();user.setId(1L);user.setName(李四);// 只更新 name// age 为 null不会出现在 SET 子句里introwsuserMapper.updateById(user);// 实际执行UPDATE user SET name李四 WHERE id1删除// 按 ID 删userMapper.deleteById(1L);// 批量删userMapper.deleteBatchIds(Arrays.asList(1L,2L,3L));这些方法覆盖了大部分单表操作场景。但实际业务中查询条件往往不只是等于还有模糊查询、范围查询、排序、指定返回字段等需求。这就需要条件构造器了。条件构造器Wrapper 怎么用MP 的条件构造器有两种QueryWrapper和LambdaQueryWrapper。它们的作用是用 Java 代码拼出 WHERE 条件。QueryWrapperQueryWrapperUserwrappernewQueryWrapper();wrapper.eq(age,25)// age 25.like(name,张)// name LIKE %张%.between(age,20,30)// age BETWEEN 20 AND 30.orderByDesc(create_time)// ORDER BY create_time DESC.select(id,name,age);// 只返回指定字段ListUserusersuserMapper.selectList(wrapper);生成的 SQLSELECTid,name,ageFROMuserWHEREage25ANDnameLIKE%张%ANDageBETWEEN20AND30ORDERBYcreate_timeDESCQueryWrapper的问题是字段名用字符串硬编码拼错了编译不报错运行时才发现。LambdaQueryWrapper解决了这个问题。LambdaQueryWrapperLambdaQueryWrapperUserwrappernewLambdaQueryWrapper();wrapper.eq(User::getAge,25).like(User::getName,张).between(User::getAge,20,30).orderByDesc(User::getCreateTime).select(User::getId,User::getName,User::getAge);ListUserusersuserMapper.selectList(wrapper);效果一样但字段通过方法引用User::getName指定重构时改了字段名会自动跟着变编译期就能发现问题。我现在基本不会再写 QueryWrapper 了全部换成 LambdaQueryWrapper。虽然代码稍微长一点但重构的时候不用担心字段名写错IDE 还能直接跟踪引用长期维护舒服很多。常用条件方法方法SQL 对应示例eq.eq(User::getName, 张三)ne.ne(User::getStatus, 0)likeLIKE %x%.like(User::getName, 张)likeLeftLIKE %x.likeLeft(User::getName, 三)likeRightLIKE x%.likeRight(User::getName, 张)betweenBETWEEN x AND y.between(User::getAge, 20, 30)inIN (...).in(User::getId, Arrays.asList(1, 2, 3))isNullIS NULL.isNull(User::getEmail)isNotNullIS NOT NULL.isNotNull(User::getEmail)orderByDescORDER BY x DESC.orderByDesc(User::getCreateTime)select指定返回字段.select(User::getId, User::getName)条件构造器还支持嵌套条件wrapper.eq(User::getStatus,1).and(w-w.gt(User::getAge,20).or().like(User::getName,张));// WHERE status 1 AND (age 20 OR name LIKE %张%)更新条件条件构造器不只能用于查询更新也能用LambdaUpdateWrapperUserwrappernewLambdaUpdateWrapper();wrapper.eq(User::getId,1L).set(User::getName,王五).set(User::getAge,null);// set nulluserMapper.update(null,wrapper);// UPDATE user SET name王五, ageNULL WHERE id1Service 层封装MP 还提供了IService接口和ServiceImpl实现类把 Service 层的通用 CRUD 也封装了。// 接口继承 IServicepublicinterfaceUserServiceextendsIServiceUser{}// 实现类继承 ServiceImplServicepublicclassUserServiceImplextendsServiceImplUserMapper,UserimplementsUserService{}IService提供的方法比BaseMapper更丰富常用的// 批量插入自动分批避免 SQL 过长userService.saveBatch(userList,1000);// 按条件查询一条UseruseruserService.getOne(newLambdaQueryWrapperUser().eq(User::getName,张三));// 按条件计数longcountuserService.count(newLambdaQueryWrapperUser().gt(User::getAge,20));// 批量查询ListUserusersuserService.listByIds(Arrays.asList(1L,2L));// 链式查询ListUserresultuserService.lambdaQuery().eq(User::getStatus,1).gt(User::getAge,20).orderByDesc(User::getCreateTime).list();lambdaQuery()和lambdaUpdate()返回链式调用的构造器写起来更简洁。自动填充时间字段不用手动 set表里的create_time、update_time这类字段每次插入或更新都要手动赋值容易遗漏。MP 支持自动填充。第一步实体类加注解TableField(fillFieldFill.INSERT)privateLocalDateTimecreateTime;TableField(fillFieldFill.INSERT_UPDATE)privateLocalDateTimeupdateTime;FieldFill的取值值含义DEFAULT不自动填充INSERT插入时填充UPDATE更新时填充INSERT_UPDATE插入和更新时都填充第二步实现MetaObjectHandlerComponentpublicclassMyMetaObjectHandlerimplementsMetaObjectHandler{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());}}之后调用insert或updateById时createTime和updateTime会自动赋值不需要手动 set。不过这里有个容易踩坑的地方只有通过 MP 提供的方法如insert、updateById执行时自动填充才会生效。如果自己写 XML 更新 SQLMetaObjectHandler是不会执行的。很多人以为配了自动填充就万事大吉结果自定义 SQL 里的时间字段全是 null排查半天才发现是这个问题。分页查询MP 内置了分页功能需要先注册分页插件ConfigurationpublicclassMybatisPlusConfig{BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptornewMybatisPlusInterceptor();interceptor.addInnerInterceptor(newPaginationInnerInterceptor(DbType.MYSQL));returninterceptor;}}分页插件一定要注册不然selectPage()虽然能调用但不会自动拼LIMIT很多人第一次接 MP 都会踩这个坑。使用方式// 第 1 页每页 10 条PageUserpagenewPage(1,10);LambdaQueryWrapperUserwrappernewLambdaQueryWrapper();wrapper.eq(User::getStatus,1).orderByDesc(User::getCreateTime);// 分页查询PageUserresultuserMapper.selectPage(page,wrapper);// 获取结果ListUserrecordsresult.getRecords();// 当前页数据longtotalresult.getTotal();// 总记录数longpagesresult.getPages();// 总页数MP 的分页是物理分页生成的 SQL 带LIMITSELECT*FROMuserWHEREstatus1ORDERBYcreate_timeDESCLIMIT0,10同时会自动执行一条SELECT COUNT(*)获取总记录数。如果不想查总数可以关闭PageUserpagenewPage(1,10,false);// 第三个参数 false 表示不查 count逻辑删除逻辑删除不是真正删除数据而是把记录标记为已删除。比如加一个deleted字段0 表示正常1 表示已删除。配置文件mybatis-plus:global-config:db-config:logic-delete-field:deleted# 逻辑删除字段logic-delete-value:1# 已删除的值logic-not-delete-value:0# 未删除的值实体类加上字段TableLogicprivateIntegerdeleted;之后调用deleteById时执行的不是DELETE而是UPDATEuserMapper.deleteById(1L);// 实际执行UPDATE user SET deleted1 WHERE id1 AND deleted0查询时也会自动过滤已删除的记录userMapper.selectById(1L);// 实际执行SELECT * FROM user WHERE id1 AND deleted0逻辑删除对开发者是透明的查询、更新、删除的方法调用方式不变MP 自动在 SQL 里加上deleted0条件。但有个坑需要提前知道逻辑删除只是帮你把DELETE改成了UPDATE数据库里的数据其实一直都在。所以涉及唯一索引时需要额外注意否则可能出现删了却不能重新插入的问题。比如用户名加了唯一索引逻辑删除张三后再注册一个张三数据库会报唯一键冲突因为那条已删除的记录还在。代码生成器MP 提供了代码生成器根据数据库表自动生成 Entity、Mapper、Service、Controller。FastAutoGenerator.create(jdbc:mysql://localhost:3306/test,root,password).globalConfig(builder-{builder.author(developer)// 作者.outputDir(src/main/java)// 输出目录.disableOpenDir();// 生成后不自动打开目录}).packageConfig(builder-{builder.parent(com.example)// 父包名.moduleName(demo)// 模块名.mapper(mapper)// Mapper 包名.service(service)// Service 包名.controller(controller);// Controller 包名}).strategyConfig(builder-{builder.addInclude(user,order)// 要生成的表.entityBuilder().enableLombok()// Entity 用 Lombok.serviceBuilder().formatServiceFileName(%sService);}).templateEngine(newFreemarkerTemplateEngine()).execute();生成的目录结构src/main/java/com/example/demo/ ├── entity/ │ └── User.java ├── mapper/ │ └── UserMapper.java ├── service/ │ ├── UserService.java │ └── impl/ │ └── UserServiceImpl.java └── controller/ └── UserController.java生成的代码可以直接跑但通常需要根据业务需求做调整比如加自定义方法、改查询逻辑等。MP 和 MyBatis 的关系MP 是 MyBatis 的增强不是替代。它们的关系维度MyBatisMyBatis Plus单表 CRUD手写 XML 或注解BaseMapper 内置方法条件查询XML 里写whereifWrapper 链式调用分页需要第三方插件PageHelper内置分页插件逻辑删除手动实现注解 配置即可自动填充手动实现MetaObjectHandler复杂查询XML 动态 SQL同样支持 XML 动态 SQL多表联查XML 写 resultMap 和 JOIN同样用 XML 写小结如果让我总结 MyBatis Plus我觉得它最大的价值不是功能多而是把那些机械重复的 CRUD 全部接管了让开发者把精力放到真正的业务逻辑上。但是多表联查、复杂 SQL这些还是老老实实写 MyBatis 更舒服。项目里一般也是两者一起用简单 CRUD 交给 MP复杂查询继续写 XML这也是大多数开发者比较常见的用法。