MybatisPlus
MybatisPlus
violet
MybatisPlus简介
在日常开发生活中,单表的 CRUD 功能代码重复度很高,也没什么难度,但是这部分代码的开发量往往却比较大,开发起来相当费时。
因此,目前企业中会使用一些组件来简化 CRUD 开发工作,而国内,使用最多的一个组件就是 MybatisPlus 。
官方网站如下:
MybatisPlus 不仅仅可以简化单表操作,而且还对 Mybatis 进行了增强。可以让我们能够简单高效地进行开发。
我们需要掌握的内容如下:
能利用 MybatisPlus 实现基本的 CRUD
使用条件构造器构建查询和更新语句
掌握 MybatisPlus 中常用的注解
会使用 MybatisPlus 处理枚举类、JSON 类型字段
会使用 MybatisPlus 实现分页
快速入门
创建一个 MybatisPlus 项目,并准备一些基础数据。

Ⅰ环境准备
① 打开 IDEA 导入 MybatisPlus 项目
② 打开 navicat 导入 mp.sql 文件

③ 配置项目 JDK 版本

④ 在 application.yml 文件中配置参数
spring: |
Ⅱ 快速开始
- 引入 MybatisPlus 依赖
- 定义 Mapper
① 引入依赖
MybatisPlus 提供了 starter,实现了自动 Mybatis 以及 MybatisPlus 的自动装配功能,坐标如下:
<!--mybatis-plus-boot-starter--> |
该依赖包含了对 mybatis 的自动装配,因此不需要 Mybatis 的 starter 。
<dependencies> |
② 定义 Mapper
为了简化单表 CRUD 开发,MybatisPlus 已经提供了一个基础的 BaseMapper 接口来实现单表 CRUD
/** |
因此我们只需要将自定义的 Mapper 实现 BaseMapper 接口,就无需自己实现 CRUD 。

代码如下:
package com.mybatisplus.mapper; |
③ 测试
新建测试类,进行 CURD 测试:
package com.mybatisplus; |
Ⅲ 常见注解
在上述的案例中我们发现仅仅引入了依赖,继承了 BaseMapper 就能够使用 MybatisPlus 。但是问题是:
MybatisPlus 是怎么知道我们要查询的是哪张表呢?表中有哪些字段呢?
其实在我们继承 BaseMapper 的时候,就给其指定了一个泛型:

泛型中的 UserEntity 就是与数据库对应的 PO 实体
MybatisPlus 就是根据 PO 实体的信息来推断出数据库表的信息,从而生成 SQL,默认情况是:
- MybatisPlus 会把 PO 实体的类名驼峰转下划线作为表名
- MybatisPlus 会把 PO 实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型
- MybatisPlus 会把名为 id 的字段作为主键
但在很多情况下,默认的实现与我们的实际场景不符,因此 MybatisPlus 提供了一些注解来让我们进行声明
① @TableName
- 描述:表名注解,标识实体类对应的表
- 使用位置:实体类
示例:
|
TableName 的属性:
| 属性 | 类型 | 必须指定 | 默认值 | 描述 |
|---|---|---|---|---|
| value | String | 否 | “” | 表名 |
| schema | String | 否 | “” | schema |
| keepGlobalPrefix | boolean | 否 | false | 是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时) |
| resultMap | String | 否 | “” | xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定) |
| autoResultMap | boolean | 否 | false | 是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入) |
| excludeProperty | String[] | 否 | {} | 需要排除的属性名 @since 3.3.1 |
② @TableId
- 描述:主键注解,标识实体类中的主键字段
- 使用位置:实体类的主键字段
示例:
|
TableId 的属性:
| 属性 | 类型 | 必须指定 | 默认值 | 描述 |
|---|---|---|---|---|
| value | String | 否 | “” | 表名 |
| type | Enum | 否 | IdType.NONE | 指定主键类型 |
IdType 支持的类型有:
| 值 | 描述 |
|---|---|
| AUTO | 数据库 ID 自增 |
| NONE | 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT) |
| INPUT | insert 前自行 set 主键值 |
| ASSIGN_ID | 分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法) |
| ASSIGN_UUID | 分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法) |
| ID_WORKER | 分布式全局唯一 ID 长整型类型(please use ASSIGN_ID) |
| UUID | 32 位 UUID 字符串(please use ASSIGN_UUID) |
| ID_WORKER_STR | 分布式全局唯一 ID 字符串类型(please use ASSIGN_ID) |
这里比较常见的有三种:
AUTO:利用数据库的id自增长INPUT:手动生成idASSIGN_ID:雪花算法生成Long类型的全局唯一id,这是默认的ID策略
③ @TableField
- 描述:普通字段的注解(与数据库字段匹配)
- 使用位置:
PO实体的普通字段
示例:
|
TableField 的属性:
| 属性 | 类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
| value | String | 否 | “” | 数据库字段值 |
| exist | boolean | 否 | true | 是否为数据库表字段 |
| condition | String | 否 | “” | 字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局 |
| update | String | 否 | “” | 字段 update set 部分注入,例如:当在version字段上注解update=”%s+1” 表示更新时会 set version=version+1 (该属性优先级高于 el 属性) |
| insertStrategy | Enum | 否 | FieldStrategy.DEFAULT | 字段验证策略之 insert: 当insert操作时,该字段拼接insert语句时的策略 |
| updateStrategy | Enum | 否 | FieldStrategy.DEFAULT | 字段验证策略之 update: 当更新操作时,该字段拼接set语句时的策略 |
| whereStrategy | Enum | 否 | FieldStrategy.DEFAULT | 字段验证策略之 where: 表示该字段在拼接where条件时的策略 |
| fill | Enum | 否 | FieldFill.DEFAULT | 字段自动填充策略 |
| select | boolean | 否 | true | 是否进行 select 查询 |
| keepGlobalFormat | boolean | 否 | false | 是否保持使用全局的 format 进行处理 |
| jdbcType | JdbcType | 否 | JdbcType.UNDEFINED | JDBC 类型 (该默认值不代表会按照该值生效) |
| typeHandler | TypeHander | 否 | 类型处理器 (该默认值不代表会按照该值生效) | |
| numericScale | String | 否 | “” | 指定小数点后保留的位数 |
Ⅳ 常见配置
mybatis-plus: |
- config-location :用于指定 Mybatis 配置文件的路径
- mapper-locations :配置 Mapper XML 文件所在的位置
- type-aliases-package :配置实体类所在的包名,MybatisPlus 会自动扫描并注册为别名
- global-config :全局配置
- configuration :自定义配置
- pagination :分页插件配置
大多数的配置都是有默认值的,因此我们都无需配置,但还是有一些没有默认值的,例如:
- 实体类的别名扫描包
- 全局 id 类型
mybatis-plus: |
需要注意的是 MybatisPlus 也支持手写 SQL 的,而 mapper 文件的读取地址可以自己配置:
mybatis-plus: |
可以看到默认值是 "classpath*:/mapper/**/*.xml",也就是说我们需要把 Mapper.xml 文件放在这个位置下被加载
例如:

