MyBatis-Plus 实战:单表 CRUD 与分页插件零样板落地

在 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,不割裂、不绑架。

上一篇 Vite 实战:为什么它比 Webpack 快,以及怎么落地
下一篇 MySQL 主从复制与读写分离实战
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]