Mybatis-Plus
MyBatis 最佳搭档,只做增强不做改变,为简化开发、提高效率而生。

快速入门
入门案例
相较于单体项目,这需要两步就可以使用MybatisPlus
-
引入依赖
MyBatisPlus官方提供了starter,其中集成了Mybatis和MybatisPlus的所有功能,并且实现了自动装配效果。因此我们可以用MybatisPlus的starter代替Mybatis的starter
<!--M{ybatisPlus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> -
定义Mapper
自定义的Mapper继承MybatisPlus提供的BaseMapper接口。注意这里的泛型要传入自己操作的实体类。
public interface UserMapper extends BaseMapper<User> { }
然后就可以直接用BaseMapper定义好的方法去实现增删改查了,不用写mapper.xml文件了。相当于继承已经写好的了,瞬间变成富二代,不用那么努力了。
userMapper.insert(user);
userMapper.selectById(5L);
userMapper.updateById(user); //没有传值的字段可以保持不变
userMapper.selectBatchIds(List.of(1L, 2L)); //idea也会有提示的,很方便
常见注解
mp是如何实现上述效果呢,我们可以继续学习。
-
MybatisPlus是如何获取实现CRUD的数据库表信息的?
通过扫描实体类【通过泛型传入】,并基于反射获取实体类信息作为数据库表信息
- 默认以类名驼峰转下划线作为表名
- 默认把名为id的字段作为主键
- 默认把变量名驼峰转下划线作为表的字段名
-
MybatisPlus的常用注解有哪些?
表明字段名不符合约定和特殊字段时就要自定义了。通过注解去指明表的信息
- @TableName:指定表名称及全局配置
- @TableId:指定id字段及相关配置
- @TableField:指定普通字段及相关配置
-
在@TableName可以设置IdType,其常见类型有哪些?
- AUTO:数据库自增长
- ASSIGN_ID:通过set方法自行输入
- INPUT【默认】:分配 ID,接口IdentifierGenerator的方法nextId来生成id,默认实现类为DefaultIdentifierGenerator雪花算法。
也就是说,对应id字段尽管符合约定,但还是要指明类型的,否则会用雪花算法【生成一个long型的id】分配
-
使用@TableField的常见场景是?
- 成员变量名与数据库字段名不一致
- 成员变量名以is开头,且是布尔值。否则在自动转换过程中会出错,会把is去掉作为变量名。
- 成员变量名与数据库关键字冲突
- 成员变量不是数据库字段
示例:
@Data
@TableName("tb_user")
public class User {
//指定为主键,声明为自增。
@TableId(value = "id", type = IdType.AUTO)
private Long id;
//成员变量名与数据库字段名不一致
@TableField("username")
private String name;
//成员变量名以is开头,是布尔值,否则误以为字段名是married了
@TableField("is_married")
private boolean isMarried;
//成员变量名与数据库关键字冲突
@TableField("`order`")
private Integer order;
//成员变量不是数据库字段。转换的时候忽略掉
@TableField("exist = false")
private String address;
}
常见配置
MyBatisPlus的配置项继承了MyBatis原生配置和一些自己特有的配置。例如:
mybatis-plus:
type-aliases-package: com.ario.mp.domain.po # 别名扫描包
mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,【默认值,符合默认值就不用配置】。这里可以支持多级目录的搜索。其实还是要写sql的,因为mp主要终于单表操作。
configuration:
map-underscore-to-camel-case: true # 是否开启下划线和驼峰的映射
cache-enabled: false # 是否开启二级缓存
global-config:
db-config:
id-type: assign_id # id为雪花算法生成
update-strategy: not_null # 更新策略:只更新非空字段
其实到部分的配置和mybatis的配置相同,可以更好的上手。当然也有新的配置,比如:
- global-config
别看着复杂,其实除了包的别名,都是有默认值的,一般不需要配置。
核心功能
条件构造器
我们上面都是用id实现的,我们很多情况需要用更复杂的条件筛选出想要的记录。mp提供了我们实现筛选的条件构造器。
MyBatisPlus支持各种复杂的where条件,可以满足日常开发的所有需求。

下面是一些继承关系

AbstractWrapper有很多方法能实现复杂的查询

其中我们常用的就是UpdateWrapper和QueryWapper。具体可以看如下案例
-
基于QueryWrapper的查询
需求:
-
查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
SELECT id,username,info,balance FROM user WHERE username LIKE ? AND balance >= ? -
更新用户名为jack的用户的余额为2000
UPDATE user SET balance = 2000 WHERE (username = "jack")
代码示例:
@Test void testQueryWrapper() { //1.利用条件构造器构建查询条件 //支持链式编程 QueryWrapper<User> queryWrapper = new QueryWrapper<User>() .select("id", "username", "info", "balance") .like("username", "o") .ge("balance", 1000); //2. 执行查询 List<User> users = userMapper.selectList(queryWrapper); } @Test void TestUpdateByQueryWrapper() { //1. 要更新的数据 User user = new User(); user.setBalance(2000); //2. 更新的条件 QueryWrapper<User> wrapper = new QueryWrapper<User>() .eq("username", "jack"); //3. 执行更新,给数据和条件 userMapper.update(user, wrapper); } -
-
基于UpdateWrapper的更新
仅有QueryWrapper在某些情况下还是不行的。UpdateWrappe通常只有在set语句比较特殊才使用。案例如下:
需求:更新id为1,2,4的用户的余额,扣200
这需要在记录的现有值上修改,不能像那样写了
UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)实现代码如下:
@Test void testUpdateWrapper() { List<Long> ids = List.of(1L, 2L); UpdateWrapper<User> updateWrapper = new UpdateWrapper<User>() .setSql("balance = balance - 200") //在这里手写sql即可 .in("id", ids); userMapper.update(null, updateWrapper); } -
LambdaQueryWrapper和LambdaUpdateWrapper
观察上面的几个案例可以发现,我用的是硬编码,我们应该尽量使用LambdaQueryWrapper和LambdaUpdateWrapper,避免硬编码。
示例代码如下:
@Test void testLambdaQueryWrapper() { LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>() //要用对应类的构造方法 .select(User::getId, User::getUsername, User::getInfo, User::getBalance) //用get方法获取字段 .like(User::getUsername, "o") .ge(User::getBalance, 1000); //2. 执行查询 List<User> users = userMapper.selectList(lambdaQueryWrapper); }
自定义SQL
为什么还要自定义SQL呢?我们可以看到,mp确实简化了SQL语句,比如实现where的作用,上面的in("id", ids)大大的缩减了代码量,可以跟下面对比一下
SELECT *
FROM user
<if test="ids != null">
WHERE id IN
<foreach collection="ids" open="(" close=")" item="id" separator=",">
#{id}
</foreach>
</if>
LIMIT 10
但是,对于另外一部分的SQL:balance = balance - 200,则是直接写在了业务层,这违反了编码规范。
由此我们可以用自定义SQL解决这样的难题,我们把全自动改为半自动,及发挥出mp的优势,又保证编码符合规范

示例代码如下:
-
业务层代码
@Test void testCustomSqlUpdate() { //1. 更新条件 List<Long> ids = List.of(1L, 2L); int amount = 200; //2. 定义条件 QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids);//也可以使用lambda的方式 //3. 调用自定义SQL方法 userMapper.updateBalanceByIds(wrapper, amount); } -
mapper层
//当然,这里也可以用注释实现 //必须要注解参数,这里Constants.WRAPPER是常量"ew",也可以直接写"ew" void updateBalanceByIds(@Param(Constants.WRAPPER) QueryWrapper<User> wrapper, @Param("amount") int amount); -
mapper.xml层
$用mp解析拼接
<update id="updateBalanceByIds"> UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment} </update>
Service接口
基本用法
mp还提供了service代码,之后就不应写常用的service代码了,其本质还是继承别人的代码。下面是mp提供的接口。

包括了常用的新增【单个、批量】、删除、更新和查询。
其中在批量删除可以区分一下
- removeByIds用的是in
- removeBatchByIds是用最简单的where id = ?的方式,然后jdbc批处理,数据量大时性能更好一点
其中lambda提供了链式编程的wrapper,不用new了。适合复杂条件的查询更新。
在实现中,我们继承就要继承全,只有接口继承是不可以的,还要继承实现类,因为要把接口中的类都实现

代码示例:
-
接口
public interface IUserService extends IService<User> { } -
实现类
//分别指出要用的mapper和实体类 public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { } -
使用代码
userService.save(user); //和之前代码无异
进入IUserService可以看到具体的实现,如果需要自定义SQL的方法,可以从中获取baseMapper实体类。

简单业务开发
对于如下的简单业务接口可以直接调用IService进行开发。
| 编号 | 接口 | 请求方式 | 请求路径 |
|---|---|---|---|
| 1 | 新增用户 | POST | /users |
| 2 | 删除用户 | DELETE | /users/ |
| 3 | 根据id查询用户 | GET | /users/ |
| 4 | 根据id批量查询 | GET | /users |
-
@RequiredArgsConstructor
在controller中注入service时,一般用构造函数注入,成员变量比较多是会先的很复杂,这是我们可以用Lombok提供的这个注解,实现按需注入。同时主要成员变量用final修饰,使其编程常量。
import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class UserController { private final IUserService userService; …… } -
hutool拷贝
在用提供的接口时,接口要求的类和已有的类可能不一样,属性基本一致时,我们可以通过拷贝来进行转换,hutool工具就提供了很好的方法。
单个拷贝:
public void saveUser(@RequestBody UserFormDTO userFormDTO){ // 1.转换DTO为PO User user = BeanUtil.copyProperties(userFormDTO, User.class); // 2.新增 userService.save(user); }集合拷贝:
public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids){ // 1.查询用户 List<User> users = userService.listByIds(ids); // 2.处理vo return BeanUtil.copyToList(users, UserVO.class); }
复杂业务开发
可以发现对于简单接口可以直接调用service方法。但对于复杂页面,需要编写自定义业务逻辑时,就需要自定义service方法,当业务百安写自定义sql语句是,依然要自定义方法,调用mapper,自定义sql语句
| 编号 | 接口 | 请求方式 | 请求路径 | ||
|---|---|---|---|---|---|
| 5 | 根据id扣减余额 | PUT | /users/{id}/deduction/ | 用户id扣减金额 | 无 |
这个接口就是稍微复杂的,要扣款,首先要判断账户是否被冻结,余额是否充足。
示例代码如下:
Controller层
@PutMapping("{id}/deduction/{money}")
public void deductBalance(@PathVariable("id") Long id, @PathVariable("money") Integer money){
userService.deductBalance(id, money);
}
Service层
接口
public interface IUserService extends IService<User> {
void deductBalance(Long id, Integer money);
}
实现类【编程技巧:反向判断,减少嵌套】
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public void deductBalance(Long id, Integer money) {
// 1.查询用户,这里就不用注入service了,因为本身就是service,直接调就行。或者调mapper,因为继承了IUserService,所以也不用注入mapper了,点进去调用对应的mapper即可。
User user = getById(id);
// 2.判断用户状态。编程技巧:反向判断,减少嵌套。
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常");
}
// 3.判断用户余额
if (user.getBalance() < money) {
throw new RuntimeException("用户余额不足");
}
// 4.扣减余额
baseMapper.deductMoneyById(id, money);
}
}
Mapper层
//这里就没必要传wrapper了
@Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")
void deductMoneyById(@Param("id") Long id, @Param("money") Integer money);
Lambda方法
IService中还提供了Lambda功能来简化我们的复杂查询及更新功能。下面通过案例了解。
案例一
需求:实现一个根据复杂条件查询用户的接口,查询条件如下:
- name:用户名关键字,可以为空
- status:用户状态,可以为空
- minBalance:最小余额,可以为空
- maxBalance:最大余额,可以为空
其复杂的原因是,不确定根据哪些字段查,如果写动态SQL的话,字段较多时会写老大一堆。这里用Lambda简化。当然我们也可以用wrapper,但这里lambda不用new,用话更简洁一点。
代码示例:
@GetMapping("/list")
public List<UserVO> queryUsers(UserQuery query){
// 1.组织条件
String username = query.getName();
Integer status = query.getStatus();
Integer minBalance = query.getMinBalance();
Integer maxBalance = query.getMaxBalance();
// 2.查询用户
List<User> users = userService.lambdaQuery()
.like(username != null, User::getUsername, username) //若不为空,则执行,判断代码更简洁了。
.eq(status != null, User::getStatus, status)
.ge(minBalance != null, User::getBalance, minBalance)
.le(maxBalance != null, User::getBalance, maxBalance)
.list();//记得调用list,否则没执行,前面的作用时构造条件的。
// 3.处理vo
return BeanUtil.copyToList(users, UserVO.class);
}
注意:
- 这里的代码都写的controller层了,实际开发中建议写道service中,否则显得臃肿。
- 因为参数比较多,可以直接传对象。需要定义一个新的对象。这也是常规操作。对于传复杂数据可以用对象,常用命名如下:
- dto封装前端查询条件
- vo返回给前端查询结果
- pojo对应数据库实体类
案例二
需求:改造根据id修改用户余额的接口,要求如下
- 完成对用户状态校验
- 完成对用户余额校验
- 如果扣减后余额为0,则将用户status修改为冻结状态
示例代码如下:
@Override
@Transactional
public void deductBalance(Long id, Integer money) {
// 1.查询用户
User user = getById(id);
// 2.校验用户状态
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常!");
}
// 3.校验余额是否充足
if (user.getBalance() < money) {
throw new RuntimeException("用户余额不足!");
}
// 4.扣减余额 update tb_user set balance = balance - ?
int remainBalance = user.getBalance() - money;
lambdaUpdate()
.set(User::getBalance, remainBalance) // 更新余额
.set(remainBalance == 0, User::getStatus, 2) // 动态判断,是否更新status
.eq(User::getId, id)
.eq(User::getBalance, user.getBalance()) // 乐观锁,避免出错
.update();//依旧要调用,否则不执行。
}
批量新增
批处理方案【例如10000条数据】:
-
普通for循环逐条插入速度极差,不推荐。【因为每次网络请求只提交一条sql语句,每个记录逐条执行SQL】
@Test void testSaveOneByOne() { long b = System.currentTimeMillis(); for (int i = 1; i <= 100000; i++) { userService.save(buildUser(i)); } long e = System.currentTimeMillis(); System.out.println("耗时:" + (e - b)); } private User buildUser(int i) { User user = new User(); user.setUsername("user_" + i); user.setPassword("123"); user.setPhone("" + (18688190000L + i)); user.setBalance(2000); user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}"); user.setCreateTime(LocalDateTime.now()); user.setUpdateTime(user.getCreateTime()); return user; } -
MP的批量新增,基于预编译的批处理,性能不错。【因为每次网络请求提交多个sql语句,但每个记录依然是逐条执行SQL】
@Test void testSaveBatch() { // 准备10万条数据 List<User> list = new ArrayList<>(1000); long b = System.currentTimeMillis(); for (int i = 1; i <= 100000; i++) { list.add(buildUser(i)); // 每1000条批量插入一次。因为一次请求的数据量是有上限的。 if (i % 1000 == 0) { userService.saveBatch(list); list.clear(); } } long e = System.currentTimeMillis(); System.out.println("耗时:" + (e - b)); } -
配置jdbc参数,开rewriteBatchedStatements【默认是不生效的】,性能最好。【因为每次网络请求提交多个sql语句,但同时多条记录合并成一个SQL了】
只在yaml文件中的连接数据库字段拼接如下字段接口,其他代码和“MP的批量新增”方式保存一直即可。
&rewriteBatchedStatements=true
扩展功能
拓展拓展,没有也行
代码生成
虽然经过我们之前的工作已经大大简化了开发过程。但是还是可以有优化之处。对于实体类、controller、service、mapper可能只是因为表名不同,我们要重复建立类似的文件。有点费时间了,mp给我们提供了代码生成功能,会根据数据库表,生成这些基本的代码。
但是生成代码前,我们需要些一堆生成代码的代码,还是挺麻烦的,我们可以用插件。

我们可以用官方提供的Mybatis X 插件,也可以用其他插件。在这了可以用MyBatisPlus插件。

安装完成后,需要我们做一些简单的配置。
-
在Other->Config Database,填写数据库信息。【dbUrl记得加数据库名】


-
填写表生成信息,生成代码
- 可以多选数据表,但是别选错了,会覆盖之前的代码。
- module是用于区分子模块的
- package填controller、service这里包的上一级。
- TablePrefix,如果不想要数据库表名的前缀带到类中,可以填这个。例如tb_user表,填tb_,能生产User类。

静态工具
有的时候Service之间也会相互调用【可以要用到多个查询语句,当然也可以用连接查询】,为了避免出现循环依赖问题【当然,也可以直接注入mapper,但是mapper不如service功能强大】,MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能。
静态工具和IService的区别就是,要告诉它实体类字节码,因为他是静态的,不能像IService那样拿到实体类上的泛型。
案例:改造根据id用户查询的接口,查询用户的同时返回用户收货地址列表【涉及到两个表】
核心代码:【UserVO需添加address字段】
@Override
public UserVO queryUserAndAddressById(Long userId) {
// 1.查询用户
User user = getById(userId);
if (user == null) {
return null;
}
// 2.查询收货地址
List<Address> addresses = Db.lambdaQuery(Address.class)
.eq(Address::getUserId, userId)
.list();
// 3.处理vo
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
return userVO;
}
补充案例:
根据id批量查询用户,并查询出用户对应的所有地址
这个实现建议先分别查出用户和地址,然后通过stream流的方式根据用户id进行分组【用过Collectors.groupingBy的方法】。一起转换成要返回的userVO。否则,如果嵌套查询的话效率会很低。
逻辑删除
如果把购物车里面的订单删掉,商家的订单肯定式不能变的。
实现逻辑删除,可以增加一个状态字段即可。但这与传统删除不一样了,可以说式更新。那之前mp提供的接口还能用吗,mp提供了方法,轻松实现。
我们只需要在配置文件中开启逻辑删除即可。之后调用之前的接口就是逻辑删除了【用法完全一样】。【当然表中要有对应字段】
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
可以在日志中看到,删除对应的操作实际上被转化为了更新操作。
但也有缺点:
- 会导致数据库表垃圾数据越来越多,从而影响查询效率
- SQL中全都需要对逻辑删除字段做判断,影响查询效率
因此,不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。
枚举处理器
数据库和Java枚举转换
在对对象状态做判断时【比如冻结/正常,未通过/通过】,我们数据库采用的是int类型,对应的PO也是Integer。因此业务操作时必须手动把枚举与Integer转换,非常麻烦。
因此,MybatisPlus提供了一个处理枚举的类型转换器,可以把枚举类型与数据库类型自动转换。
-
首先我们要定义一个枚举
import com.baomidou.mybatisplus.annotation.EnumValue; import lombok.Getter; @Getter public enum UserStatus { NORMAL(1, "正常"), FREEZE(2, "冻结") ; @EnumValue //告诉MybatisPlus,枚举中的哪个字段的值作为数据库值 private final int value; private final String desc; UserStatus(int value, String desc) { this.value = value; this.desc = desc; } }同时,实体类和返回类等涉及到该字段的也要变,用枚举类修改变量
private UserStatus status; -
配置,开启功能。用mp进行数据库和Java枚举转换
mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler -
然后没有就可以在代码直接使用啦。但这里要注意,这里查到的默认是枚举项名NORMAL/FROZEN。如果想指定,也可以通过注释指定【告诉SpringMVC要传什么】,如下
@JsonValue private final String desc;
JSON类型处理器
如果数据库的user表中有一个info字段,是JSON类型的。但是Java中没有json类型。所以需要转换也行,mp给我提供了类型处理器。分为两步。
-
定义实体
import lombok.Data; @Data public class UserInfo { private Integer age; private String intro; private String gender; } -
使用类型处理器【mp没有提供专门的配置,需要用注释声明】,同时,因为出现了类的嵌套,需要在User类上添加一个注解,声明自动映射。这样就免的我们定义复杂的resultMap了。
@TableName(value = "user", autoResultMap = true) public class User { …… @TableField(typeHandler = JacksonTypeHandler.class) private UserInfo info; …… }
插件功能
MyBatisPlus基于MyBatis的Interceptor实现了一个基础拦截器,并在内部保存了MyBatisPlus的内置拦截器的集合:


MyBatisPlus提供的内置拦截器有下面这些:
| 序号 | 拦截器 | 描述 |
|---|---|---|
| 1 | TenantLineInnerInterceptor | 多租户插件 |
| 2 | DynamicTableNameInnerInterceptor | 动态表名插件 |
| 3 | PaginationInnerInterceptor | 分页插件 |
| 4 | OptimisticLockerInnerInterceptor | 乐观锁插件 |
| 5 | IllegalSQLInnerInterceptor | SQL性能规范插件,检测并拦截垃圾SQL |
| 6 | BlockAttackInnerInterceptor | 防止全表更新和删除的插件 |
具体细节可以在官网了解学习。

浙公网安备 33010602011771号