MyBatis-Plus全攻略:快速入门 + 核心功能 + 扩展应用 + 插件功能一站式掌握
一、快速入门
1、入门案例(使用MybatisPlus的基本步骤)
1.1 引入MybatisPlus的起步依赖
引入MybatisPlus的起步依赖官方提供了starter,集成了Mybatis和MybatisPlus的所有功能,并且实现了自动装配效果。
因此我们可以用MybatisPlus的starter代替Mybatis的starter:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
1.2 定义Mapper
自定义的Mapper继承MybatisPlus提供的BaseMapper接口:
public interface UserMapper extends BaseMapper<User> {
}

继承之后,UserMapper.xml中的sql不用写了, 上面这个接口中的方法也不要了,直接调用BaseMapper里的方法就行。
2、常见注解
MyBatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。

- 类名驼峰转下划线作为表名
- 名为id的字段作为主键
- 变量名驼峰转下划线作为表的字段名
MybatisPlus中比较常用的几个注解如下:
- @TableName:用来指定当前实体类对应的数据库表名
- @TableId: 用来指定表中的主键字段信息
- @TableField:用来指定表中的普通字段信息

IdType枚举:
• AUTO:数据库自增长
• INPUT:通过set方法自行输入
• ASSIGN_ID:分配ID,供IdentifierGenerator的方法nextId来生成id, 默认实现为DefaultIdentifierGenerator雪花算法
使用@TableField的常见场景:
• 成员变量名与数据库字段名不一致
• 成员变量名以is开头,且是布尔值
• 成员变量名与数据库关键字冲突
• 成员变量不是数据库字段
3、常见配置
MybatisPlus的配置项继承了Mybatis原生配置和一些自己特有的配置。例如:

注:只更新非空字段意思是:实体类中有哪些字段它才会更新哪些字段
二、核心功能
1、条件构造器
MybatisPlus支持各种复杂的where条件,可以满足日常开发的所有需求。

1.1 Wrapper查询
通过BaseMapper提供的一些方法我们可以完成一些基本的CRUD,但无法完成复杂条件的查询;对于复杂条件的查询,MyBatisPlus提供了Wrapper接口来处理;
Wrapper是MyBatisPlus提供的一个条件构造器,主要用于构建一系列条件,当Wrapper构建完成后,可以使用Wrapper中的条件进行查询、修改、删除等操作;


Wrapper是条件构造抽象类,最顶端父类,其主要实现类有如下:
AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
- QueryWrapper : Query条件封装
- UpdateWrapper : Update条件封装

QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id","username","info","balance")
.like("username","o")
.ge("balancfe",1000);
List <User> users = userMapper.selectList(wrapper);
User user = new User();
user.setBalance(2000);
//更新的条件
QueryWrapper<User> wrapper = new QueryWrapper<User>.eq("username","jack");
//执行更新(第一个参数是更新的内容,第二个参数是更新的条件)
userMapper.update(user,wrapper);

List<Long> ids = List.of(1L,2L,4L);
UpdateWrapper<User> wrapper = new UpdateWrapper<User>
.setSql("balance = balance - 100")
.in("id",ids);
userMapper.update(null,wrapper);
无论是QueryWrapper还是UpdateWrapper在构造条件的时候都需要写死字段名称,会出现字符串魔法值。这在编程规范中显然是不推荐的。
那怎么样才能不写字段名,又能知道字段名呢?
AbstractLambdaWrapper : 使用Lambda语法
- LambdaQueryWrapper :基于Lambda语法使用的查询Wrapper
- LambdaUpdateWrapper : 基于Lambda语法使用的更新Wrapper
void testLambdaQueryWrapper() {
// 1.构建查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
.select(User::getId, User::getUsername, User::getInfo, User::getBalance)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000);
// 2.查询
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
1.2 总结
条件构造器的用法:
- QueryWrapper 和 LambdaQueryWrapper 通常用来构建 select、delete、update 的 where 条件部分
- UpdateWrapper 和 LambdaUpdateWrapper 通常只有在 set 语句比较特殊才使用
- 尽量使用 LambdaQueryWrapper 和 LambdaUpdateWrapper,避免硬编码
2、自定义SQL
我们可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分。


这种写法在某些企业也是不允许的,因为SQL语句最好都维护在持久层,而不是业务层。就当前案例来说,由于条件是in语句,只能将SQL写在Mapper.xml文件,利用foreach来生成动态SQL。
这实在是太麻烦了。假如查询条件更复杂,动态SQL的编写也会更加复杂。
所以,MybatisPlus提供了自定义SQL功能,可以让我们利用Wrapper生成查询条件,再结合Mapper.xml编写SQL。
2.1 基本用法

