有次我给一个外包项目做代码审查打开UserMapper.xml时整个人都愣住了——selectById、selectList、insert、updateById、deleteById每个表都写了一套几乎一模一样的 SQL。五个表80 多个 XML 方法全是单表 CRUD。我问开发小哥你为什么不直接用 MyBatis-Plus 自动生成这些他挠挠头说项目比较老Mybatis用得好好的不敢引进新东西...我深吸一口气。那一刻我意识到——很多 Java 开发者知道 MyBatis也知道 MyBatis-Plus却不知道他们完全可以共存。 人们总是习惯于自己熟悉的工具而不敢尝试新的东西。这篇文章就是给那个小哥以及所有还在手写单表 CRUD的人准备的。即使你现在已经在使用了这篇文章也能带你从入门到精通。前置要求阅读本文前如果你对 MyBatis 不太熟悉建议你往前翻一翻先看看之前的 MyBatis 教程。如果你已经会用 MyBatis理解 MP 会更加轻松——毕竟 MP 就是在 MyBatis 的肩膀上站起来的。一、基础入门认识这位MyBatis 的效率外挂1.1 MyBatis-Plus 到底解决什么问题回顾一下纯 MyBatis 的日常——即使是再简单的单表操作你也得写 XML 或注解!-- 每个表都要写这些样板代码又臭又长 -- select idselectById resultTypeUserSELECT * FROM user WHERE id #{id}/select insert idinsertINSERT INTO user(name, age) VALUES(#{name}, #{age})/insert update idupdateByIdUPDATE user SET name#{name}, age#{age} WHERE id#{id}/update delete iddeleteByIdDELETE FROM user WHERE id #{id}/delete select idselectList resultTypeUserSELECT * FROM user WHERE age #{age} ORDER BY id DESC/select用上 MyBatis-Plus 之后上面这些 XML 全部可以删掉——因为 BaseMapper 已经帮你写好了。Mapper public interface UserMapper extends BaseMapperUser { // 什么都不用写常用 CRUD 全都有 }就这么简单。BaseMapperUser带来了 17 个现成方法public interface BaseMapperT { int insert(T entity); // 插入 int deleteById(Serializable id); // 按 ID 删 int deleteByMap(MapString, Object map); // 按条件删 int delete(WrapperT wrapper); // 按条件删Wrapper int deleteBatchIds(Collection? ids); // 批量删 int updateById(T entity); // 按 ID 改 int update(T entity, WrapperT wrapper); // 按条件改 T selectById(Serializable id); // 按 ID 查 ListT selectBatchIds(Collection? ids); // 批量查 ListT selectByMap(MapString, Object map); // 按条件查 T selectOne(WrapperT wrapper); // 查一条 long selectCount(WrapperT wrapper); // 统计数量 ListT selectList(WrapperT wrapper); // 列表查询 ListMapString, Object selectMaps(WrapperT wrapper); // 查 Map ListObject selectObjs(WrapperT wrapper); // 只查第一列 P extends IPageT P selectPage(P page, WrapperT wrapper); // 分页查询 P extends IPageMapString, Object P selectMapsPage(P page, WrapperT wrapper); }关键理解MyBatis-Plus 不是替代 MyBatis而是增强它。MP 底层仍然是 MyBatis你在 XML 里写的 SQL 依然有效。MP 只是在 MyBatis 之上封装了一层让你不用写那些重复的样板 SQL。1.2 3 分钟迁移从 MyBatis 到 MyBatis-Plus第一步替换依赖把mybatis-spring-boot-starter换成mybatis-plus-boot-starter已包含 MyBatis 核心依赖dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.7/version /dependencySpring Boot 3.x 用mybatis-plus-spring-boot3-starterSpring Boot 2.x 用mybatis-plus-boot-starter。第二步修改配置# 原来的 mybatis 配置前缀 换成 mybatis-plus mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印 SQL global-config: # MP 的全局配置 db-config: id-type: auto # 主键策略 logic-delete-field: deleted # 逻辑删除字段 logic-delete-value: 1 # 已删除的值 logic-not-delete-value: 0 # 未删除的值第三步实体类加上注解Data TableName(user) // 指定表名类名和表名一致可省略 public class User { TableId(type IdType.AUTO) // 主键自增 private Long id; private String name; private Integer age; private String email; TableField(fill FieldFill.INSERT) // 插入时自动填充 private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) // 更新时也填充 private LocalDateTime updateTime; TableLogic // 逻辑删除标记 private Integer deleted; }第四步Mapper 继承 BaseMapperMapper public interface UserMapper extends BaseMapperUser { // 单表 CRUD 不需要写任何代码 // 复杂查询仍可以在这里定义方法配合 XML 使用 ListUser selectUsersWithOrders(Param(minAge) Integer minAge); }搞定你原有的 MyBatis 代码不需要任何修改XML 映射依然有效。MP 只是额外给你提供了BaseMapper的便捷方法。一个值得思考的问题MP 生成的 CRUD SQL 是怎么来的它底层通过SqlMethod枚举维护了一套 SQL 模板运行时根据实体类的注解信息动态拼接 SQL。本质上和你在 XML 里写的是一样的只是框架帮你写了。1.3 第一个 MP 测试感受不写 SQL的快感SpringBootTest class UserMapperTest { Autowired private UserMapper userMapper; Test void testBaseMapper() { // 插入 User user new User(); user.setName(张三); user.setAge(25); user.setEmail(zhangsantest.com); userMapper.insert(user); // 自动回填 ID System.out.println(插入后的 ID user.getId()); // 查询 User result userMapper.selectById(user.getId()); System.out.println(查出来 result); // 更新 result.setAge(26); userMapper.updateById(result); // 删除 userMapper.deleteById(user.getId()); // 批量操作 ListUser users List.of( new User(李四, 30, lisitest.com), new User(王五, 28, wangwutest.com) ); // 逐条插入 users.forEach(userMapper::insert); } }看到没没有 XML没有注解甚至没有 SQL 字符串——代码自动帮你做了一切。这就是 MyBatis-Plus 的核心价值让单表 CRUD 变成真正的零代码。二、核心进阶条件构造器和那些用了就回不去的功能2.1 Wrapper 条件构造器Java 版动态 SQLMyBatis 用 XML 写动态 SQL 很优雅但 MP 给了你另一种选择——用纯 Java 代码构造查询条件。QueryWrapper用字符串写字段名与 XML 中的字段对应// 等同于SELECT * FROM user WHERE name LIKE %张% AND age 20 ORDER BY age DESC QueryWrapperUser wrapper new QueryWrapper(); wrapper.like(name, 张) // name LIKE %张% .gt(age, 20) // age 20 .orderByDesc(age); // ORDER BY age DESC ListUser users userMapper.selectList(wrapper);LambdaQueryWrapper用 Lambda 表达式引用字段类型安全重构友好// 效果同上但字段名是编译时检查的不会写错 LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.like(User::getName, 张) .gt(User::getAge, 20) .orderByDesc(User::getAge); ListUser users userMapper.selectList(wrapper);强烈推荐 LambdaQueryWrapper——字段名用User::getName而不是字符串name这样重构时改了字段名 IDE 会自动提示不会出现线上因为字段名写错导致查不出数据的诡异 Bug。常用条件大全LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); // 等值 wrapper.eq(User::getAge, 25); // age 25 wrapper.ne(User::getAge, 25); // age 25 // 比较 wrapper.gt(User::getAge, 18); // age 18 wrapper.ge(User::getAge, 18); // age 18 wrapper.lt(User::getAge, 60); // age 60 wrapper.le(User::getAge, 60); // age 60 // 范围 wrapper.between(User::getAge, 18, 60); // age BETWEEN 18 AND 60 wrapper.in(User::getAge, 20, 25, 30); // age IN (20, 25, 30) wrapper.inSql(User::getId, SELECT user_id FROM order WHERE amount 100); // IN 子查询 wrapper.notIn(User::getAge, 1, 2, 3); // age NOT IN (1, 2, 3) // 模糊 wrapper.like(User::getName, 张); // name LIKE %张% wrapper.likeLeft(User::getName, 张); // name LIKE %张 wrapper.likeRight(User::getName, 张); // name LIKE 张% // 空值判断 wrapper.isNull(User::getEmail); // email IS NULL wrapper.isNotNull(User::getEmail); // email IS NOT NULL // 排序 wrapper.orderByAsc(User::getAge); // ORDER BY age ASC wrapper.orderByDesc(User::getAge); // ORDER BY age DESC wrapper.orderByDesc(User::getAge, User::getId); // 多字段排序 // 分组 wrapper.groupBy(User::getAge); // GROUP BY age wrapper.having(COUNT(*) {0}, 5); // HAVING COUNT(*) 5 // OR 条件 wrapper.eq(User::getAge, 25).or().eq(User::getAge, 30); // age 25 OR age 30 // 嵌套条件重要 wrapper.and(w - w.eq(User::getAge, 18).or().eq(User::getAge, 60)); // 生成(age 18 OR age 60)条件拼接的优雅写法——动态查询在 Java 代码里的最佳实践// Service 层 public ListUser searchUsers(String name, Integer minAge, Integer maxAge) { LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); // 只有传了参数才拼接条件等价于 XML 的 if wrapper.like(StringUtils.hasText(name), User::getName, name) .ge(minAge ! null, User::getAge, minAge) .le(maxAge ! null, User::getAge, maxAge) .orderByDesc(User::getCreateTime); return userMapper.selectList(wrapper); }第一个参数传boolean条件为false时该条件不会拼到 SQL 里——这就是动态 SQL和 XML 的if test...异曲同工。UpdateWrapper 和 LambdaUpdateWrapper// 按条件更新下个月起所有未成年用户年龄 1 LambdaUpdateWrapperUser updateWrapper new LambdaUpdateWrapper(); updateWrapper.lt(User::getAge, 18) .setSql(age age 1); // setSql 可以直接写 SQL 片段 userMapper.update(null, updateWrapper);2.2 分页查询3 行代码实现物理分页MyBatis-Plus 的分页插件让物理分页变得极其简单第一步配置分页插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }第二步使用分页// Service 层 public IPageUser getUsersByPage(int pageNum, int pageSize) { PageUser page new Page(pageNum, pageSize); // 带条件的分页 LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.gt(User::getAge, 18); // selectPage 方法来自 BaseMapper return userMapper.selectPage(page, wrapper); } // 返回的 IPage 包含这些信息 // page.getRecords() → 当前页数据 // page.getTotal() → 总记录数 // page.getCurrent() → 当前页码 // page.getPages() → 总页数 // page.getSize() → 每页大小自定义分页 SQL多表联查分页!-- UserMapper.xml -- select idselectUserPage resultTypecom.example.vo.UserVO SELECT u.*, o.order_count FROM user u LEFT JOIN (SELECT user_id, COUNT(*) as order_count FROM order GROUP BY user_id) o ON u.id o.user_id where if testminAge ! nullAND u.age #{minAge}/if /where ORDER BY u.id DESC /select// Mapper 接口 —— 注意第一个参数必须是 Page Mapper public interface UserMapper extends BaseMapperUser { IPageUserVO selectUserPage(Page? page, Param(minAge) Integer minAge); }MP 会自动拦截在 SQL 末尾追加 LIMIT。2.3 逻辑删除数据软删除的标准实现面试经常被问到软删除怎么做——MP 的逻辑删除就是标准答案配置mybatis-plus: global-config: db-config: logic-delete-field: deleted # 逻辑删除字段 logic-delete-value: 1 # 删除后的值 logic-not-delete-value: 0 # 未删除的值实体类标记Data public class User { private Long id; private String name; TableLogic private Integer deleted; // 0未删除1已删除 }效果演示// 你调用 userMapper.deleteById(1L); // MP 实际执行的 SQLDELETE 变成了 UPDATE // UPDATE user SET deleted 1 WHERE id 1 AND deleted 0 // 查询时自动过滤已删除数据 userMapper.selectById(1L); // SELECT * FROM user WHERE id 1 AND deleted 0TableLogic让所有 CRUD 操作自动带上deleted 0你完全不用手动过滤。这才是真正的无感软删除。2.4 乐观锁用版本号防止并发冲突假设你和同事同时编辑一条用户数据——你改年龄同事改邮箱。如果都用updateById后提交的会覆盖先提交的内容。这就是并发冲突。MP 的乐观锁插件用版本号解决这个问题第一步实体类加字段Data public class User { private Long id; private String name; Version private Integer version; // 版本号字段 }第二步注册插件Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 乐观锁 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }原理// 更新时MP 自动在 WHERE 里加上版本号条件 userMapper.updateById(user); // UPDATE user SET name 张三, age 30, version 2 // WHERE id 1 AND version 1 // 如果 version 已经变成 2别人改过这个 UPDATE 影响行数为 0 // MP 检测到影响行数为 0会抛出 OptimisticLockException这就是 CASCompare And Swap思想在数据库层面的应用。2.5 自动填充createTime 和 updateTime 不用手写每个表几乎都有create_time和update_time每次插入更新都手动设置太烦了。MP 的自动填充帮你搞定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()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }Data public class User { TableField(fill FieldFill.INSERT) // 插入时自动填 private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) // 插入和更新时都填 private LocalDateTime updateTime; }之后你再也不用user.setCreateTime(new Date())了插入更新时 MP 会自动帮你填。三、深入理解MP 的进阶特性和原理3.1 代码生成器一键生成 CRUD 全家桶如果说 BaseMapper 是刀那代码生成器就是一键生成一整套工具车。它可以自动生成 Entity、Mapper、Service、ServiceImpl、Controller 全套代码。public class CodeGenerator { public static void main(String[] args) { FastAutoGenerator.create( jdbc:mysql://localhost:3306/my_db?useSSLfalseserverTimezoneAsia/Shanghai, root, password) .globalConfig(builder - builder .author(你的名字) .outputDir(D:/code/my-project/src/main/java) .disableOpenDir()) .packageConfig(builder - builder .parent(com.example) .moduleName(user) .entity(entity) .mapper(mapper) .service(service) .serviceImpl(service.impl) .controller(controller)) .strategyConfig(builder - builder .addInclude(user, order, product) // 要生成的表 .entityBuilder() .enableLombok() .enableTableFieldAnnotation() .controllerBuilder() .enableRestStyle()) .execute(); } }运行之后整个 CRUD 体系就生成了——Entity 的结构、Mapper 继承了 BaseMapper、Service 继承了 IService、Controller 实现了基础 CRUD 接口。实用建议新项目初期可以用代码生成器快速搭建但项目稳定后建议关闭避免有人在生成器配置里多加了一张表导致全覆盖已手动修改过的文件。3.2 IService 接口连 Service 层都不用写MP 不仅在 Mapper 层提供了BaseMapper还在 Service 层提供了IService和ServiceImpl// Service 接口 public interface UserService extends IServiceUser { // 除了 IService 提供的通用方法这里定义业务方法 boolean register(UserRegisterReq req); } // Service 实现类 Service public class UserServiceImpl extends ServiceImplUserMapper, User // 关键继承 ServiceImpl implements UserService { Override public boolean register(UserRegisterReq req) { // 可以直接用 IService 提供的方法 LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getEmail, req.getEmail()); if (this.count(wrapper) 0) { // this.count 来自 IService throw new BusinessException(邮箱已被注册); } User user new User(); user.setName(req.getName()); user.setEmail(req.getEmail()); return this.save(user); // this.save 来自 IService } }IService提供了一批比BaseMapper更智能的方法BaseMapper 方法IService 方法区别insert(entity)save(entity)IService 多了批量操作和事务控制updateById(entity)updateById(entity)一致—saveOrUpdate(entity)IService 独有有 ID 则更新无 ID 则插入selectList(wrapper)list(wrapper)一致selectPage(page, wrapper)page(page, wrapper)一致3.3 ActiveRecord 模式连 Mapper 注入都省了如果你连Autowired private UserMapper userMapper都不想写MP 还提供了 ActiveRecord 模式——实体类自己就能操作数据库Data TableName(user) public class User extends ModelUser { // 关键继承 Model private Long id; private String name; // 不需要 Mapper实体类直接操作数据库 } // 使用方法 User user new User(); user.setName(张三); user.insert(); // 自己插入自己 User found new User(); found.setId(1L); User result found.selectById(); // 自己查自己 user.setName(张三三); user.updateById(); // 自己更新自己不过说实话这种模式在实际项目中用得不多——它把数据库操作混入了实体类违反了单一职责原则。但小项目或快速原型开发时确实很方便。选型建议正式项目推荐BaseMapper模式Mapper 层清晰快速原型可以用Model省事。3.4 与 MyBatis 原生 SQL 的共存之道MP 并不意味着你要放弃手写 SQL。事实上MP 自定义 SQL 是最佳组合Mapper public interface UserMapper extends BaseMapperUser { // ✅ 单表简单查询 → 用 BaseMapper 提供的方法 // ✅ 复杂联表查询 → 自定义方法 XML ListUserOrderVO selectUserOrders(Param(userId) Long userId); // ✅ 分页查询 → BaseMapper 方法 自定义 SQL 结合 IPageUserVO selectUserPage(Page? page, Param(keyword) String keyword); // ✅ 统计分析 → 自定义 SQL MapString, Object selectUserStats(); }对应的 XML 依然写在resources/mapper/下和纯 MyBatis 项目完全一样mapper namespacecom.example.mapper.UserMapper select idselectUserOrders resultTypecom.example.vo.UserOrderVO SELECT u.id, u.name, o.order_no, o.amount FROM user u INNER JOIN order o ON u.id o.user_id WHERE u.id #{userId} /select /mapper核心原则简单操作用 MP复杂 SQL 用手写。两者不冲突MP 只是在 MyBatis 上加了增强功能。3.5 MyBatis-Plus Join省去手写联表 SQL对于多表联查官方社区版也提供了mybatis-plus-join插件// 联表查询 —— 不用写 JOIN SQL ListUserVO list JoinLambdaWrapper.of(User.class) .selectAll(User.class) .select(Order::getAmount) .leftJoin(Order.class, Order::getUserId, User::getId) .gt(User::getAge, 18) .list(UserVO.class);不过这个功能目前还在社区维护中生产环境使用时建议评估稳定性和版本兼容性。保守方案仍然是手写 XML 联表 SQL。四、实战与面试避坑指南和高频考题4.1 常见踩坑指南坑 1LambdaQueryWrapper 中eq传 null// ❌ 错误name 为 null 时会生成 WHERE name null查不到数据 wrapper.eq(User::getName, name); // ✅ 正确用条件参数控制 wrapper.eq(name ! null, User::getName, name);坑 2逻辑删除后唯一索引冲突如果name字段有唯一索引用户张三被逻辑删除后想再注册一个张三会因为唯一索引冲突而失败。解决方案是联合唯一索引CREATE UNIQUE INDEX uk_name_deleted ON user(name, deleted);这样(张三, 0)和(张三, 1)不算冲突。坑 3updateById不更新 null 值User user userMapper.selectById(1L); user.setEmail(null); // 想把 email 清空 userMapper.updateById(user); // ❌ email 字段不会被更新为 NULLMP 默认策略是只更新非 null 字段。如果要清空字段需要// 方案一用 UpdateWrapper LambdaUpdateWrapperUser wrapper new LambdaUpdateWrapper(); wrapper.eq(User::getId, 1L) .set(User::getEmail, null); userMapper.update(null, wrapper); // 方案二字段上加注解不推荐会影响其他更新场景 TableField(updateStrategy FieldStrategy.IGNORED) private String email;坑 4分页插件忘记配置// ❌ 没配分页插件selectPage 返回的是全量数据 userMapper.selectPage(page, wrapper); // page.getTotal() 总记录数没分页必须在MybatisPlusInterceptor中注册PaginationInnerInterceptor。4.2 最佳实践总结MyBatis-Plus 最佳实践清单 │ ├── 1. 实体类规范 │ ├── 用 TableName 明确指定表名避免类名和表名不同时找不到表 │ ├── 用 TableId 明确指定主键策略默认雪花算法不是自增 │ ├── 用 TableLogic 标记逻辑删除字段 │ └── 日期字段用 LocalDateTimeJava 8 │ ├── 2. 条件构造器规范 │ ├── 优先用 LambdaQueryWrapper类型安全 │ ├── 每个 eq/like 都带上 boolean 条件防止 null 值 │ └── 复杂嵌套条件要仔细检查括号用 .and(w - ...) │ ├── 3. 更新操作规范 │ ├── 更新单个对象用 updateById │ ├── 批量条件更新用 UpdateWrapper │ └── 注意 updateById 不更新 null 的默认策略 │ └── 4. 性能规范 ├── 列表查询一定加分页避免一次查出几十万条 ├── 大批量插入用 saveBatch(new ArrayList(), 1000) └── 合理使用 select() 指定查询列减少网络传输4.3 10 道高频面试题精讲Q1MyBatis-Plus 和 MyBatis 是什么关系MP 是 MyBatis 的增强工具在 MyBatis 基础上提供了开箱即用的 CRUD 接口、条件构造器、分页插件、逻辑删除等功能。它不替代 MyBatis只是让你少写样板代码。MP 底层仍是 MyBatis你写的 XML/注解 SQL 完全不受影响。Q2BaseMapper 里的方法是怎么实现的MP 通过AbstractMethod的各个子类维护了一套 SQL 模板如SelectById、Insert等运行时根据实体类的注解信息动态拼接 SQL再注入到 MyBatis 的Configuration中。本质是框架帮你写了那些你懒得写的 CRUD SQL。Q3MyBatis-Plus 的分页原理是什么分页插件PaginationInnerInterceptor拦截了 MyBatis 的Executor.prepare()方法在原始 SQL 外层包裹 LIMIT/OFFSET如 MySQL 的LIMIT 0,10。同时执行一条 COUNT 查询获得总记录数。Q4逻辑删除是怎么实现的实体类字段加TableLogic后MP 自动将 DELETE 改写为 UPDATE设置删除标记所有 SELECT/UPDATE 自动拼入deleted 0条件。这是通过SqlInjector注入自定义 SQL 方法实现的。Q5MP 的主键策略有哪些ASSIGN_ID默认雪花算法、AUTO数据库自增、INPUT手动设置、NONE无策略、ASSIGN_UUIDUUID。推荐分布式系统用 ASSIGN_ID单机用 AUTO。Q6如何处理多表联查三种方式① 手写 XML JOIN SQL最推荐最灵活② 使用mybatis-plus-join插件③ 分别在 Service 层多次查询后组装不推荐有 N1 问题。Q7LambdaQueryWrapper 和 QueryWrapper 的区别LambdaQueryWrapper 用 Lambda 表达式引用字段User::getName编译时检查字段名QueryWrapper 用字符串name运行时才检查。生产环境推荐 Lambda 方式。Q8乐观锁插件的实现原理更新时自动在 WHERE 条件中加入version #{oldVersion}同时在 SET 中将version 1。如果影响行数为 0说明版本已被别人修改抛出异常。Q9MP 的自动填充是怎么实现的通过MetaObjectHandler接口在插入或更新数据前MP 会回调insertFill()或updateFill()方法自动设置指定字段的值。常用于createTime、updateTime等。Q10什么场景不适合用 MyBatis-Plus① 极度复杂的报表查询多表联查、聚合统计手写 SQL 更可控② 数据库不是标准关系型NoSQL 等③ 团队对 SQL 生成有极度审计要求需要精确控制每一行 SQL。写在最后从 MyBatis 到 MyBatis-Plus本质上是从手动挡升级到了手自一体。日常通勤单表 CRUD挂 D 档自动走偶尔想爽一把复杂联表查询还能切手动自己写 SQL。记住一个原则MP 帮你省掉的是重复劳动不是思考。条件构造器的嵌套逻辑、分页插件的 COUNT 查询、逻辑删除的唯一索引——这些细节仍然需要你理解原理才能用对。