在 Java 后端项目里,单表 CRUD 往往占据持久层 60% 以上的代码量。MyBatis-Plus 作为 MyBatis 的增强工具,通过 BaseMapper 与分页插件消灭了绝大多数样板 SQL:你只需写实体类和 Mapper 接口,增删改查、分页、条件构造全自动生成。本文用一份可运行的订单服务示例,带你把 MyBatis-Plus 从接入到生产最佳实践一次落地。
一、为什么还需要 MyBatis-Plus
原生 MyBatis 灵活但啰嗦:每张表都要手写 insert/selectById/update/delete 的 XML 与 SQL。JPA 自动化程度高,却在对复杂多表关联、精细 SQL 调优时束手束脚。MyBatis-Plus 的定位是”只增强、不替换”——它保留 MyBatis 的全部能力,只在单表场景上做加法:
- BaseMapper:17 个通用 CRUD 方法开箱即用,无需任何 XML。
- 分页插件:两行配置即支持物理分页,自动在 SQL 后追加
LIMIT。 - 条件构造器:用 Java 代码拼 WHERE,避免字符串拼接出错。
- 逻辑删除 / 自动填充:通过注解与全局配置统一处理通用字段。
对绝大多数业务表而言,MyBatis-Plus 能把持久层代码量砍掉一半以上,同时保留你在需要时回退到原生 XML 的能力。
二、快速接入:依赖与配置
Spring Boot 3 项目请使用 mybatis-plus-spring-boot3-starter(Spring Boot 2 用旧版 mybatis-plus-boot-starter)。下面以稳定的 3.5.x 为例:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.7</version>
</dependency>
配置文件里集中声明全局行为——主键策略、逻辑删除字段、下划线转驼峰。一次写好,全项目生效:
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
global-config:
db-config:
id-type: auto # 主键自增
logic-delete-field: deleted
logic-delete-value: 1 # 已删除
logic-not-delete-value: 0 # 未删除
configuration:
map-underscore-to-camel-case: true
如果你正从 Spring Boot 2 升级到 3(详见本站《Spring Boot 3 升级踩坑实录》),务必同步把 starter 换成 spring-boot3 版本,否则会因 Jakarta 命名空间不兼容而启动失败。
三、BaseMapper 与 CRUD 零样板
实体类用 @TableName 关联表,@TableId 标主键;需要逻辑删除的字段加 @TableLogic。Mapper 接口只需继承 BaseMapper<T>,不写任何方法:
@Data
@TableName("t_order")
public class Order {
@TableId(type = IdType.AUTO)
private Long id;
private String orderNo;
private Long userId;
private BigDecimal amount;
private Integer status;
@TableLogic
private Integer deleted;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
}
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}
有了它,下面这些方法直接可用,连 XML 都不用建:insert / deleteById / updateById / selectById / selectList / selectCount / selectPage / saveBatch 等。批量插入推荐用 saveBatch,它在事务内分批提交,比循环单条 insert 快一个数量级——这点与《Spring 事务传播机制深度解析》里讨论的批量写入事务边界要配合理解。
四、分页插件配置与分页查询
分页是高频需求,也是最容易踩坑的地方:漏配拦截器会导致分页失效,变成全表扫描。先注入 MybatisPlusInterceptor 并加上 PaginationInnerInterceptor:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(
new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
查询时构造 Page<T> 对象传入 selectPage,MyBatis-Plus 会自动改写 SQL 并回填总数:
Page<Order> page = new Page<>(1, 10); // 第 1 页,每页 10 条
QueryWrapper<Order> qw = new QueryWrapper<>();
qw.eq("status", 1).orderByDesc("create_time");
Page<Order> result = orderMapper.selectPage(page, qw);
List<Order> records = result.getRecords(); // 当前页数据
long total = result.getTotal(); // 总条数(自动 count)
五、条件构造器 QueryWrapper 实战
复杂查询用 QueryWrapper。强烈建议用 lambda 表达式版本,字段名以方法引用形式出现,重构实体时编译器会替你报错,而字符串写法改字段名毫无提示:
orderMapper.selectList(new QueryWrapper<Order>().lambda()
.eq(Order::getUserId, 1001)
.gt(Order::getAmount, new BigDecimal("100"))
.in(Order::getStatus, 1, 2)
.orderByDesc(Order::getCreateTime));
更新同样有 UpdateWrapper,可附带 set 指定更新字段;如果只更新非空字段,配合 @TableField(strategy = FieldStrategy.NOT_NULL) 的实体即可避免把字段更新成 NULL。
六、自动填充与逻辑删除
create_time、update_time 这类字段适合用自动填充:实现 MetaObjectHandler,在插入/更新时统一写入时间,业务代码零感知。
@Component
public class MetaHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
strictInsertFill(metaObject, "createTime",
LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
strictUpdateFill(metaObject, "updateTime",
LocalDateTime.class, LocalDateTime.now());
}
}
逻辑删除则让 deleteById 自动变成 UPDATE ... SET deleted=1,查询自动追加 deleted=0 条件。注意:逻辑删除字段上若有唯一索引,删除后会产生冲突,需把业务唯一键与 deleted 组合成联合唯一索引。
七、代码生成器:把样板一次生成
新建表过多时,用 FastAutoGenerator 一键产出 Entity、Mapper、Service、Controller:
FastAutoGenerator.create("jdbc:mysql://localhost:3306/demo", "root", "pwd")
.globalConfig(b -> b.author("dev").outputDir("src/main/java"))
.packageConfig(b -> b.parent("com.demo"))
.strategyConfig(b -> b.addInclude("t_order")
.entityBuilder().enableLombok()
.controllerBuilder().enableRestStyle())
.execute();
八、生产最佳实践与避坑
上述坑点在中小团队里尤其高发,根源往往是”以为开箱即用、却忘了配置拦截器与全局策略”。分页失效最隐蔽:开发环境数据量小,LIMIT 缺失看不出异常,一到生产大表就变成全表扫描、拖垮整个数据库。逻辑删除同理,没接全局配置时 deleteById 会真删数据,事后才发现无法恢复。把下面这张表当作上线前 checklist 逐项核对,能挡掉绝大多数线上事故。
结合真实项目,整理一张高频场景对照表,照着做能避开大部分坑:
| 场景 | 推荐做法 | 常见坑 |
|---|---|---|
| 分页 | 注入 MybatisPlusInterceptor + PaginationInnerInterceptor | 漏配拦截器导致不分页、触发全表扫描 |
| 条件构造 | 优先用 lambda 表达式写法 | 字符串字段名拼错、改字段名不报错 |
| 逻辑删除 | @TableLogic + 全局配置 | 手写 delete 绕过、唯一索引冲突 |
| 大批量写入 | saveBatch 分批提交 | 循环单条 insert,性能差且连接占用久 |
| SQL 可观测 | 接 p6spy 或日志框架输出完整 SQL | 只看到 ? 占位符,排查慢查询无从下手 |
SQL 执行细节可通过日志框架观察,推荐按《Java 日志体系实战:SLF4J + Logback》把 MyBatis 的 SQL 日志接到统一日志管道,便于线上排查慢查询。另外,saveBatch 批量操作若跨多个事务边界,记得回到事务传播规则(Spring 事务传播机制)确认 REQUIRED / REQUIRES_NEW 的取舍。
九、与原生 MyBatis、JPA 的取舍边界
MyBatis-Plus 不是银弹。当业务以单表操作为主、又偶尔需要手写复杂 SQL 时,它最顺手;一旦遇到多表 JOIN 报表、存储过程调用、或需要对每一条 SQL 做精细 hint 优化的场景,就应当退回原生 XML——MyBatis-Plus 完全兼容,你只需在 Mapper 中声明方法、在 XML 里提供实现,两者可共存于同一项目。与 JPA 相比,MyBatis-Plus 把 SQL 控制权留在开发者手里,学习曲线更低、对 DBA 更友好,代价是关联映射等高级 ORM 特性要自己实现。
选型建议很直接:新项目若以单表为主、需要快速交付,直接上 MyBatis-Plus;存量 MyBatis 项目想减负,渐进式引入 BaseMapper 即可,无需推倒重来。无论走哪条路,分页拦截器、逻辑删除、SQL 日志这三项都是上线前必须确认到位的”基础设施”——漏掉任意一项,都会在第一个线上问题出现时让你措手不及。
总结:MyBatis-Plus 用极低的接入成本,消除了单表 CRUD 的重复劳动。把 BaseMapper、分页插件、条件构造器、逻辑删除四件套用熟,你的持久层就能既简洁又可控——需要极致 SQL 时随时回到原生 XML,不割裂、不绑架。