以当前案例来说,我们可以这样写:
@Test
void testCustomWrapper() {
// 1.准备自定义查询条件
List<Long> ids = List.of(1L, 2L, 4L);
QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids);
// 2.调用mapper的自定义方法,直接传递Wrapper
userMapper.deductBalanceByIds(200, wrapper);
}
然后在 UserMapper中自定义SQL:
package com.itheima.mp.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.mp.domain.po.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.apache.ibatis.annotations.Param;
public interface UserMapper extends BaseMapper<User> {
@Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")
void deductBalanceByIds(@Param("money") int money, @Param("ew") QueryWrapper<User> wrapper);
}
这样就省去了编写复杂查询条件的烦恼了。
总结:
在
mapper方法参数中用@Param注解声明wrapper变量名称,必须是ew。
为什么必须要用ew?
mybatisPlus源码规定的,这也成为一种约定俗成的规范。当然,你也可以手动修改生成的代码,将ew改为其他名字,但这样可能导致一些示例代码不适用。
2.2 多表关联
理论上来讲MyBatisPlus是不支持多表查询的,不过我们可以利用Wrapper中自定义条件结合自定义SQL来实现多表查询的效果。
例如,我们要查询出所有收货地址在北京的并且用户id在1、2、4之中的用户
要是自己基于mybatis实现SQL,大概是这样的:
<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">
SELECT *
FROM user u
INNER JOIN address a ON u.id = a.user_id
WHERE u.id
<foreach collection="ids" separator="," item="id" open="IN (" close=")">
#{id}
</foreach>
AND a.city = #{city}
</select>
可以看出其中最复杂的就是WHERE条件的编写,如果业务复杂一些,这里的SQL会更变态。
但是基于自定义SQL结合Wrapper的玩法,我们就可以利用Wrapper来构建查询条件,然后手写SELECT及FROM部分,实现多表查询。
查询条件这样来构建:
@Test
void testCustomJoinWrapper() {
// 1.准备自定义查询条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.in("u.id", List.of(1L, 2L, 4L))
.eq("a.city", "北京");
// 2.调用mapper的自定义方法
List<User> users = userMapper.queryUserByWrapper(wrapper);
users.forEach(System.out::println);
}
然后在UserMapper中自定义方法:
@Select("SELECT u.* FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}")
List<User> queryUserByWrapper(@Param("ew")QueryWrapper<User> wrapper);
当然,也可以在UserMapper.xml中写SQL:
<select id="queryUserByWrapper" resultType="com.itheima.mp.domain.po.User">
SELECT * FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}
</select>
3、Service接口
MybatisPlus不仅提供了BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的service模板方法。
通用接口为IService,默认实现为ServiceImpl,其中封装的方法可以分为以下几类:
- save:新增
- remove:删除
- update:更新
- get:查询单个结果
- list:查询集合结果
- count:计数
- page:分页查询


总结:
MP 的 Service 接口使用流程是怎样的?
- 自定义 Service 接口继承 IService 接口
public interface IUserService extends IService<User> {
}
- 自定义 Service 实现类,实现自定义接口并继承 ServiceImpl 类
public class UserServiceImpl extends ServiceImpl<UserMapper, User>implements IUserService{
}
【案例】基于Restful风格实现下列接口