核心功能
Ⅰ 条件构造器
除了新增以外,修改、删除、查询的 SQL 语句都需要指定 where 条件。因此 BaseMapper 中提供了了相关的方法除了支持 id 还支持更复杂的 where 条件。

参数中的 Wrapper 就是条件构造的抽象类,其下有很多默认实现,继承关系如下图所示:

Wrapper 的子类 AbstractWrapper 提供了 where 中所包含的所有条件构造方法:

而 QueryWrapper 在 AbstractWrapper 基础上拓展了一个 select 方法,用于指定查询字段:
public class QueryWrapper<T> extends AbstractWrapper<T, String, QueryWrapper<T>> implements Query<QueryWrapper<T>, T, String> { |
而 UpdateWrapper 则在 AbstractWrapper 基础上拓展了了一个 set 方法,用于指定 SQL 中的 SET 部分:
public class UpdateWrapper<T> extends AbstractWrapper<T, String, UpdateWrapper<T>> implements Update<UpdateWrapper<T>, String> { |
① QueryWrapper
无论是修改、更新、查询,都可以使用 QueryWrapper 来构建查询条件。
查询:
/** |
更新:
/** |
② UpdateWrapper
/** |
③ LambdaQueryWrapper
其中一种办法是基于变量的 gettter 方法结合反射技术。因此我们只要将条件对应的字段的 gettter 方法传递给 MybatisPlus ,它就能计算出对应的变量名了。而传递方法可以使用 JDK8 中的方法引用和 Lambda 表达式。 因此 MybatisPlus 又提供了一套基于 Lambda 的 Wrapper ,包含两个:
- LambdaQueryWrapper
- LambdaUpdateWrapper
分别对应 QueryWrapper 和 UpdateWrappe
/** |
Ⅱ 自定义 SQL
MybatisPlus 提供了自定义 SQL 功能,可以让我们先利用 Wrapper 来构建查询条件,然后再结合 Mapper.xml 来编写 SQL
① 基本用法
先构建查询条件:
/** |
再在 UserMapper 中定义 SQL:
public interface UserMapper extends BaseMapper<UserEntity> { |
② 多表关联
理论上来说 MybatisPlus 是单表操作不支持多表关联的,不过我们可以利用 Wrapper 中自定义条件结合自定义 SQL 来实现多表查询的效果。
例如,我们要查出所有收获地址在北京并且用户 id 在 1、2、4 的用户
如果是基于 mybatis 实现 SQL:
<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User"> |
我们也可以使用自定义 SQL 结合 Wrapper :
先构建查询条件:
|
再在 UserMapper 中自定义 SQL 方法:
|
当然也可以在 UserMapper.xml 中写 SQL:
<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User"> |
Ⅲ Service 接口
通用接口为 Iservice ,默认实现为 ServiceImpl ,其中封装方法为一下几类:
save:新增remove:删除update:更新get:查询单个结果list:查询集合结果count:计数page:分页查询
① CRUD
public interface IService<T> { |
新增:
save是新增单个元素saveBatch是批量新增saveOrUpdate是根据id判断,如果数据存在就更新,不存在则新增saveOrUpdateBatch是批量的新增或修改
删除:
removeById:根据id删除removeByIds:根据id批量删除removeByMap:根据Map中的键值对为条件删除remove(Wrapper<T>):根据Wrapper条件删除~~removeBatchByIds~~:暂不支持
修改:
updateById:根据id修改update(Wrapper<T>):根据UpdateWrapper修改,Wrapper中包含set和where部分update(T,Wrapper<T>):按照T内的数据修改与Wrapper匹配到的数据updateBatchById:根据id批量修改
Get:
getById:根据id查询1条数据getOne(Wrapper<T>):根据Wrapper查询1条数据getBaseMapper:获取Service内的BaseMapper实现,某些时候需要直接调用Mapper内的自定义SQL时可以用这个方法获取到Mapper
List:
listByIds:根据id批量查询list(Wrapper<T>):根据Wrapper条件查询多条数据list():查询所有
Count:
count():统计所有数量count(Wrapper<T>):统计符合Wrapper条件的数据数量
getBaseMapper:
- 当我们在service中要调用Mapper中自定义SQL时,就必须获取service对应的Mapper,就可以通过这个方法
② 基本用法
首先,定义 IUserService ,继承 IService:
public interface IUserService extends IService<User> { |
然后,编写 UserServiceImpl 类,继承 ServiceImpl ,实现 UserService:
|
项目结构如下:

接下来,我们快速实现下面4个接口:
| 编号 | 接口 | 请求方式 | 请求路径 | 请求参数 | 返回值 |
|---|---|---|---|---|---|
| 1 | 新增用户 | POST | /users | 用户表单实体 | 无 |
| 2 | 删除用户 | DELETE | /users/{id} | 用户id | 无 |
| 3 | 根据id查询用户 | GET | /users/{id} | 用户id | 用户VO |
| 4 | 根据id批量查询 | GET | /users | 用户id集合 | 用户VO集合 |
为了方便测试,引入 swagger-knife4j:
依赖:
<!--swagger--> |
yml 文件中 swagger 配置
knife4j: |
UserController:
/** |
UserVo:返回用户对象给前端
/** |
IUserService:
/** |
UserServiceImpl:
/** |
UserMapper:
/** |
可以看到上述接口,通过引入在 Controller 层就可以完成业务接口,但是为了能够更加规范,所以对于复杂的业务,我们任然会选择将业务功能放到 Service 层去进行业务实现。
③ Lambda
IService 中还提供了 Lambda 方法来简化我们复杂的查询和更新功能。
案例一:实现一个根据复杂条件查询用户的接口,查询条件如下:
- name :用户关键名称,可以为空
- status :用户状态,可以为空
- minBalance :最小余额,可以为空
- maxBalance :最大余额,可以为空
首先定义查询实体,UserQuery :
import io.swagger.annotations.ApiModel; |
接下来,在 UserController 中定义查询方法:
|
在组织查询的时候,我们加入 username != null 这样的参数,表示只有当这个条件成立的时候才会添加到这个查询条件里,类似与 Mapper.xml 文件中的 <if>标签。这样就实现了动态查询效果。
Service 中对LambdaQueryWrapper和LambdaUpdateWrapper的用法进一步做了简化,不需要使用 new 来创建 Wrapper ,而是直接调用 lambdaQuery 和 lambdaUpdate 方法:
lambdaQuery :
|
可以发现 lambdaQuery 方法中除了可以构建条件,还需要在链式编程的最后添加一个list(),这是在告诉 MP 我们的调用结果需要是一个 list 集合。这里不仅可以用list(),可选的方法有:
.one():最多1个结果.list():返回集合结果.count():返回计数结果
lambdaUpdate :
|
④ 批量新增
逐条插入:
|
- 耗时长
- 速度慢

批量插入:
|
- 耗时较短
- 速度较快

可以看到批处理后,比逐条新增的效率提高了10倍左右,性能还是不错的。
不过我们查看 MybatisPlus 源码:
|
可以发现其实 MybatisPlus 的批处理是基于 PrepareStatement 的预编译模式,然后批量提交,最终在数据库执行时还是会有多条 insert 语句,逐条插入数据。
Preparing: INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ? ) |
而如果想要得到最佳性能,最好将多条 SQL 合并为一条:
INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) |
该怎么做呢?
MySQL 的客户端连接参数中,有一个 rewriteBatchedStatements ,顾名思义,就是重写批处理 statement 语句。
修改 application.yml 在 jdbc 的 url 后面添加参数&rewriteBatchedStatements=true :
spring: |
再次测试插入10万条数据,可以发现速度有明显的提升:

在ClientPreparedStatement的executeBatchInternal中,有判断rewriteBatchedStatements值是否为true并重写SQL的功能:
最终,SQL 被重写了:

扩展功能
Ⅰ代码生成
在使用 MybatisPlus 以后,基础的 Mapper 、Servce 、PO 代码相对固定,重复编写也比较麻烦。
推荐利用 MyBatisX 插件进行代码生成:
① 插件安装

② 使用
IDEA 集成数据库:


选择生成数据:

- 点击右侧 Database 展开连接数据库
- 选择所需生成业务表,右键
- 点击 MybatisX-Generator 打开代码生成窗口
配置代码生成:


生成结果:

Ⅱ 静态工具
有的时候 Service 之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus 提供了一个静态工具类:Db ,其中的一些静态方法与 IService 中方法签名基本一致,也可以帮助我们实现 CRUD 功能:
/** |
示例:
|
需求:改造根据 id 查询用户接口,查询用户的同时返回用户收获地址列表:
UserVO:
/** |
添加一个地址列表的属性
UserController:
/** |
需求:根据id批量查询用户,并查询出用户对应的所有地址
/** |
Ⅲ 逻辑删除
对于一些比较重要的数据,我们往往会采用逻辑删除的方式,而不是直接将其冲数据库中删除。
- 在表中添加一个字段标记数据是否被删除
- 当删除数据时把标记置为true
- 查询时过滤掉标记为true的数据
MybatisPlus 就添加了对逻辑删除的支持:
注意,只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。
例如,给 adress 表添加逻辑删除字段:
alter table address add deleted bit default b'0' null comment '逻辑删除'; |
AdressEntity:
/** |
application.yml:
mybatis-plus: |
方法与普通的方法一样,但是底层的逻辑已经改变了:

查询:
|
会发现 id 为59的确实没有查询出来,而且 SQL 中也对逻辑删除字段做了判断:

综上, 开启了逻辑删除功能以后,我们就可以像普通删除一样做CRUD,基本不用考虑代码逻辑问题。还是非常方便的。
注意: 逻辑删除本身也有自己的问题,比如:
- 会导致数据库表垃圾数据越来越多,从而影响查询效率
- SQL中全都需要对逻辑删除字段做判断,影响查询效率
Ⅳ 通用枚举
UserEntity 中有一个用户状态

像这种字段,我们一般会定义一个枚举,做业务判断的时候,就可以直接基于枚举比较,但是我们数据库采用的是 int 型,对应的 PO 也是一个 Integer 。因此业务操作时必须手动转换枚举和 Integer 。
但是 MybatisPlus 提供了一个处理枚举类型的类型转换器,可以帮我们把枚举类型和数据库类型自动转换。
① 定义枚举
定义一个用户状态枚举:

代码如下:
|
然后将 UserEntity 类中的 status 字段改为 UserStatus 类型:

当然,要让 MybatisPlus 处理枚举与数据库自动类型转换,我们必须要告诉 MybatisPlus ,枚举中的哪个字段值作为数据库值。
MybatisPlus 提供了 @EnumValue 来标记枚举属性
② 配置枚举处理器
在 application.yml 中添加配置:
mybatis-plus: |
测试:
/** |
最终,查询出的User类的status字段会是枚举类型:

同时,为了使页面查询结果也是枚举格式,我们需要修改UserVO中的status属性
并且,在UserStatus枚举中通过@JsonValue注解标记JSON序列化时展示的字段:

最后,在页面查询,结果如下:

Ⅴ JSON 类型处理器
数据库的user表中有一个info字段,是 JSON 类型:

格式:
{"age": 20, "intro": "佛系青年", "gender": "male"} |
而目前UserEntity实体类中却是String类型:

这样一来,我们要读取info中的属性时就非常不方便。如果要方便获取,info的类型最好是一个Map或者实体类。
而一旦我们把info改为对象类型,就需要在写入数据库时手动转为String,再读取数据库时,手动转换为对象,这会非常麻烦。
因此MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler处理器。
① 定义实体

代码如下:
/** |
② 使用类型处理器
将 UserEntity 类的 info 字段修改为 UserInfo 类型,并声明类型处理器:

插件功能
MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:
PaginationInnerInterceptor:自动分页TenantLineInnerInterceptor:多租户DynamicTableNameInnerInterceptor:动态表名OptimisticLockerInnerInterceptor:乐观锁IllegalSQLInnerInterceptor:sql 性能规范BlockAttackInnerInterceptor:防止全表更新与删除
注意: 使用多个分页插件的时候需要注意插件定义顺序,建议使用顺序如下:
- 多租户,动态表名
- 分页,乐观锁
- sql 性能规范,防止全表更新与删除
Ⅰ分页插件
在未引入分页插件的情况下,MybatisPlus是不支持分页功能的,IService和BaseMapper中的分页方法都无法正常起效。 所以,我们必须配置分页插件。
① 配置分页插件
在 config 下配置分页插件

代码如下:
/** |
② 分页 API
/** |

这里用到了分页参数,Page ,即可以支持分页参数,也可以支持排序参数。常见的 API 如下:
int pageNo = 1, pageSize = 5; |
Ⅱ 通用分页实体
现在要实现一个用户分页查询的接口,接口规范如下:
| 参数 | 说明 |
|---|---|
| 请求方式 | GET |
| 请求路径 | /users/page |
| 请求参数 | {
"pageNo": 1,
"pageSize": 5,
"sortBy": "balance",
"isAsc": false,
"name": "o",
"status": 1
}
|
| 返回值 | {
"total": 100006,
"pages": 50003,
"list": [
{
"id": 1685100878975279298,
"username": "user_9****",
"info": {
"age": 24,
"intro": "英文老师",
"gender": "female"
},
"status": "正常",
"balance": 2000
}
]
}
|
| 特殊说明 | 如果排序字段为空,默认按照更新时间排序;如果排序字段不为空,则按照排序字段排序。 |
这里需要定义3个实体:
UserQuery:分页查询条件的实体,包含分页、排序参数、过滤条件PageDTO:分页结果实体,包含总条数、总页数、当前页数据UserVO:用户页面视图实体
① 实体
UserQuery:
|
PageQuery:
|
PageQuery是前端提交的查询参数,一般包含四个属性:
pageNo:页码pageSize:每页数据条数sortBy:排序字段isAsc:是否升序
PageDTO:
|
② 开发接口
/** |



