前言
MyBatis 是 Java 生态中经久不衰的持久层框架,但原生 MyBatis 需要手写大量模板 XML。MyBatis-Plus(简称 MP)在此基础上做了增强:内置通用 CRUD、分页插件、逻辑删除、自动填充、乐观锁、Auto DDL……几乎解决了持久层开发中 80% 的重复劳动。
本文结合 personal-blog-backend 项目,讲解:
- BOM 依赖引入与 Spring Boot 3 的兼容性
- Entity 注解全解(@TableId、@TableField、@TableLogic、@Version)
- 分页插件 + 乐观锁 + 防全表删除插件配置
- 字段自动填充(审计字段:createTime/updateBy 等)
- Auto DDL:用代码替代 Flyway/Liquibase 管理建表脚本
本文所有代码均来自开源项目 personal-blog-backend,基于 Spring Boot 3.5 + Java 21 + MyBatis-Plus 3.5.14 构建,欢迎 Star。
一、依赖引入:使用 BOM 统一版本
MyBatis-Plus 3.5.x 提供了 BOM,在 Spring Boot 3 项目中通过 BOM 引入:
<!-- 根 pom.xml 的 dependencyManagement -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-bom</artifactId>
<version>3.5.14</version>
<type>pom</type>
<scope>import</scope>
</dependency>
业务模块只需声明 starter:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
注意:Spring Boot 3 必须使用 mybatis-plus-spring-boot3-starter(专为 Jakarta EE 适配),而不是旧版的 mybatis-plus-boot-starter,两者并存会 Class 冲突。
二、Entity 注解详解
以项目中的 ArticleEntity 为例:
// blog-module-article/.../domain/entity/ArticleEntity.java
@Data
@TableName("art_article")
public class ArticleEntity {
@TableId(type = IdType.ASSIGN_ID) // 雪花算法生成 ID
private Long id;
private String title;
private String content;
private Integer status;
// 向量字段,查询时默认跳过
@TableField(select = false)
private String embedding;
@Version // 乐观锁
private Integer version;
@TableField(fill = FieldFill.INSERT) // INSERT 时自动填充
private Long createBy;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) // INSERT/UPDATE 都填充
private Long updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic // 逻辑删除
private Integer isDeleted;
}
注解速查表
| 注解 | 作用 |
|---|---|
| @TableName | 指定数据库表名 |
| @TableId | 主键字段,IdType.ASSIGN_ID=雪花 |
| @TableField(select=false) | 查询时忽略,适合 BLOB/VECTOR 大字段 |
| @TableField(fill=...) | 自动填充触发时机 |
| @Version | 乐观锁版本字段 |
| @TableLogic | 逻辑删除,查询自动加 WHERE is_deleted=0 |
三、插件配置:分页 + 乐观锁 + 防全表删除
// blog-application/.../config/MybatisPlusConfig.java
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. 防全表更新/删除(放最前):UPDATE/DELETE 没有 WHERE 则抛异常
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
// 2. 分页插件
PaginationInnerInterceptor paginationInterceptor =
new PaginationInnerInterceptor(DbType.MYSQL);
paginationInterceptor.setMaxLimit(500L); // 单页上限,防恶意大量查询
paginationInterceptor.setOverflow(false); // 超出总页数返回空
paginationInterceptor.setOptimizeJoin(true); // 优化 COUNT SQL 中的 JOIN
interceptor.addInnerInterceptor(paginationInterceptor);
// 3. 乐观锁(放最后)
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
分页使用示例:
Page<ArticleEntity> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<ArticleEntity> wrapper = new LambdaQueryWrapper<ArticleEntity>()
.eq(ArticleEntity::getStatus, 2)
.orderByDesc(ArticleEntity::getPublishTime);
Page<ArticleEntity> result = articleMapper.selectPage(page, wrapper);
// result.getRecords() 当前页数据
// result.getTotal() 总条数(MP 自动执行 COUNT)
// result.getPages() 总页数
乐观锁使用(必须先查再改):
// ✅ 正确:先查,再更新(version 会被自动附加到 WHERE 条件)
ArticleEntity article = articleMapper.selectById(id); // version=3
article.setTitle("新标题");
int rows = articleMapper.updateById(article); // WHERE id=? AND version=3
if (rows == 0) throw new BusinessException("并发冲突,请重试");
// ❌ 错误:直接 new,version=null,乐观锁失效
ArticleEntity article = new ArticleEntity();
article.setId(1L);
articleMapper.updateById(article); // 不会检查 version
四、字段自动填充(审计字段)
// blog-application/.../handler/MyMetaObjectHandler.java
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
Long userId = getCurrentUserId();
if (userId != null) {
this.strictInsertFill(metaObject, "createBy", () -> userId, Long.class);
this.strictInsertFill(metaObject, "updateBy", () -> userId, Long.class);
}
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
Long userId = getCurrentUserId();
if (userId != null) {
this.strictUpdateFill(metaObject, "updateBy", () -> userId, Long.class);
}
}
private Long getCurrentUserId() {
try {
return SecurityUtils.getCurrentUserId(); // 从 Spring Security 上下文获取
} catch (Exception e) {
log.debug("无法获取当前用户ID(定时任务/初始化场景): {}", e.getMessage());
return null;
}
}
}
注册 Handler:
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MyMetaObjectHandler();
}
五、Auto DDL
MyBatis-Plus 3.5.3+ 内置 Auto DDL:应用启动时自动执行 SQL 脚本,并在 ddl_history 表记录版本,避免重复执行:
@Component
public class MysqlDdl implements IDdl {
@Override
public List<String> getSqlFiles() {
return Arrays.asList(
"db/schema/01-system-schema.sql",
"db/schema/02-article-schema.sql",
"db/data/01-init-data.sql"
);
}
}
六、踩坑记录
1. Spring Boot 3 必须用 spring-boot3-starter:旧 starter 会报 ClassNotFoundException: javax.persistence.Table
2. 乐观锁不生效:必须先 selectById 拿到含 version 的实体,不能直接 new Entity 再 updateById
3. strictInsertFill vs setFieldValByName:使用 strict 系列,仅在字段值为 null 时填充,避免覆盖业务层手动设置的值
4. @TableField(select=false) 的范围:对普通 selectById/selectList 生效,对 selectMaps 或自定义 XML 不生效
总结
| 特性 | 收益 |
|---|---|
| 通用 CRUD | 无需写 XML,单表操作全覆盖 |
| 分页插件 | 自动 COUNT + LIMIT,防大页攻击 |
| 乐观锁 | 并发安全,无侵入 |
| 防全表误删 | 生产环境兜底保障 |
| 自动填充 | 无需手动赋值审计字段 |
| Auto DDL | 替代 Flyway 轻量管理建表 |
参考资料
- MyBatis-Plus 官方文档
- MyBatis-Plus 分页插件
- MyBatis-Plus 自动填充字段
- MyBatis-Plus 自动维护 DDL
- 项目源码:personal-blog-backend
专栏:Spring Boot 3 整合实战:主流技术栈深度整合 源码:github.com/liusxml/per…