首先,我们在项目中引入几个依赖:
<!--swagger-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
然后在application.xml需要配置swagger信息:
knife4j:
# 启用Knife4j接口文档增强功能
enable: true
openapi:
# 接口文档标题
title: 用户管理接口文档
# 接口文档详细描述
description: "用户管理接口文档"
# 维护者邮箱
email: ......
# 维护者姓名
concat: ......
# 项目相关网址
url: https://www.itcast.cn
# 接口文档版本号
version: v1.0.0
group:
default:
# 分组名称
group-name: default
# 接口扫描规则(按包扫描)
api-rule: package
# 要扫描的接口包路径列表
api-rule-resources:
- com.itheima.mp.controller
这是一段 Knife4j 的配置代码(通常用于 Spring Boot 项目的接口文档生成),Knife4j 是基于 Swagger 的增强工具,通过此类配置可快速生成可视化的接口文档,便于接口的调试与管理。
浏览器输入 http://localhost:8080/doc.html#/home
然后,接口需要两个实体:
- UserFormDTO:代表新增时的用户表单
- UserVO:代表查询的返回结果
首先是UserFormDTO:
package com.itheima.mp.domain.dto;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "用户表单实体")
public class UserFormDTO {
@ApiModelProperty("id")
private Long id;
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("密码")
private String password;
@ApiModelProperty("注册手机号")
private String phone;
@ApiModelProperty("详细信息,JSON风格")
private String info;
@ApiModelProperty("账户余额")
private Integer balance;
}
然后是UserVO:
package com.itheima.mp.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "用户VO实体")
public class UserVO {
@ApiModelProperty("用户id")
private Long id;
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("详细信息")
private String info;
@ApiModelProperty("使用状态(1正常 2冻结)")
private Integer status;
@ApiModelProperty("账户余额")
private Integer balance;
}
最后,按照Restful风格编写Controller接口方法:
package com.itheima.mp.controller;
import cn.hutool.core.bean.BeanUtil;
import com.itheima.mp.domain.dto.UserFormDTO;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Api(tags = "用户管理接口")
@RequiredArgsConstructor
@RestController
@RequestMapping("users")
public class UserController {
private final IUserService userService;
@PostMapping
@ApiOperation("新增用户")
public void saveUser(@RequestBody UserFormDTO userFormDTO){
// 1.转换DTO为PO
User user = BeanUtil.copyProperties(userFormDTO, User.class);
// 2.新增
userService.save(user);
}
@DeleteMapping("/{id}")
@ApiOperation("删除用户")
public void removeUserById(@PathVariable("id") Long userId){
userService.removeById(userId);
}
@GetMapping("/{id}")
@ApiOperation("根据id查询用户")
public UserVO queryUserById(@PathVariable("id") Long userId){
// 1.查询用户
User user = userService.getById(userId);
// 2.处理vo
return BeanUtil.copyProperties(user, UserVO.class);
}
@GetMapping
@ApiOperation("根据id集合查询用户")
public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids){
// 1.查询用户
List<User> users = userService.listByIds(ids);
// 2.处理vo
return BeanUtil.copyToList(users, UserVO.class);
}
}
可以看到上述接口都直接在controller即可实现,无需编写任何service代码,非常方便。
但是一些带有业务逻辑的接口则需要在service中自定义实现,例如根据id扣减用户余额时,需要两个判断:
- 判断用户状态是否正常
- 判断用户余额是否充足
首先在UserController中定义一个方法:
@PutMapping("{id}/deduction/{money}")
@ApiOperation("扣减用户余额")
public void deductBalance(@PathVariable("id") Long id,
@PathVariable("money")Integer money){
userService.deductBalance(id, money);
}
然后是UserService接口:
public interface IUserService extends IService<User> {
void deductBalance(Long id, Integer money);
}
最后是UserServiceImpl实现类:
package com.itheima.mp.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
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.扣减余额
baseMapper.deductMoneyById(id, money);
}
}
最后是mapper:
@Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")
void deductMoneyById(@Param("id") Long id, @Param("money") Integer money);
3.1 Lambda
IService中还提供了Lambda功能来简化复杂查询及更新功能。这里通过两个案例来学习。
案例一:实现一个复杂条件查询用户的接口,查询条件如下:
- name:用户名关键字,可以为空
- status:用户状态,可以为空
- minBalance:最小余额,可以为空
- maxBalance:最大余额,可以为空
定义一个查询条件实体,UserQuery实体:
package com.itheima.mp.domain.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
@ApiModelProperty("用户名关键字")
private String name;
@ApiModelProperty("用户状态:1-正常,2-冻结")
private Integer status;
@ApiModelProperty("余额最小值")
private Integer minBalance;
@ApiModelProperty("余额最大值")
private Integer maxBalance;
}
在UserController中定义一个controller方法:
@GetMapping("/list")
@ApiOperation("根据id集合查询用户")
public List<UserVO> queryUsers(UserQuery query){
// 1.组织条件
String username = query.getName();
Integer status = query.getStatus();
Integer minBalance = query.getMinBalance();
Integer maxBalance = query.getMaxBalance();
LambdaQueryWrapper<User> wrapper = new QueryWrapper<User>().lambda()
.like(username != null, User::getUsername, username)
.eq(status != null, User::getStatus, status)
.ge(minBalance != null, User::getBalance, minBalance)
.le(maxBalance != null, User::getBalance, maxBalance);
// 2.查询用户
List<User> users = userService.list(wrapper);
// 3.处理vo
return BeanUtil.copyToList(users, UserVO.class);
}
这里第一个参数xxx != null就是一个条件,只有条件满足时才会添加这个查询条件。
Service中对LambdaQueryWrapper和LambdaUpdateWrapper的用法进一步做了简化,无需new一个Wrapper,而是直接调用lambdaQuery和lambdaUpdate方法:
@GetMapping("/list")
@ApiOperation("根据id集合查询用户")
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();
// 3.处理vo
return BeanUtil.copyToList(users, UserVO.class);
}
链式编程的最后一步list()就是执行SQL,告诉MP调用结果需要是一个list集合,可选的方法有:
- .one():最多1个结果
- .list():返回集合结果
- .count():返回计数结果
MybatisPlus会根据链式编程的最后一个方法来判断最终的返回结果。
与lambdaQuery方法类似,IService中的lambdaUpdate方法可以非常方便的实现复杂更新业务。
案例二:改造根据id修改用户余额接口,如果扣减后余额为0,则将用户status修改为冻结状态(status=2)
@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();
}
3.2 批量新增
创建方法buildUser用于得到一条用户信息:
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;
}
逐条插入的代码如下(普通for循环插入):(不推荐)
@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));
}
批量插入的代码如下(IService的批量插入):(性能不错)
@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));
}
由于批量插入每次都传递一个集合,所以插入速率会显著提高,但是,运行时间还是比较长,效率低。
开启rewriteBatchedStatements=true参数:(性能最好)
需要在配置jdbc参数,开启rewriteBatchedStatements,性能最好。
先简单查看一下MybatisPlus源码:
@Transactional(rollbackFor = Exception.class)
@Override
public boolean saveBatch(Collection<T> entityList, int batchSize) {
String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
}
// ...SqlHelper
public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
Assert.isFalse(batchSize < 1, "batchSize must not be less than one");
return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, sqlSession -> {
int size = list.size();
int idxLimit = Math.min(batchSize, size);
int i = 1;
for (E element : list) {
consumer.accept(sqlSession, element);
if (i == idxLimit) {
sqlSession.flushStatements();
idxLimit = Math.min(idxLimit + batchSize, size);
}
i++;
}
});
}
可以发现其实MybatisPlus的批处理是基于PrepareStatement的预编译模式,然后批量提交,最终在数据库执行时还是会有多条insert语句,逐条插入数据,但这是MySQL数据库的原因,MybatisPlus不背锅。
如果想要批量插入,一次提交多条数据,需要修改MySQL的客户端连接参数rewriteBatchedStatements,这个参数的默认值是false,我们需要修改连接参数,将其配置为true(在url最后拼接即可):
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 你的密码
此时再次测试,批处理的效率就很高了。
三、扩展功能
1、代码生成
在使用MybatisPlus的过程中,基础的Mapper、Service、PO代码相对固定,重复高(仅仅泛型不一样),如果有多张表,就需要写很多次。虽然MybatisPlus官方提供了代码生成器可以根据数据库表结构生成PO、Mapper、Service等相关代码,但是代码生成器同样要编码使用,也很麻烦。
通过idea的MybatisPlus插件,可以基于图形化界面完成MybatisPlus的代码生成,非常简单。
1.1 安装插件
在Idea的plugins市场中搜索并安装MyBatisPlus插件,然后重启idea:

