MyBatis-Plus
介绍 / Introduction¶
MyBatis-Plus(MP)是 MyBatis 的增强工具,只做增强不做改变:在不破坏原生 MyBatis 能力的前提下,提供 CRUD 封装、条件构造器、分页、代码生成等开箱即用功能。
Note
引入 MP 后仍可继续使用原生 MyBatis 的 XML 映射,两者可共存。
核心概念与优势 / Core Concept¶
| 维度 | 原生 MyBatis | MyBatis-Plus |
|---|---|---|
| 单表 CRUD | 每个方法都要写 XML/SQL | BaseMapper 内置,零 SQL |
| 条件拼接 | 手写 <if test> 动态 SQL |
Wrapper 链式构造 |
| 分页 | 手写 limit / 分页插件自配 |
内置 PaginationInnerInterceptor |
| 逻辑删除 | 手写 where deleted=0 |
注解/全局配置自动生效 |
| 自动填充 | 手动 set 时间字段 | MetaObjectHandler 自动填充 |
| 代码生成 | 手写或用 MBG | FastAutoGenerator 一键生成 |
| 扩展能力 | 无 | 乐观锁、多租户、防全表更新等 |
Tip
MP 的主战场是「单表增删改查」。复杂多表关联统计,仍推荐写 XML 或自定义 SQL。
依赖与配置 / Dependencies & Config¶
Maven 依赖¶
<!-- Spring Boot 3.x 使用 spring-boot3-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.7</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
application.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 XML 位置(纯注解方式可省略)
mapper-locations: classpath*:/mapper/**/*.xml
# 实体包别名,多模块/多包场景需显式指定
type-aliases-package: com.example.demo.entity
configuration:
# 开启下划线转驼峰(默认即 true)
map-underscore-to-camel-case: true
# 打印 SQL,仅开发环境使用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
banner: false
db-config:
# 全局主键策略(默认 ASSIGN_ID 雪花算法)
id-type: auto
# 逻辑删除字段名
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
Warning
log-impl: StdOutImpl 会把每条 SQL 打到控制台,性能开销较大,生产环境务必关闭。
启动类扫描¶
@SpringBootApplication
@MapperScan("com.example.demo.mapper") // 一次性扫描,免去逐个 @Mapper
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
实体注解 / Entity Annotations¶
@Data
@TableName("sys_user") // 表名映射(表名与类名不一致时)
public class User {
@TableId(type = IdType.AUTO) // 主键策略:数据库自增
private Long id;
@TableField("user_name") // 字段名映射
private String name;
@TableField(exist = false) // 非数据库字段
private String token;
@TableField(select = false) // 查询时不返回该列(如密码)
private String password;
@TableLogic // 逻辑删除标记字段
private Integer deleted;
@TableField(fill = FieldFill.INSERT) // 插入时自动填充
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE) // 插入和更新都填充
private LocalDateTime updateTime;
@Version // 乐观锁版本号
private Integer version;
}
主键策略 IdType¶
| 取值 | 说明 |
|---|---|
AUTO |
数据库自增,需表设置 AUTO_INCREMENT |
NONE |
不设置,跟随全局 id-type(默认 ASSIGN_ID) |
INPUT |
手动赋值,插入前自行 set |
ASSIGN_ID |
雪花算法生成 Long/String(默认) |
ASSIGN_UUID |
生成 UUID 字符串 |
BaseMapper 内置 CRUD / Built-in CRUD¶
// 增
userMapper.insert(user);
// 删
userMapper.deleteById(1L);
userMapper.deleteBatchIds(Arrays.asList(1L, 2L, 3L));
userMapper.delete(Wrappers.<User>lambdaQuery().eq(User::getAge, 18));
// 改
User u = new User();
u.setId(1L);
u.setName("张三");
userMapper.updateById(u); // 按主键更新非空字段
userMapper.update(u, Wrappers.<User>lambdaQuery().eq(User::getAge, 18));
// 查
User one = userMapper.selectById(1L);
List<User> list = userMapper.selectBatchIds(Arrays.asList(1L, 2L));
List<User> all = userMapper.selectList(null); // 传 null 表示无条件
User first = userMapper.selectOne(Wrappers.<User>lambdaQuery().eq(User::getName, "张三"));
Long count = userMapper.selectCount(null);
Tip
updateById 只更新实体中**非 null** 的字段;要把某字段置空,用 UpdateWrapper 显式 set。
条件构造器 / Wrapper¶
QueryWrapper(字符串列名)¶
QueryWrapper<User> qw = new QueryWrapper<>();
qw.select("id", "user_name", "age") // 指定返回列
.eq("age", 18)
.like("user_name", "张")
.orderByDesc("create_time");
List<User> users = userMapper.selectList(qw);
LambdaQueryWrapper(推荐,编译期安全)¶
LambdaQueryWrapper<User> lqw = Wrappers.lambdaQuery();
lqw.eq(User::getAge, 18)
.ne(User::getName, "李四")
.gt(User::getAge, 10)
.like(User::getName, "张")
.in(User::getId, Arrays.asList(1L, 2L, 3L))
.between(User::getAge, 18, 30)
.orderByAsc(User::getAge)
.orderByDesc(User::getCreateTime);
List<User> users = userMapper.selectList(lqw);
UpdateWrapper¶
UpdateWrapper<User> uw = new UpdateWrapper<>();
uw.set("age", 20) // set 字段
.set("user_name", null) // 允许置空
.eq("id", 1L);
userMapper.update(null, uw); // 实体传 null,仅按 wrapper 更新
常用方法速查¶
| 方法 | 作用 |
|---|---|
eq / ne |
等于 / 不等于 |
gt / ge |
大于 / 大于等于 |
lt / le |
小于 / 小于等于 |
like / notLike |
模糊包含 / 不包含 |
likeLeft / likeRight |
%值 / 值% |
in / notIn |
包含 / 排除集合 |
between / notBetween |
区间查询 |
isNull / isNotNull |
空 / 非空 |
groupBy / having |
分组 / 分组过滤 |
orderByAsc / orderByDesc |
升序 / 降序 |
and / or |
连接条件(注意加括号包裹) |
select |
指定查询列 |
Warning
使用 or() 时务必用括号包裹子条件,否则会与前后条件直接拼接造成语义错误:
qw.eq(...).and(w -> w.eq(...).or().eq(...))。
分页插件 / Pagination¶
配置拦截器¶
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件,指定数据库类型
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
使用¶
Page<User> page = new Page<>(1, 10); // 当前页, 每页条数
// page.setSearchCount(false); // 不需要 total 时可关闭 count
Page<User> result = userMapper.selectPage(page, Wrappers.<User>lambdaQuery().ge(User::getAge, 18));
result.getRecords(); // 当前页数据 List
result.getTotal(); // 总条数
result.getPages(); // 总页数
Note
PaginationInnerInterceptor 需放在其他拦截器之前添加,否则分页可能不生效。
自动填充 / Auto Fill¶
@Data
public class User {
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 严格填充:字段必须有 @TableField(fill=...) 才生效
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());
}
}
乐观锁 / Optimistic Locking¶
// 配置见上文:interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor())
User u = userMapper.selectById(1L);
u.setAge(20);
int rows = userMapper.updateById(u); // SQL: update ... set version=version+1 where id=? and version=?
// rows == 0 表示版本冲突,更新失败,需重试或提示
Warning
乐观锁仅在 updateById 与 update(entity, wrapper) 中生效,UpdateWrapper.set 手动赋值不受保护。
逻辑删除 / Logic Delete¶
方式一:全局配置(见 application.yml 的 logic-delete-field)。
方式二:实体字段加注解:
生效后:
// 删除 → UPDATE user SET deleted=1 WHERE id=? AND deleted=0
userMapper.deleteById(1L);
// 查询自动追加 WHERE deleted=0
userMapper.selectList(null);
代码生成器 / Code Generator¶
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
public class CodeGen {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/demo?serverTimezone=Asia/Shanghai";
FastAutoGenerator.create(url, "root", "root")
.globalConfig(b -> b
.author("磊哥")
.outputDir(System.getProperty("user.dir") + "/src/main/java")
.disableOpenDir()
.commentDate("yyyy-MM-dd"))
.packageConfig(b -> b
.parent("com.example.demo")
.entity("entity").mapper("mapper")
.service("service").serviceImpl("service.impl")
.controller("controller"))
.strategyConfig(b -> b
.addInclude("sys_user") // 指定生成的表
.addTablePrefix("sys_") // 去除表前缀
.entityBuilder().enableLombok().logicDeleteColumnName("deleted")
.controllerBuilder().enableRestStyle())
.execute();
}
}
多数据源 / Dynamic DataSource¶
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>4.3.1</version>
</dependency>
spring:
datasource:
dynamic:
primary: master # 默认数据源
strict: false # 未匹配到数据源时是否报错
datasource:
master:
url: jdbc:mysql://localhost:3306/db_master?serverTimezone=Asia/Shanghai
username: root
password: root
slave:
url: jdbc:mysql://localhost:3306/db_slave?serverTimezone=Asia/Shanghai
username: root
password: root
@Service
public class UserService {
@DS("slave") // 该方法走从库
public List<User> listFromSlave() {
return userMapper.selectList(null);
}
}
Tip
@DS 可作用于方法或类;未标注时按 primary 数据源执行。事务方法中也支持通过 @DS 切换。
常见坑与最佳实践 / Pitfalls & Best Practices¶
| 问题 | 现象 | 解决 |
|---|---|---|
| Lambda 缓存告警 | can not find lambda cache for this entity |
用 Wrappers.<User>lambdaQuery() 显式类型,避免 selectOne(wrapper) 匿名泛型 |
| 批量插入慢 | saveBatch 逐条提交 |
加 rewriteBatchedStatements=true,或配置 insertBatchSql |
| 更新置空失败 | updateById 不更新 null 字段 |
用 UpdateWrapper.set 显式赋值 |
| 分页不生效 | 返回全量数据 | 确认已加 PaginationInnerInterceptor |
| 字段未填充 | 时间字段为 null | 检查处理器 strict* 方法与 @TableField(fill) 是否配套 |
| 条件语义错乱 | or 拼接异常 |
用 and(w -> ...) / or(w -> ...) 括号包裹 |
| SQL 无法注入 | 未扫到 Mapper | 启动类加 @MapperScan |
// 批量插入优化:实体主键策略设为 INPUT 并使用 saveBatch
// 或直接重写 insertBatchSomeColumn 自定义批量方法
boolean ok = userService.saveBatch(userList, 500); // 每 500 条一批提交
Tip
- 单表用
BaseMapper,多表关联写 XML 或@Select注解。 - 生产环境禁止开启
StdOutImpl,日志交给logging.level控制。 - 复杂查询优先
LambdaQueryWrapper,列名重构时编译期即可发现问题。