我最早接触MyBatis-Plus是在刚接手一个老后端项目的时候。那会儿项目里有二十多张表,每新增一张表,都要先写一遍Mapper接口、XML文件里的insert、delete、update、selectById,再补两个多条件查询——光这部分机械重复的代码,就能耗掉大半天。后来把基础CRUD全部切换到MyBatis-Plus,从建表到跑通接口,速度直接上了一个台阶。
这篇文章就围绕MyBatis-Plus的核心玩法来写,定位是“速成版”,所以不讲太多底层源码,直接讲怎么搭、怎么用、有哪些坑。面向的读者是:已经会用Spring Boot和MyBatis,但还没系统用过MyBatis-Plus,或者用过一点但想补全知识点的Java后端开发。看完以后,你至少能独立完成一个项目的MP集成、CRUD、条件查询、分页、逻辑删除、乐观锁这些高频需求,并且知道哪里容易出事。
1. 为什么是MyBatis-Plus:从手写CRUD的疲惫感说起
1.1 原生MyBatis的单表CRUD体验
很多人最初用MyBatis,是被它的灵活SQL吸引的。复杂多表联查、动态SQL、自定义结果映射,确实比JPA直白,也比JDBC手动拼参数舒服。但随着项目越做越大,一个让人烦躁的问题会浮出来:单表CRUD太重复了。
每来一张新表,你就得写这些方法:
- insert(User user)
- deleteById(Long id)
- selectById(Long id)
- updateById(User user)
- selectList(UserQuery query)
每个方法都要在XML里来一遍INSERT INTO user (...)、DELETE FROM user WHERE id = ?、SELECT * FROM user WHERE id = ?,大同小异。如果项目里再用MyBatis Generator生成一遍基础代码,生成的实体类、Mapper、XML一大坨,看着很多,其实全是模板。关键是业务查询一有变化,比如多个筛选条件、排序规则变了,你还得手动改XML,效率很低。
1.2 MP的设计哲学:只增强,不侵入
MyBatis-Plus(简称MP)是一个国产的MyBatis增强工具,官网有一句原话叫“只做增强不做侵入”。这句话怎么理解?
- 不改变MyBatis原有的写法,你之前手写的Mapper、XML照样能用。
- 它在你现有的Mapper接口之上,内置了一个
BaseMapper<T>,把单表的增删改查、批量查询、分页查询全部封装好。 - 查询条件多变的问题,用它提供的
Wrapper(条件构造器)解决,不用写XML。 - 分页、逻辑删除、乐观锁、自动填充这些高频能力,用插件机制加进去。
做了这些事以后,你手写SQL的量会大幅下降。大部分单表操作不再需要XML,只有真正复杂的多表联查、子查询、报表统计才需要自己写。
1.3 什么时候该用MP,什么时候别硬上
用MP前先明确它的适用边界,不是所有项目都无脑上。
适合用MP的项目:
- 以单表CRUD为主的业务系统,比如后台管理系统、中台服务、各种内部平台。
- 快速迭代的项目,连表结构都不稳定,用MP可以少写很多重复代码,改字段直接改实体就行。
- 团队里MyBatis水平参差不齐,用MP可以把基础数据访问层的门槛降下来。
不适合用MP或需要谨慎的场景:
- 大量复杂的多表关联查询、报表统计、动态列查询。这类需求MP帮不上太多忙,还是得手写SQL,甚至会因为实体映射的约定影响SQL可读性。
- 对SQL执行完全可控要求极高的金融、账务系统。MP内置方法生成的SQL虽然能看,但毕竟多了一层封装,部分团队会忌讳这种“不可控”。
- 已有完善的MyBatis代码规范、生成体系和大量XML的存量项目,硬引入MP可能收益不大,还要解决共存问题。
一句话总结:MP解决的是“单表CRUD写起来烦”的问题,它不解决“复杂SQL怎么写”的问题。带着这个认知去用,你就不会对它抱有不切实际的期待。
2. 环境搭建:Spring Boot集成MP的几个关键点
2.1 依赖引入与版本避坑
我直接给结论。如果是Spring Boot 3.x项目,用这个:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.7</version> </dependency>如果是Spring Boot 2.x项目,用这个:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.7</version> </dependency>注意这个差异。MP从3.5.4左右开始适配Spring Boot 3,官方拆分了新的starter。如果用的是Spring Boot 3,却引入了旧的mybatis-plus-boot-starter,启动时很可能遇到Failed to introspect Class或者MyBatis相关Bean装配失败。这种版本错位问题在社区里很常见,排查起来还容易绕远路。
另外,引入MP starter后,不要再重复引入mybatis-spring-boot-starter,否则会有冲突。MP自带了一套MyBatis的整合逻辑,重复引入会导致SqlSessionFactory重复创建或Bean覆盖。
2.2 yml配置中最容易被忽略的两项
下面是一份最小可用的配置:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: banner: false db-config: id-type: assign_id logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0第一个容易忽略的是log-impl。不配置这个,控制台看不到SQL日志,出了问题全靠猜。加上以后,每次执行数据库操作都会在控制台打印完整的SQL和参数,排错效率直线上升。生产环境记得关掉。
第二个是map-underscore-to-camel-case。MP默认是true,但如果你在多个项目之间复制配置,可能被改成false,导致user_name字段映射不到实体的userName属性,查询结果全是null且不报错。这个问题排查起来非常坑,建议显式写出来。
global-config.db-config下面的几个配置是MP的全局策略:
id-type: assign_id表示主键默认使用雪花算法生成Long型ID,适合分布式场景。logic-delete-field声明所有实体里叫deleted的字段都走逻辑删除逻辑,后面会细说。banner: false是关掉启动时的MP那个大Banner,纯属图清爽。
2.3 第一个继承BaseMapper的Mapper
配置完成后,写一个实体类和一个Mapper接口,就算是环境通了。
@Data @TableName("sys_user") public class User { @TableId(type = IdType.ASSIGN_ID) 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; @Version private Integer version; }public interface UserMapper extends BaseMapper<User> { }然后在启动类或者配置类上加上@MapperScan:
@SpringBootApplication @MapperScan("com.example.demo.mapper") public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }到这一步,UserMapper就已经有了一整套单表操作方法。接下来直接注入调用即可。
3. CRUD接口:先掌握这些就够用了
3.1 BaseMapper:单表操作的底牌
BaseMapper<T>里提供的方法,实际开发中百分之八十用得到。我整理了一份速查表:
| 方法 | 作用 | 备注 |
|---|---|---|
insert(entity) | 插入一条记录 | 主键策略由@TableId决定 |
deleteById(id) | 按主键删除 | 开启逻辑删除后自动转为UPDATE |
deleteBatchIds(ids) | 按主键批量删除 | 同上 |
updateById(entity) | 按主键更新 | 实体中为null的字段不会更新 |
selectById(id) | 按主键查询 | 返回实体对象 |
selectBatchIds(ids) | 按主键批量查询 | 返回实体列表 |
selectOne(wrapper) | 按条件查询一条 | 查到多条会抛异常,要注意 |
selectCount(wrapper) | 按条件统计数量 | 返回Long |
selectList(wrapper) | 按条件查询列表 | wrapper传null就是查询全部 |
selectPage(page, wrapper) | 分页查询 | 需要分页插件 |
selectMaps(wrapper) | 按条件查询列表 | 返回List<Map<String, Object>> |
看几个常用写法:
// 新增 User user = new User(); user.setName("张三"); user.setAge(20); user.setEmail("zhangsan@example.com"); userMapper.insert(user); // 查询单个 User u = userMapper.selectById(1L); // 条件查询列表 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getAge, 20); List<User> users = userMapper.selectList(wrapper); // 修改 User u = userMapper.selectById(1L); u.setAge(21); userMapper.updateById(u); // 删除 userMapper.deleteById(1L);有一个细节特别值得注意:updateById只更新实体中非null的字段。如果业务上需要把某个字段置为null,用这个方法做不到,得用后面讲的UpdateWrapper显式 set。很多新手在这里踩坑:想清空某个字段,发现执行成功了但数据库完全没变化。
3.2 IService:批量操作和链式调用的体验
如果你用的是Service层开发模式,MP还提供了一个IService<T>接口和对应的ServiceImpl<M, T>实现类。继承它以后,除了拿到CRUD能力,还有一些Service层很实用的方法。
public interface UserService extends IService<User> { } @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }这样写完之后,UserService自带的能力包括:
save(entity):单条插入saveBatch(list):批量插入,内部会分批执行saveOrUpdate(entity):存在则更新,不存在则插入list(wrapper):查询列表count(wrapper):统计getById(id):按主键查removeById(id):按主键删page(page, wrapper):分页
更重要的是链式查询写法,代码可读性比传统写法高不少:
// 查询名字叫张三且年龄大于18的用户列表 List<User> users = userService.lambdaQuery() .eq(User::getName, "张三") .gt(User::getAge, 18) .list(); // 把id为1的用户年龄更新为30 boolean updated = userService.lambdaUpdate() .set(User::getAge, 30) .eq(User::getId, 1) .update();用IService的好处是Service层语义清晰,且自带批量、链式能力,不用自己定义一堆重复方法。比较适合标准的三层架构项目。
3.3 自写SQL + MP内置方法的组合拳
有些朋友用上MP以后,误以为不能再手写XML了,其实不是。MP和手写SQL完全不冲突,而且经常要配合使用。
比如要在UserMapper上加一个多表关联查询:
public interface UserMapper extends BaseMapper<User> { User selectUserWithOrders(@Param("userId") Long userId); }XML里正常写:
<select id="selectUserWithOrders" resultType="com.example.demo.entity.User"> SELECT u.* FROM sys_user u LEFT JOIN sys_order o ON u.id = o.user_id WHERE u.id = #{userId} </select>这里的UserMapper既继承了BaseMapper的通用方法,又能定义自己的自定义方法,两者共存互不干扰。实际项目中,我用MP处理单表CRUD,手写SQL处理多表联查和复杂统计,配合得很舒服。
4. 条件构造器Wrapper:最值得吃透的部分
4.1 QueryWrapper与LambdaQueryWrapper的选择逻辑
MP里最核心也是最容易掌握不透的就是Wrapper条件构造器。它有两种主要形态:
QueryWrapper:用字符串指定列名,例如.eq("name", "张三")。LambdaQueryWrapper:用方法引用指定属性,例如.eq(User::getName, "张三")。
我强烈建议优先用LambdaQueryWrapper。原因很简单:
- 编译期就能检查属性名是否拼错,而
QueryWrapper的字符串写错了要运行时报错。 - 实体字段重命名时,IDE能自动同步修改方法引用,字符串可是不会变的。
- 字符串方式在SQL注入和列名合法性上虽然有内部处理,但可读性和维护性都差一截。
当然,QueryWrapper也不是完全没用。比如你从外部动态接收一个字段名做排序,LambdaQueryWrapper不太好表达,这时用QueryWrapper.orderByAsc(column)更方便。但主体查询条件,还是Lambda写法为主。
4.2 动态条件:condition参数的正确打开方式
一个典型的查询场景是:用户列表接口,支持按姓名模糊搜索、按年龄范围筛选,条件是前端可选的。如果手动拼SQL,你得写一堆if判断。用MP的Wrapper,可以这样:
public List<User> listUsers(String name, Integer minAge, Integer maxAge) { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); 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); }这里的关键就是ge这类方法的第一个参数boolean condition。当condition为true时,当前条件才拼进SQL;为false时直接跳过。这样把原本要写好几层的if判断压缩成一行一个条件,代码干净得多。
这是MP最实用的一个特性,建议形成肌肉记忆:所有动态筛选条件,都走条件参数。
4.3 UpdateWrapper:只更新你想更新的字段
前面提到updateById不会更新null字段。如果要把某个字段更新成null,或者要按复杂条件更新多条记录,就需要UpdateWrapper。
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>(); wrapper.eq(User::getName, "张三") .set(User::getAge, null) .set(User::getEmail, "new@example.com"); userMapper.update(null, wrapper);这里第一个参数传null,表示不按实体更新,所有更新内容都由wrapper的set决定。这种方式还能做“把年龄小于18的用户状态改为禁用”这类批量更新,比先查出来再逐个updateById高效得多。
4.4 Wrapper的三个“坑”找出来:or优先级、like转义、last注入
这部分是实际项目中高频踩坑的地方,每个都值得单独说说。
第一个坑是or的优先级。看这个写法:
wrapper.eq(User::getName, "张三") .eq(User::getAge, 18) .or() .eq(User::getStatus, 1);生成的SQL是:
WHERE name = '张三' AND age = 18 OR status = 1因为SQL里AND优先级高于OR,这条SQL实际含义是(name = '张三' AND age = 18) OR status = 1,和你心里想的“名字是张三,并且(年龄18或状态1)”完全不一样。如果要做括号内的OR组合,得用嵌套:
wrapper.eq(User::getName, "张三") .and(w -> w.eq(User::getAge, 18).or().eq(User::getStatus, 1));这样生成的SQL才是WHERE name = '张三' AND (age = 18 OR status = 1)。
第二个坑是like的百分号转义。MP的.like(User::getName, name)生成的SQL是LIKE '%' ? '%',也就是说它帮你把参数用百分号包好了,你传参时不需要自己再加%%。但如果你查询的关键字本身包含%,比如用户搜“100%”,这个%会直接作为通配符进SQL,导致结果不准确。处理方式是手动转义,或者对输入做过滤。
第三个坑是last方法。wrapper.last("limit 10")、wrapper.last("for update")这类写法能把任意SQL片段拼到查询语句末尾,非常方便,但这也是最危险的入口。绝对不要直接把前端传参拼进last,比如wrapper.last("limit " + pageSize),如果pageSize是用户可控的,这就是SQL注入点。建议只用固定的安全片段,或者限制参数类型再做白名单校验。
5. 分页插件:配置一次,收效长久
5.1 拦截器配置与分页原理
MP的分页依赖一个内置拦截器。不配置这个拦截器,调用selectPage不会生效,那就要注意了:它在查询时会查出全表数据到内存里,但total始终为0,页面数据却是全量的,特别误导人。
正确的配置方式如下:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); // 单页最大条数限制,防止恶意大分页 pagination.setMaxLimit(500L); interceptor.addInnerInterceptor(pagination); return interceptor; } }原理上,分页拦截器会在SQL执行前做两件事:
- 改写原SQL,生成一条
COUNT查询语句,用于统计总条数。 - 在原SQL后面拼接
LIMIT offset, size,执行分页查询。
这两步对一个请求来说是一次完成的,Page对象里既带records又有total,所以前端只要拿到Page对象就能直接渲染。
5.2 分页查询的正确用法
public Page<User> pageUsers(int pageNum, int pageSize, String name) { Page<User> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(name), User::getName, name) .orderByDesc(User::getCreateTime); return userMapper.selectPage(page, wrapper); }这里有几个点要注意:
Page的页码从1开始,前端传0的话需要转换。selectPage的第一个参数是Page对象,第二个是查询条件wrapper,wrapper可以传null表示无条件分页。- 查询结果通过
page.getRecords()获取列表,page.getTotal()获取总条数。 page.getCurrent()和page.getSize()后端可以直接用传入的页码和条数,不需要再多声明两个参数。
5.3 大分页与count的优化思路
分页用的多了,必然遇到性能问题。最典型的是深度分页:用户翻到第10000页,SQL会被改写成LIMIT 1000000, 20,MySQL会扫描前100万行再丢弃,性能非常差。
几个优化方向,按我的使用经验排列:
- 限制最大页码和每页条数,从产品层面避免用户翻到很深的页。
- 用游标分页(keyset分页)替代传统page分页。做法是记录上一页最后一条记录的ID,查询条件改成
WHERE id < ? ORDER BY id DESC LIMIT 20,这样即使数据量很大,也能走索引快速定位。 - 优化count查询。默认生成的count在某些复杂查询下效率不高,可以手动指定。Page对象有一个
setSearchCount(false)方法,可以关掉自动count,然后自己单独执行一次count查询,对结果做精确控制。
这套分页插件在中小型项目里已经够用,但一旦数据量上了千万级,就要考虑换更针对性的方案。
6. 逻辑删除、乐观锁、自动填充:三个高频企业需求
6.1 逻辑删除:删除变成更新,查询自动过滤
逻辑删除的思路很简单:数据不物理删除,而是用一个字段标记为已删除。好处是数据可追溯、可恢复,这在企业系统里几乎是刚需。
MP里开启逻辑删除有两种方式:
- 全局配置:
logic-delete-field: deleted,所有实体的deleted字段自动生效。 - 实体字段加
@TableLogic注解:只对当前实体生效。
配置之后,MP的默认行为是:
deleteById变成UPDATE ... SET deleted = 1 WHERE id = ? AND deleted = 0selectList、selectById等查询自动追加AND deleted = 0- 逻辑删除字段的值默认是0(未删除)和1(已删除),可以在全局配置里改。
看起来很方便,但有两个必须在设计阶段想清楚的坑。
第一个坑是唯一索引冲突。如果用户表给username加了唯一索引,逻辑删除老用户后再注册一个同名的用户,会插入失败,因为老用户那条记录还在表里,username仍然冲突。处理方案通常有两种:把逻辑删除字段也放进唯一索引里形成联合唯一索引,或者删除时给deleted字段写入主键ID、时间戳等唯一值,保证删除后不会和别人冲突。如果一直用固定值1,且唯一约束是(username, deleted),那么多次删除不同用户不会冲突,但同一条业务数据被删除后想再插入相同业务主键就要评估:它本身已经不存在有效记录,理论上可以插入。问题通常出在历史数据残留上,所以最稳妥的还是让逻辑删除标记值每次不同。
第二个坑是手写SQL不会自动带deleted = 0条件。MP内置方法会自动加,但你自己写的XML里的查询,MP没能力帮你改。所以在手写关联查询、多表查询时,记得自己把逻辑删除条件带上,否则会出现“已删除”的数据出现在列表里的情况。
6.2 乐观锁:拦截器没配上,@Version就是个摆设
并发更新是后端绕不开的话题。悲观锁用数据库for update,代价高;乐观锁则是基于版本号判断,适合读多写少的场景。
MP的乐观锁实现很轻量:
- 实体里加一个
@Version注解的字段,比如version。 - 在拦截器链路里加上
OptimisticLockerInnerInterceptor。 - 更新时,MP自动把
version带上,SQL变成:UPDATE user SET age = ?, version = ? WHERE id = ? AND version = ?
如果更新影响行数为0,说明version已经被别人改了,业务上可以提示“操作冲突,请刷新后重试”。
需要特别提醒的是:@Version注解本身不会生效,必须配合拦截器。很多人只在实体里加了注解,忘配拦截器,结果并发问题照样出,还以为MP失效了。
另外,更新时要保证version是从库里查出来的最新值:
User u = userMapper.selectById(1L); u.setAge(u.getAge() + 1); userMapper.updateById(u);如果自己 new 一个 User 然后随便 set version = 0,更新时where条件AND version = 0匹配不到记录,导致更新总是不成功。
乐观锁特别适合做“点赞数+1”“阅读量+1”这类轻量并发递增,但如果是高并发扣减库存这种强一致性场景,还是得用数据库行锁或分布式锁,这点要有数。
6.3 自动填充:createTime/updateTime不再手动赋值
很多表都有create_time和update_time字段。最原始的做法是每次插入、更新时手动 set 一下,写多了就烦,漏了就出问题。MP的字段自动填充可以解决。
步骤分两步:
第一步,实体字段指定填充时机:
@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;INSERT表示插入时填充,INSERT_UPDATE表示插入和更新时都填充。
第二步,实现MetaObjectHandler:
@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()); } }这里有一个小经验:strictInsertFill的第二个参数是实体属性名,不是数据库列名,写错了不会报错,但字段不会被填充。第三个参数需要和实体字段类型保持一致,类型不一致也会静默失败。我遇到过实体字段用LocalDateTime、填充值却传Date的情况,查了半天才发现是类型不匹配。
自动填充适合所有实体都有的审计字段。除了创建时间和更新时间,像create_by、update_by这类操作人字段,也可以从当前登录用户上下文里取出来填充,效果是一样的。
7. 代码生成器:从建表到代码,十分钟一整套
7.1 FastAutoGenerator的极简玩法
MP从3.5.1开始主推FastAutoGenerator,相比旧版AutoGenerator,配置方式更简洁,链式结构一目了然。
先看一眼极简配置:
FastAutoGenerator.create( "jdbc:mysql://localhost:3306/demo", "root", "root") .globalConfig(builder -> builder.author("zhangsan") .outputDir(System.getProperty("user.dir") + "/src/main/java")) .packageConfig(builder -> builder.parent("com.example") .moduleName("system")) .strategyConfig(builder -> builder.addInclude("sys_user", "sys_order") .addTablePrefix("sys_")) .execute();这个配置的意思是:连接demo数据库,把sys_user、sys_order两张表生成代码,去掉表名前缀sys_,输出到当前项目的src/main/java目录,包结构为com.example.system。
生成的代码包括:
entity:实体类,带@TableName、@TableId、Lombok注解。mapper:Mapper接口,继承BaseMapper。service:Service接口,继承IService。service.impl:Service实现类,继承ServiceImpl。controller:Controller,带基础的REST接口。
这一套下来,一张表的增删改查接口基本就齐了。
7.2 生成之后的第一轮检查清单
生成器不是万能的,生成完不要直接拿来用,按下面这个清单过一遍:
- 实体类是否加了
@TableName?如果表名和类名对不上,一定要检查,特别注意复数表名。 - 主键策略是否正确?生成器默认按表主键生成
@TableId,但自增主键需要设为IdType.AUTO,雪花ID策略则保持默认。 - 逻辑删除字段是否带
@TableLogic?如果全局配置了,可以不用在实体上标,但生成器可能没帮你生成deleted字段,需要手动补。 - 有版本号的表,实体里是否加
@Version? - Controller返回的是什么类型?如果公司有统一返回体,要把生成的
Result里的返回类型改过来。 - 生成器生成的
service和controller如果不符合项目规范,可以只保留entity和mapper,其他删掉自己写。
这个检查清单每次生成后都过一遍,能避免很多“生成的代码为什么跑不通”的问题。
7.3 生产项目里的补充配置
如果项目里用了逻辑删除字段、乐观锁字段、自动填充字段,可以给生成器加上额外策略:
.strategyConfig(builder -> builder.addInclude("sys_user") .addTablePrefix("sys_") .entityBuilder() .enableLombok() .logicDeleteColumnName("deleted") .versionColumnName("version") .addTableFills(new Column("create_time", FieldFill.INSERT)) .addTableFills(new Column("update_time", FieldFill.INSERT_UPDATE)))这样生成的实体类会自动带上@TableLogic、@Version和填充字段注解,省去手动补注解的步骤。
用代码生成器最大的体会是:建表、生成、微调、联调,一套流程下来非常顺。不过要强调的是,生成器解决的是“机械重复劳动”,不是“业务设计”。表结构设计得合理,生成代码才可能好用;表设计乱,生成器只会帮你快速生成一堆到处是坑的代码。
最后再分享一个实操中的小习惯。我一般会在项目的开发环境把SQL日志打开,观察MP内置方法实际生成的SQL语句,尤其是update和delete,看它where条件到底带了什么。很多人说MP“黑盒”、“不放心”,其实底层SQL完全可见,你只要看过一遍就明白它做了什么。理解了它生成SQL的规律,踩坑的概率会大幅下降,用起来也会踏实很多。