1.2 使用
以生成数据库表对应的实体和mapper等基础代码为例学习使用MyBatisPlus插件。
配置数据库地址,在Idea顶部菜单中,找到other,选择Config Database:

填写数据库连接的基本信息:

点击OK后,再次点击Idea顶部菜单中的other,然后选择Code Generator:

在弹出的表单中填写信息:

最后查看项目就会发现相应的代码就已经生成了。
2、静态工具
有时Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能:

示例:
@Test
void testDbGet() {
User user = Db.getById(1L, User.class);
System.out.println(user);
}
@Test
void testDbList() {
// 利用Db实现复杂条件查询
List<User> list = Db.lambdaQuery(User.class)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000)
.list();
list.forEach(System.out::println);
}
@Test
void testDbUpdate() {
Db.lambdaUpdate(User.class)
.set(User::getBalance, 2000)
.eq(User::getUsername, "Rose");
}
需求:①改造根据id用户查询的接口,查询用户的同时返回用户收货地址列表
首先,我们要添加一个收货地址的VO对象:
package com.itheima.mp.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(description = "收货地址VO")
public class AddressVO{
@ApiModelProperty("id")
private Long id;
@ApiModelProperty("用户ID")
private Long userId;
@ApiModelProperty("省")
private String province;
@ApiModelProperty("市")
private String city;
@ApiModelProperty("县/区")
private String town;
@ApiModelProperty("手机")
private String mobile;
@ApiModelProperty("详细地址")
private String street;
@ApiModelProperty("联系人")
private String contact;
@ApiModelProperty("是否是默认 1默认 0否")
private Boolean isDefault;
@ApiModelProperty("备注")
private String notes;
}
然后,改造原来的UserVO,添加一个地址属性:

