news 2026/9/10 9:27:45

MyBatis-Plus速成实战:从CRUD到分页、逻辑删除与乐观锁

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MyBatis-Plus速成实战:从CRUD到分页、逻辑删除与乐观锁

我最早接触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执行前做两件事:

  1. 改写原SQL,生成一条COUNT查询语句,用于统计总条数。
  2. 在原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 = 0
  • selectListselectById等查询自动追加AND deleted = 0
  • 逻辑删除字段的值默认是0(未删除)和1(已删除),可以在全局配置里改。

看起来很方便,但有两个必须在设计阶段想清楚的坑。

第一个坑是唯一索引冲突。如果用户表给username加了唯一索引,逻辑删除老用户后再注册一个同名的用户,会插入失败,因为老用户那条记录还在表里,username仍然冲突。处理方案通常有两种:把逻辑删除字段也放进唯一索引里形成联合唯一索引,或者删除时给deleted字段写入主键ID、时间戳等唯一值,保证删除后不会和别人冲突。如果一直用固定值1,且唯一约束是(username, deleted),那么多次删除不同用户不会冲突,但同一条业务数据被删除后想再插入相同业务主键就要评估:它本身已经不存在有效记录,理论上可以插入。问题通常出在历史数据残留上,所以最稳妥的还是让逻辑删除标记值每次不同。

第二个坑是手写SQL不会自动带deleted = 0条件。MP内置方法会自动加,但你自己写的XML里的查询,MP没能力帮你改。所以在手写关联查询、多表查询时,记得自己把逻辑删除条件带上,否则会出现“已删除”的数据出现在列表里的情况。

6.2 乐观锁:拦截器没配上,@Version就是个摆设

并发更新是后端绕不开的话题。悲观锁用数据库for update,代价高;乐观锁则是基于版本号判断,适合读多写少的场景。

MP的乐观锁实现很轻量:

  1. 实体里加一个@Version注解的字段,比如version
  2. 在拦截器链路里加上OptimisticLockerInnerInterceptor
  3. 更新时,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_timeupdate_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_byupdate_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_usersys_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里的返回类型改过来。
  • 生成器生成的servicecontroller如果不符合项目规范,可以只保留entitymapper,其他删掉自己写。

这个检查清单每次生成后都过一遍,能避免很多“生成的代码为什么跑不通”的问题。

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的规律,踩坑的概率会大幅下降,用起来也会踏实很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 9:27:38

ECC 构建修复指南:用 /build-fix 分步解决 TypeScript 与构建错误

ECC 构建修复指南&#xff1a;用 /build-fix 分步解决 TypeScript 与构建错误 【免费下载链接】ECC The agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and…

作者头像 李华
网站建设 2026/9/10 9:26:27

数字滤波器分析---频率响应

数字滤波器分析---频率响应 幅值、相位、冲激和阶跃响应、相位和群延迟、零极点分析。 分析滤波器的频域和时域响应。可视化复平面中的滤波器极点和零点。 幅频响应 & 相位响应 这两个合起来叫频率响应&#xff0c;描述线性系统对不同频率正弦输入的稳态作用。 幅频响…

作者头像 李华
网站建设 2026/9/10 9:23:41

context-mode:轻量级本地上下文检索协议解析

1. “context-mode”到底是什么&#xff1f;别被术语唬住&#xff0c;它本质是智能体与数据交互的“上下文调度协议” 最近在多个技术社区和开发者群聊里&#xff0c;“context-mode”这个词突然高频出现&#xff0c;尤其和MCP、SQLite、FTS5、BM25这些词绑在一起。很多人第一…

作者头像 李华
网站建设 2026/9/10 9:22:04

js随机数设置概率

有时候需要产生随机数。并让这些随机数出现以概率的方式出现 下面举个例子&#xff1a;随机产生1-8的整数&#xff0c;希望 1的概率是50% 2的概率是10%3的概率是10%4的概率是10%5的概率是5%6的概率是5%7的概率是5%8的概率是5%想法&#xff1a;先随机1-100的随机整数 然后出现的…

作者头像 李华
网站建设 2026/9/10 9:21:18

STM8S105与AFE4300的VirtualSPI链路:人体阻抗测量全流程验证

简介&#xff1a;这是一份基于STM8S105微控制器与AFE4300模拟前端的完整工程源码&#xff0c;用于人体成分分析场景中的阻抗测量&#xff0c;面向嵌入式医疗电子开发者及相关专业学习者。工程采用虚拟SPI方式与AFE4300通信&#xff0c;涵盖初始化、测量时序、数据读取等关键环节…

作者头像 李华