接下来,修改UserController中根据id查询用户的业务接口:
@GetMapping("/{id}")
@ApiOperation("根据id查询用户")
public UserVO queryUserById(@PathVariable("id") Long userId){
// 基于自定义service方法查询
return userService.queryUserAndAddressById(userId);
}
由于查询业务复杂,所以要在service层来实现。首先在IUserService中定义方法:
package com.itheima.mp.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.vo.UserVO;
public interface IUserService extends IService<User> {
void deduct(Long id, Integer money);
UserVO queryUserAndAddressById(Long userId);
}
然后,在UserServiceImpl中实现该方法:
@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;
}
在查询地址时,采用了Db的静态方法,避免了注入AddressService,减少了循环依赖的风险。
需求:②改造根据id批量查询用户的接口,查询用户的同时,查询出用户对应的所有地址
public List<UserVO> queryUSersByIds(List<Long> ids) {
//查询用户
List<User> users = listByIds(ids);
//用户为空
if (CollUtil.isEmpty(users)){
return Collections.emptyList();
}
//获取用户id
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
//获取用户地址
List<Address> addressList = Db.lambdaQuery(Address.class).
in(Address::getUserId, userIds)
.list();
//转换地址
List<AddressVO> addressVOS = BeanUtil.copyToList(addressList, AddressVO.class);
//用户地址集合分组处理,相同用户的放入一个集合
Map<Long, List<AddressVO>> addressMap = new HashMap<>();
if (ObjectUtil.isNotEmpty(addressVOS)){
addressMap = addressVOS.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
}
//转换为UserVo返回
List<UserVO> userVO = new ArrayList<>(users.size());
for (User user : users) {
UserVO uv = BeanUtil.copyProperties(user, UserVO.class);
userVO.add(uv);
//设置地址
uv.setAddressVO(addressMap.get(user.getId()));
}
return userVO;
}
③实现根据用户id查询收货地址功能,需要验证用户状态,冻结用户抛出异常
public List<AddressVO> queryUSersAddressById(Long id) {
List<Address> address = lambdaQuery().eq(Address::getUserId, id).list();
//用户状态冻结
List<User> list = Db.lambdaQuery(User.class).eq(User::getId,id).eq(User::getStatus, 2).list();
if (CollUtil.isNotEmpty(list)){
throw new RuntimeException("用户状态异常");
}
return BeanUtil.copyToList(address,AddressVO.class);
}
3、逻辑删除
对于一些比较重要的数据,往往会采用逻辑删除的方案,即:
- 在表中添加一个字段标记数据是否被删除
- 当删除数据时把标记置为true
- 查询时过滤掉标记为true的数据
一旦采用了逻辑删除,所有的查询和删除逻辑都要跟着变化,非常麻烦。
为了解决这个问题,MybatisPlus就添加了对逻辑删除的支持。
注意:只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。
例如,给address表添加一个逻辑删除字段:
alter table address add deleted bit default b'0' null comment '逻辑删除';
然后给Address实体添加deleted字段:

接下来,在application.yaml中配置逻辑删除字段:
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)
测试
首先,执行一个删除操作:
@Test
void testDeleteByLogic() {
// 删除方法与以前没有区别
addressService.removeById(59L);
}
方法与普通删除一模一样,但是底层的SQL逻辑变了:

查询一下试试:
@Test
void testQuery() {
List<Address> list = addressService.list();
list.forEach(System.out::println);
}
会发现id为59的确实没有查询出来,而且SQL中也对逻辑删除字段做了判断:

注意:逻辑删除会导致数据库表垃圾数据越来越多,而且SQL全都需要对逻辑删除字段做判断,导致查询效率变低,所以不推荐使用逻辑删除,如果实在需要,可以把删除数据迁移到其他表。
4、通用枚举(枚举处理器)
User类中有一个用户状态字段status,它只有两个值:1(正常)、2(冻结),这种字段一般会定义一个枚举,业务判断时可以直接基于枚举做比较,但是数据库采用的是int类型,对应的是Integer类型,因此业务操作需要手动转换枚举与Integer,比较麻烦。
MybatisPlus提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换。
4.1 定义枚举
在com.itheima.mp.enums包下定义一个用户状态的枚举:
package com.itheima.mp.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;
@Getter
public enum UserStatus {
//两个参数分别对应value和desc
NORMAL(1, "正常"),
FREEZE(2, "冻结")
;
@EnumValue //mp就知道把value往数据库里写
private final int value;
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
把User类中的status字段改为UserStatus 类型:

要让MybatisPlus处理枚举与数据库类型自动转换,必须告诉MybatisPlus,枚举中的哪个字段的值作为数据库值。
MybatisPlus提供了@EnumValue注解来标记枚举属性:

4.2 配置枚举处理器
在application.yaml文件中添加配置:
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
4.3 测试
@Test
void testService() {
List<User> list = userService.list();
list.forEach(System.out::println);
}
最终,查询出的User类的status字段会是枚举类型:

为了使前端页面查询结果也是枚举格式,我们需要修改UserVO中的status属性(这一步可有可无):

并且,在UserStatus枚举中通过@JsonValue注解标记JSON序列化时展示的字段:
加在value上返回的就是1或者2;加在desc上返回的就是正常或者冻结

最终,前端得到的结果如下:

5、JSON类型处理器
数据库的user表中有一个info字段,是JSON类型,而目前User实体类中却是String类型。一旦把info改为对象类型,就需要在写入数据库时手动转为String,再读取数据库时,手动转换为对象,这会非常麻烦。

MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler处理器。
5.1 定义实体
首先,定义一个单独实体类来与info字段的属性匹配:
package com.itheima.mp.domain.po;
import lombok.Data;
@Data
@NoArgsContructor
@AllArgsConstructor
public class UserInfo {
private Integer age;
private String intro;
private String gender;
}
5.2 使用类型处理器
将User类的info字段修改为UserInfo类型,并声明类型处理器:

同时,在User类上添加一个注解,声明自动映射:

测试可以发现,所有数据都正确封装到UserInfo当中了。
同时,为了让页面返回的结果也以对象格式返回,我们要修改UserVO中的info字段:

6、配置加密
由于配置文件中的很多参数都是明文,如果开发人员发生流动,很容易导致敏感信息的泄露。所以MybatisPlus支持配置文件的加密和解密功能。
我们以数据库的用户名和密码为例。
首先,我们利用AES工具生成一个随机秘钥,然后对用户名、密码加密:
package com.itheima.mp;
import com.baomidou.mybatisplus.core.toolkit.AES;
import org.junit.jupiter.api.Test;
class MpDemoApplicationTests {
@Test
void contextLoads() {
// 生成 16 位随机 AES 密钥
String randomKey = AES.generateRandomKey();
System.out.println("randomKey = " + randomKey);
// 利用密钥对用户名加密
String username = AES.encrypt("root", randomKey);
System.out.println("username = " + username);
// 利用密钥对密码加密
String password = AES.encrypt("你的密码", randomKey);
System.out.println("password = " + password);
}
}
打印结果如下:
randomKey = 6234633a66fb399f
username = px2bAbnUfiY8K/IgsKvscg==
password = FGvCSEaOuga3ulDAsxw68Q==
6.2 修改配置
修改application.yaml文件,把jdbc的用户名、密码修改为刚刚加密生成的密文:
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: mpw:px2bAbnUfiY8K/IgsKvscg== # 密文要以 mpw:开头
password: mpw:FGvCSEaOuga3ulDAsxw68Q== # 密文要以 mpw:开头
6.3 测试
把刚才生成的秘钥添加到启动参数中,模版:–mpw.key=6234633a66fb399f

随意运行一个单元测试,可以发现数据库查询正常。
四、插件功能
MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:
- PaginationInnerInterceptor:自动分页
- TenantLineInnerInterceptor:多租户
- DynamicTableNameInnerInterceptor:动态表名
- OptimisticLockerInnerInterceptor:乐观锁
- IllegalSQLInnerInterceptor:sql 性能规范
- BlockAttackInnerInterceptor:防止全表更新与删除
注意:使用多个分页插件的时候需要注意插件定义顺序,建议使用顺序如下:
- 多租户,动态表名
- 分页,乐观锁
- sql 性能规范,防止全表更新与删除
1.分页插件
在未引入分页插件的情况下,MybatisPlus是不支持分页功能的,IService和BaseMapper中的分页方法都无法正常起效。
1.1 配置分页插件
在com.itheima.mp.config包下新建一个配置类:
package com.itheima.mp.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 1.初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 2.添加分页插件
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
pageInterceptor.setMaxLimit(1000L); // 设置分页上限
interceptor.addInnerInterceptor(pageInterceptor);
return interceptor;
}
}
1.2 分页API
编写一个分页查询的测试:
@Test
void testPageQuery() {
// 1.分页查询,new Page()的两个参数分别是:页码、每页大小
Page<User> p = userService.page(new Page<>(2, 2));
// 2.总条数
System.out.println("total = " + p.getTotal());
// 3.总页数
System.out.println("pages = " + p.getPages());
// 4.数据
List<User> records = p.getRecords();
records.forEach(System.out::println);
}
运行的SQL如下:

这里用到了分页参数,Page,即可以支持分页参数,也可以支持排序参数。常见的API如下:
@Test
void testPageQuery() {
// 1.查询
int pageNo = 2, pageSize = 2;
// 1.1.分页参数
Page<User> page = Page.of(pageNo, pageSize);
// 1.2.排序参数, 通过OrderItem来指定
page.addOrder(new OrderItem("balance", false));
// 1.3.分页查询
Page<User> p = userService.page(page);
// 2.总条数
System.out.println("total = " + p.getTotal());
// 3.总页数
System.out.println("pages = " + p.getPages());
// 4.分页数据
List<User> records = p.getRecords();
records.forEach(System.out::println);
}
2.通用分页实体
实现一个用户分页查询的接口,接口规范如下:
-
请求方式: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:用户页面视图实体
2.1 实体
在com.itheima.mp.query包下创建分页参数实体类PageQuery:
@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
@ApiModelProperty("页码")
private Long pageNo;
@ApiModelProperty("每页数据条数")
private Long pageSize;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否升序")
private Boolean isAsc;
}
使已有的UserQuery继承PageQuery:
package com.itheima.mp.domain.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/*@Data 注解本身包含 @EqualsAndHashCode(callSuper = false) 的默认逻辑,
所以必须显式添加 @EqualsAndHashCode(callSuper = true) 来覆盖默认行为,
否则父类的分页字段会被忽略;*/
@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery extends PageQuery {
@ApiModelProperty("用户名关键字")
private String name;
@ApiModelProperty("用户状态:1-正常,2-冻结")
private Integer status;
@ApiModelProperty("余额最小值")
private Integer minBalance;
@ApiModelProperty("余额最大值")
private Integer maxBalance;
}
沿用之前定义的UserVO实体,最后定义分页实体PageDTO:
package com.itheima.mp.domain.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel(description = "分页结果")
public class PageDTO<T> {
@ApiModelProperty("总条数")
private Long total;
@ApiModelProperty("总页数")
private Long pages;
@ApiModelProperty("集合")
private List<T> list;
}
2.2 开发接口
在UserController中定义分页查询用户的接口:
package com.itheima.mp.controller;
import com.itheima.mp.domain.dto.PageDTO;
import com.itheima.mp.domain.query.PageQuery;
import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/page")
public PageDTO<UserVO> queryUsersPage(UserQuery query){
return userService.queryUsersPage(query);
}
// ...
}
然后在IUserService中创建queryUsersPage方法:
PageDTO<UserVO> queryUsersPage(PageQuery query);
接下来,在UserServiceImpl中实现该方法:
public PageDTO<UserVO> queryPage(UserQuery query) {
String name = query.getName();
Integer status = query.getStatus();
//1.构建分页条件
//1.1 分页
Page<User> page = Page.of(query.getPageNo(), query.getPageSize());
//1.2 排序 如果排序条件为空就默认排序
if (StrUtil.isNotBlank(query.getSortBy())){
page.addOrder(new OrderItem(query.getSortBy(),query.getIsAsc()));
}else{
page.addOrder(new OrderItem("update_time",false));
}
//2.分页查询
Page<User> userPage = lambdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.page(page);
//3.封装VO结果
PageDTO<UserVO> userVoPageDTO = new PageDTO<UserVO>();
//总条数
userVoPageDTO.setTotal(userPage.getTotal());
//总页数
userVoPageDTO.setPages(userPage.getPages());
//当前页数据
List<User> records = userPage.getRecords();
if (CollUtil.isEmpty(records)){
userVoPageDTO.setList(Collections.emptyList());
return userVoPageDTO;
}
//转换对象
List<UserVO> userVOS = BeanUtil.copyToList(records, UserVO.class);
userVoPageDTO.setList(userVOS);
return userVoPageDTO;
}
启动项目,在浏览器访问http://localhost:8080/users/page?pageNo=1&pageSize=2就可以看到查询的数据了。
2.3 改造PageQuery实体
在刚才的代码中,从PageQuery到MybatisPlus的Page之间转换的过程还是比较麻烦的。
完全可以在PageQuery这个实体中定义一个工具方法,简化开发。
package com.itheima.mp.domain.query;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Data;
@Data
public class PageQuery {
private Integer pageNo = 1;
private Integer pageSize = 5;
private String sortBy;
private Boolean isAsc;
//先声明 <T>,再在返回值中使用 T
public <T> Page<T> toMpPage(OrderItem ... orders){
// 1.分页条件
Page<T> p = Page.of(pageNo, pageSize);
// 2.排序条件
// 2.1.先看前端有没有传排序字段
if (sortBy != null) {
p.addOrder(new OrderItem(sortBy, isAsc));
return p;
}
// 2.2.再看有没有手动指定排序字段
if(orders != null){
p.addOrder(orders);
}
return p;
}
public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc){
return this.toMpPage(new OrderItem(defaultSortBy, isAsc));
}
public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {
return toMpPage("create_time", false);
}
public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {
return toMpPage("update_time", false);
}
}
这样就可以省去从PageQuery到Page的转换:
// 1.构建条件
Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
2.4 改造PageDTO实体
在查询出分页结果后,数据的非空校验,数据的vo转换都是模板代码,编写起来很麻烦。
完全可以将其封装到PageDTO的工具方法中,简化整个过程:
package com.itheima.mp.domain.dto;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageDTO<V> {
private Long total;
private Long pages;
private List<V> list;
/**
* 返回空分页结果
* @param p MybatisPlus的分页结果
* @param <V> 目标VO类型
* @param <P> 原始PO类型
* @return VO的分页对象
*/
public static <V, P> PageDTO<V> empty(Page<P> p){
return new PageDTO<>(p.getTotal(), p.getPages(), Collections.emptyList());
}
/**
* 将MybatisPlus分页结果转为 VO分页结果
* @param p MybatisPlus的分页结果
* @param voClass 目标VO类型的字节码
* @param <V> 目标VO类型
* @param <P> 原始PO类型
* @return VO的分页对象
*/
public static <V, P> PageDTO<V> of(Page<P> p, Class<V> voClass) {
// 1.非空校验
List<P> records = p.getRecords();
if (records == null || records.size() <= 0) {
// 无数据,返回空结果
return empty(p);
}
// 2.数据转换
List<V> vos = BeanUtil.copyToList(records, voClass);
// 3.封装返回
return new PageDTO<>(p.getTotal(), p.getPages(), vos);
}
/**
* 将MybatisPlus分页结果转为 VO分页结果,允许用户自定义PO到VO的转换方式
* @param p MybatisPlus的分页结果
* @param convertor PO到VO的转换函数
* @param <V> 目标VO类型
* @param <P> 原始PO类型
* @return VO的分页对象
*/
public static <V, P> PageDTO<V> of(Page<P> p, Function<P, V> convertor) {
// 1.非空校验
List<P> records = p.getRecords();
if (records == null || records.size() <= 0) {
// 无数据,返回空结果
return empty(p);
}
// 2.数据转换
List<V> vos = records.stream().map(convertor).collect(Collectors.toList());
// 3.封装返回
return new PageDTO<>(p.getTotal(), p.getPages(), vos);
}
}
最终,业务层的代码可以简化为:
@Override
public PageDTO<UserVO> queryUserByPage(PageQuery query) {
// 1.构建条件
Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
// 2.查询
page(page);
// 3.封装返回
return PageDTO.of(page, UserVO.class);
}
如果希望自定义PO到VO的转换过程,可以这样做:
@Override
public PageDTO<UserVO> queryUserByPage(PageQuery query) {
// 1.构建条件
Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
// 2.查询
page(page);
// 3.封装返回
return PageDTO.of(page, user -> {
// 拷贝属性到VO
UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
// 用户名脱敏
String username = vo.getUsername();
vo.setUsername(username.substring(0, username.length() - 2) + "**");
return vo;
});
}
或者:
// 2. 自定义转换逻辑:User → UserVO
return PageDTO.of(page, user -> {
UserVO vo = new UserVO();
vo.setId(user.getId());
vo.setUsername(user.getUsername());
// 处理状态转换:1→正常,2→冻结
vo.setStatusDesc(user.getStatus() == 1 ? "正常" : "冻结");
return vo;
});
更多推荐


所有评论(0)