
MyBatis-Plus 学习笔记
特别鸣谢:黑马程序员 — 虎哥《SpringCloud微服务开发与实战》课程
一、基本使用
① 引入依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>② 继承 BaseMapper<T>
package com.itheima.mp.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.mp.domain.po.User;
public interface UserMapper extends BaseMapper<User> {
}MyBatisPlus实现原理:通过扫描实体类,基于反射机制获取实体类信息作为数据库表信息。
当实体类与数据库表满足以下约定时,实体类和数据库可正确映射,无需额外配置,使用MyBatisPlus一定要设置主键。
(1)类名驼峰转下划线作为表名;
(2)名为id的字段作为主键;
(3)变量名驼峰转下划线作为表中的字段名。
若不满足以上约定,则需要使用以下注解:
| 注解名 | 用途 |
| @TableName | 指定表名 |
| @TableId | 指定表中的主键字段信息 |
| @TableField | 指定表中的普通字段信息 |
其中@TableId(value="数据库中的主键名", type= IdType.*),IdType枚举类:
| 枚举字段 | 含义 |
| IdType.AUTO | 数据库自增长 |
| IdType.INPUT | 通过set方法自行输入(自己指定ID) |
| IdType.ASSIGN_ID | 分配的ID·接口IdentifierGenerator的方法nextId()来生成id,默认实现为DefaultIdentifierGenerator雪花算法 |
如果需要ID自增长,需要显式设置@TableId(type= IdType.AUTO),否则默认使用雪花算法(生成一个Long类型的长ID)
使用@TableField的常见场景:
- 成员变量名与数据库字段名不一致
- 成员变量名以is开头,且是boolean类型
- 成员变量名与数据库关键字冲突
- 成员变量不是数据库字段 --> 加上@TableField(exist = false)
例如:① Boolean isMarried 由于是布尔类型,会默认映射到数据库的married字 ② Integer order 与数据库的order排序关键字冲突
注解使用范例:


注解的作用:告诉MybatisPlus,数据库中的字段是怎样定义的。
更多注解信息,请参考官方文档:注解配置 | MyBatis-Plus
③ 常见配置
mybatis-plus的配置项继承了mybatis原生配置,和自己一些特有的配置

其中 global-config 是 mybatis-plus 特有的配置。其中:
id-type默认值为ASSIGN_ID,当和注解配置@TableId同时存在时,注解的优先级更高。如果没有注解,默认使用这里的id-type配置。update-strategy默认值就是not_null,在更新数据时,只更新非null的字段。
更多配置信息,请参考官方文档:使用配置 | MyBatis-Plus
二、核心功能
2.1 条件构造器(Wrapper)
注意到 BaseMapper 中有一部分方法的传参为 Wrapper 类型:

继承体系:

QueryWrapper 和 LambdaQueryWrapper 通常用来构建 select、delete、update 和 where 条件
UpdateWrapper 和 LambdaUpdateWrapper 通常只有在 set 语句比较特殊才会使用
尽量使用 LambdaQueryWrapper 和 LambdaUpdateWrapper 来避免硬编码
2.1.1 QueryWrapper 范例
使用范例1:
/**
* 查询出名字中带 o 的,存款大于等于 1000 的人的id、username、
* info 和 balance
*/
@Test
void testQueryWrapper() {
// 1、构建查询条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 1000);
// 2、调用方法查询
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}// 对应的 SQL
SELECT id, username, info, balance FROM user
WHERE username LIKE "%o%" AND balance >= 1000运行结果:
Preparing: SELECT id,username,info,balance FROM user WHERE (username LIKE ? AND balance >= ?)
Parameters: %o%(String), 1000(Integer)
<== Total: 2
User(id=3, username=Hope, password=null, phone=null, info={"age": 25, "intro": "上进青年", "gender": "male"}, status=null, balance=100000, createTime=null, updateTime=null)
User(id=4, username=Thomas, password=null, phone=null, info={"age": 29, "intro": "伏地魔", "gender": "male"}, status=null, balance=1200, createTime=null, updateTime=null)使用范例2:
/**
* 使用 QueryWrapper 更新 jack 的余额为 2000
*/
@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);
}// 对应SQL
UPDATE user SET balance=2000 WHERE username = "Jack"2.1.2 UpdateWrapper 范例
/**
* 使用 UpdateWrapper 更新 id 在 1,2,4 中的用户的余额减 200
*/
@Test
void testUpdateWrapper() {
List<Long> ids = List.of(1L, 2L, 4L);
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200")
.in("id", ids);
userMapper.update(null, wrapper);
}// 对应SQL
UPDATE user SET balance = balance - 200 WHERE id IN (1, 2, 4)运行结果:
Preparing: UPDATE user SET balance = balance - 200 WHERE (id IN (?,?,?))
Parameters: 1(Long), 2(Long), 4(Long)
<== Updates: 32.1.3 LambdaQueryWrapper
/**
* 使用 LambdaQueryWrapper 查询出名字中带 o 的,存款大于等于 1000 的人的 username、
* info 和 balance
*/
@Test
void testLambdaQueryWrapper() {
// 以下两种创建 LambdaQueryWrapper 的方式均可
// LambdaQueryWrapper<User> wrapper = new QueryWrapper<User>().lambda()
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
.select(User::getUsername, User::getInfo, User::getBalance)
.like(User::getUsername, "o")
.ge(User::getBalance, 1000);
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}运行结果:
Preparing: SELECT username,info,balance FROM user WHERE (username LIKE ? AND balance >= ?)
Parameters: %o%(String), 1000(Integer)
<== Total: 2
User(id=null, username=Hope, password=null, phone=null, info={"age": 25, "intro": "上进青年", "gender": "male"}, status=null, balance=100000, createTime=null, updateTime=null)
User(id=null, username=Thomas, password=null, phone=null, info={"age": 29, "intro": "伏地魔", "gender": "male"}, status=null, balance=1200, createTime=null, updateTime=null)2.2 自定义SQL
to do
三、使用IService接口
使用mybatis-plus,不仅基本的增删改查mapper不用写了,就连简单的service也不用自己写了,以下是常见IService接口提供的增删改查方法:

3.1 基本使用
但是接口定义的方法都需要实现,自定义的Service接口,如果继承了IService接口,那么这些方法都需要实现。为此,mybatis-plus也提供了ServiceImpl实现类,实现了IService中规定的方法。使用步骤如下:
① 自定义接口继承IService接口:
package com.itheima.mp.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;
public interface IUserService extends IService<User> {
}
② 自定义实现类,继承ServiceImpl,并实现自定义的接口
ServiceImpl<对应的Mapper, 对应的实体类>
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 {
}创建单元测试:

一定要加上 @SpringBootTest 注解!
package com.itheima.mp.service;
import com.itheima.mp.domain.po.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
@SpringBootTest
class IUserServiceTest {
@Autowired
private IUserService userService;
@Test
public void testSave() {
User user = new User();
user.setUsername("LiLei");
user.setPassword("123");
user.setPhone("18688990011");
user.setBalance(200);
user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
userService.save(user);
}
@Test
void testQuery() {
List<User> users = userService.listByIds(List.of(1L, 2L, 4L));
users.forEach(System.out::println);
}
}3.2 Mybatis 实现 Restful CRUD 案例
基于Restful风格实现下面的接口:

3.2.1 引入相关依赖
<!--swagger 方便测试 api-->
<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>knife4j:
enable: true
openapi:
title: 用户管理接口文档
description: "用户管理接口文档"
email: zhanghuyi@itcast.cn
concat: 虎哥
url: https://www.itcast.cn
version: v1.0.0
group:
default:
group-name: default
api-rule: package
api-rule-resources:
- com.itheima.mp.controller3.2.2 编写 UserController
package com.itheima.mp.domain.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 io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Api(tags = "用户管理接口")
@RestController
@RequestMapping("/users")
// Generates a constructor with required arguments.
// Required arguments are final fields and fields with constraints such as @NonNull
@RequiredArgsConstructor
public class UserController {
// 配合 @RequiredArgsConstructor 生成构造函数注入
private final IUserService userService;
@ApiOperation("新增用户")
@PostMapping
public void saveUser(@RequestBody UserFormDTO userFormDTO) {
// 1、把 DTO 拷贝到 PO
User user = BeanUtil.copyProperties(userFormDTO, User.class);
// 2、新增
userService.save(user);
}
@ApiOperation("删除用户")
@DeleteMapping("/{id}")
public void deleteUser(@ApiParam("用户id") @PathVariable("id") Long id) {
userService.removeById(id);
}
@ApiOperation("根据id查询用户")
@GetMapping("/{id}")
public UserVO getUserById(@ApiParam("用户id") @PathVariable("id") Long id) {
User user = userService.getById(id);
return BeanUtil.copyProperties(user, UserVO.class);
}
@ApiOperation("根据id批量查询用户")
@GetMapping
public List<UserVO> listUsersByIds(
@ApiParam("用户id列表") @RequestParam("ids") List<Long> ids) {
List<User> users = userService.listByIds(ids);
return BeanUtil.copyToList(users, UserVO.class);
}
@ApiOperation("扣减用余额")
@PutMapping("/{id}/deduction/{money}")
public void deleteUser(
@ApiParam("用户id") @PathVariable("id") Long id,
@ApiParam("扣减的金额") @PathVariable("money") Integer money) {
// Alt + Enter 换出并在IUserService中创建方法
userService.deductBalance(id, money);
}
}注意:“根据id批量查询用户”接口,接收GET请求,请求参数为 List<Long> ids 类型,请求方式有以下几种:
方式一:
GET http://localhost:8080/users?ids=1&ids=2&ids=3 HTTP/1.1
方式二:
GET http://localhost:8080/users?ids=1,2,3 HTTP/1.1
axios.get('/users/listUsersByIds', {
params: {
ids: [1, 2, 3] // Axios 会自动转换为 ?ids=1&ids=2&ids=3
}
});3.2.3 编写 UserService
package com.itheima.mp.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;
public interface IUserService extends IService<User> {
void deductBalance(Long id, Integer money);
}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);
// 或者使用 baseMapper,它实际上就是UserMapper
// User user = baseMapper.selectById(id);
// 2、校验用户状态
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常!");
}
// 3、校验余额是否充足
if (user.getBalance() < money) {
throw new RuntimeException("用户余额不足!");
}
// 4、扣减余额 update user set balance = balance - ? where id = ?
baseMapper.deductBalance(id, money);
}
}3.2.4 编写 UserMapper
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;
public interface UserMapper extends BaseMapper<User> {
@Update("update user set balance = balance - #{money} where id = #{id}")
void deductBalance(@Param("id") Long id, @Param("money") Integer money);
}3.2.5 测试接口
http://localhost:8080/doc.html 接口 -> 调试
3.3 IService 的 Lambda 查询方式
3.3.1 案例一:使用 Lambda 实现复杂查询
需求:实现一个根据复杂条件查询用户的接口,查询条件如下:
① name:用户名,可以为空
② status:账户状态,可以为空
③ minBalance:最小余额,可以为空
④ maxBalance:最大余额,可以为空
可以理解成一个用户的后台管理界面,管理员可以自己选择条件来筛选用户,因此上述条件不一定存在,需要做判断。如果是xml文件,应该长这样:

① 用于接收查询参数的Entity
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
@ApiOperation("根据复杂条件批量查询用户")
@GetMapping("/list")
public List<UserVO> queryUsers(UserQuery query) {
List<User> users = userService.queryUsers(
query.getName(), query.getStatus(),
query.getMinBalance(), query.getMaxBalance());
return BeanUtil.copyToList(users, UserVO.class);
}③ 编写 UserService
public interface IUserService extends IService<User> {
...
List<User> queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance);
}@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
...
@Override
public List<User> queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance) {
return lambdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.ge(minBalance != null, User::getBalance, minBalance)
.le(maxBalance != null, User::getBalance, maxBalance)
.list();
}
}可以发现lambdaQuery方法中除了可以构建条件,还需要在链式编程的最后添加一个list(),这是在告诉MP我们的调用结果需要是一个list集合。这里不仅可以用list(),可选的方法有:
| 函数名 | 描述 |
| .one() | 最多1个结果 |
| .list() | 返回集合结果 |
| .count() | 返回计数结果 |
| .page() | 用来做分页 |
3.3.2 案例二:使用 Lambda Update 修改 3.2.3 扣减余额逻辑
需求:实现一个根据复杂条件查询用户的接口,查询条件如下:
① 对用户状态校验
② 对用户余额校验
③ 如果扣减后余额为0,则修改用户status为冻结状态(2)
@Override
@Transactional
public void deductBalance(Long id, Integer money) {
// 1、查询用户
User user = getById(id);
// 或者使用 baseMapper,它实际上就是UserMapper
// User user = baseMapper.selectById(id);
// 2、校验用户状态
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常!");
}
// 3、校验余额是否充足
if (user.getBalance() < money) {
throw new RuntimeException("用户余额不足!");
}
// 4、扣减余额 update user set balance = balance - ? where id = ?
// baseMapper.deductBalance(id, money);
int remainBalance = user.getBalance() - money;
// 这里的操作是有并发安全问题的,比如多个线程同时通过了前两个校验,
// 要执行接下来的余额扣减。所以需要加锁(悲观锁,或乐观锁)
lambdaUpdate()
.set(User::getBalance, remainBalance)
.set(remainBalance == 0, User::getStatus, 2)
.eq(User::getId, id)
// 乐观锁
.eq(User::getBalance, user.getBalance())
// 这里必须加 update 才会执行
.update();
}① lambdaQuery() 主要是用来构建复杂查询的,简单查询推荐使用 IService 中以及定义好的方法;
② lambdaUpdate() 用来构建复杂更新,这两个 lambda 都可以用第一个 condition 参数,来决定对应的参数是否参与SQL拼接;
③ lambdaUpdate() 最后必须加 update() 才会执行。
四、批处理&性能测试
4.1 单条循环插入
@SpringBootTest
class IUserServiceTest {
@Autowired
private IUserService userService;
@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;
}
}10万条数据插入速度超级慢(没耐心等了直接终止了),黑马虎哥测试是20万ms
-- 删除刚刚插入的大量数据
DELETE FROM user WHERE id > 5 4.2 批处理方式
批处理需要先new出来,放到内存里,占用内存太多,所以需要分批次,每批次1000条数据。而且向MySQL发送网络请求的数据包,大小是有限制的。
@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));
}-- 查询插入是否成功
SELECT count(*) FROM user WHERE id > 5 耗时:6136ms(mybatis的foreach拼接成一条SQL是最快的)
其实MybatisPlus的批处理是基于PrepareStatement的预编译模式,然后批量提交,最终在数据库执行时还是会有多条insert语句,逐条插入数据。SQL类似这样:
Preparing: INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
Parameters: user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01Preparing: INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
Parameters: user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01MySQL的客户端连接参数中有这样的一个参数:rewriteBatchedStatements。顾名思义,就是重写批处理的statement语句。参考文档:MySQL :: Connectors and APIs Manual :: 3.5.3.13 Performance Extensions 的 rewriteBatchedStatements
这个参数的默认值是false,我们需要修改连接参数,将其配置为true
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mp?...&rewriteBatchedStatements=true耗时:4117ms
五、扩展功能
5.1 代码生成

新款 IDEA 2024 使用方法:
① 工具 --> Config Database

如果测试链接还是连不上,需要加&useSSL=false
② 工具 --> Code Generator

直接点 Code Generate 没反应的,应该先点 check field 然后点 OK,然后再生成
5.2 静态工具
有的时候Service之间也会相互调用,如果使用@Autowired相互注入,就会产生循环依赖问题。为了避免这种情况,MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能。使用的时候有些方法需要提供字节码类(SomeClassName.class)。
当然也可以直接去使用mapper,能不用Service就不用。
—— 摘自黑马 day01-MybatisPlus - 飞书云文档
5.2.1 案例——静态工具查询
需求介绍:
① 改造根据id查询用户的接口,查询用户的同时,给出用户对应的所有地址;
② 改造根据id批量查询用户的接口,查询用户的同时,查询出用户对应的所有地址;
③ 实现根据用户id查询收货地址功能,需要验证用户状态,冻结用户抛出异常。
① 改造根据id查询用户的接口,查询用户的同时,给出用户对应的所有地址。
AddressVO 实体类:
package com.itheima.mp.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @author 虎哥
* @since 2023-07-01
*/
@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 加字段(无需加到User实体类,也就是说User类中不含address):
@ApiModelProperty("用户收货地址列表")
private List<AddressVO> addresses;UserController 直接调 UserService
@ApiOperation("根据id查询用户(含地址)")
@GetMapping("/{id}")
public UserVO getUserById(@ApiParam("用户id") @PathVariable("id") Long id) {
return userService.queryUserAndAddressById(id);
}IUserService 添加对应的方法声明:
UserVO queryUserAndAddressById(Long id);UserServiceImpl 实现对应方法:
@Override
public UserVO queryUserAndAddressById(Long id) {
// 1、查询用户
User user = getById(id);
if (user == null || user.getStatus() == 2) {
throw new RuntimeException("用户状态异常!");
}
// 2、查询地址
List<Address> addresses = Db.lambdaQuery(Address.class)
.eq(Address::getUserId, id).list();
// 3、封装 VO
// 3.1 转 User 的 PO 为 VO
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
// 3.2 转 Address 的 PO 为 VO
if (!CollUtil.isEmpty(addresses)) {
userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
}
return userVO;
}② 改造根据id批量查询用户的接口,查询用户的同时,查询出用户对应的所有地址;
UserController:
@ApiOperation("用id批量查用户(含地址)")
@GetMapping
public List<UserVO> listUsersByIds(
@ApiParam("用户id列表") @RequestParam("ids") List<Long> ids) {
return userService.queryUserAndAddressByIds(ids);
}IUserService 添加对应的方法声明:
List<UserVO> queryUserAndAddressByIds(List<Long> ids);UserServiceImpl 实现对应方法:
场景:要查多个User,每个User又对应多个地址
相当好的案例,之前不知道如何优雅地处理这种情况,建议熟读并背诵 🤣
@Override
public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {
// 1、查询用户
List<User> users = listByIds(ids);
if (CollUtil.isEmpty(users)) {
return Collections.emptyList();
}
// 2、查询地址
// 2.1 获取用户id集合
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
// 2.2 根据用户id查询地址
List<Address> addresses = Db.lambdaQuery(Address.class)
.in(Address::getUserId, userIds).list();
// 2.3 转换地址 VO
List<AddressVO> addressVOList = BeanUtil.copyToList(addresses, AddressVO.class);
// 2.4 对用户地址集合分组,相同用户的放入一个集合中
Map<Long, List<AddressVO>> addressMap = new HashMap<>(0);
if (CollUtil.isNotEmpty(addressVOList)) {
addressMap = addressVOList
.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
}
// 3、转 VO 返回
List<UserVO> list = new ArrayList<>(users.size());
for (User user : users) {
// 3.1 转 User 的 PO 为 VO
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
// 3.2 转换 Address 的 PO 为 VO
userVO.setAddresses(addressMap.get(user.getId()));
// 3.3 添加到集合中
list.add(userVO);
}
return list;
}5.3 逻辑删除
注意:只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。
MybatisPlus提供了对逻辑删除的支持,无需改变代码中调用CRUD的方式,只需要修改配置,即可为所有的操作自动拼接:
-- 用 deleted = 0 表示未删除
WHERE deleted = 05.3.1 配置步骤
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除的字段名(类型为boolean、integer)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)-- 添加逻辑删除字段的SQL
alter table address add deleted bit default b'0' null comment '逻辑删除';5.3.2 编辑测试类
package com.itheima.mp.service;
import com.itheima.mp.domain.po.Address;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class IAddressServiceTest {
@Autowired
private IAddressService addressService;
@Test
void testLogicDelete() {
// 1、删除
addressService.removeById(59L);
// 2、查询
Address address = addressService.getById(59L);
System.out.println("address = " + address);
}
}运行结果:
==> Preparing: UPDATE address SET deleted=1 WHERE id=? AND deleted=0
==> Parameters: 59(Long)
<== Updates: 1
==> Preparing: SELECT id,user_id,province,city,town,mobile,street,contact,is_default,notes,deleted FROM address WHERE id=? AND deleted=0
==> Parameters: 59(Long)
<== Total: 0
address = null逻辑删除本身也有自己的问题,比如:
① 会导致数据库表垃圾数据越来越多,从而影响查询效率
② SQL中全都需要对逻辑删除字段做判断,影响查询效率
因此,虎哥不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。
5.4 枚举处理器
掌握Java中的枚举类型,与数据库字段之间的相互转换
User 类中有一个用户状态字段:
/**
* 使用状态(1正常 2冻结)
*/
private Integer status;5.4.1 创建枚举类
package com.itheima.mp.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FROZEN(2, "冻结");
@EnumValue // 告诉 mybatis-plus 这个字段是枚举值
private final int value;
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
5.4.2 配置枚举处理器
配置枚举处理器类型为:MybatisEnumTypeHandler
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler5.4.3 修改User和UserVO
/**
* 使用状态(1正常 2冻结)
*/
private Integer status; --> private UserStatus status;public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
@Transactional
public void deductBalance(Long id, Integer money) {
...
[-] if (user == null || user.getStatus() == 2) {
[+] if (user == null || user.getStatus() == UserStatus.FROZEN) {
...
}
}其余的魔法值也都改成 UserStatus.NORMAL 或者 UserStatus.FROZEN
前端查询到的结果:
{
"id": 1,
"username": "Jack",
"info": "{\"age\": 20, \"intro\": \"佛系青年\", \"gender\": \"male\"}",
"status": "NORMAL",
"balance": 2400,
"addresses": [ ]
}可以看到默认返回的是枚举变量名,而非枚举值!
SpringMVC 处理 JSON 默认使用的是 Jackson 包,配置方式如下:
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FROZEN(2, "冻结");
@EnumValue // 告诉 mybatis-plus 这个字段是枚举值
[+] @JsonValue
private final int value;
private final String desc;
...
}@JsonValue 加在 value 上,返回的就是value值(1或2);加在 desc 上,返回的就是"正常"或"冻结"
5.5 JSON处理器
用于处理对象嵌套对象查询的自动映射,例如User类有个UserInfo的属性🤠
数据库的user表中有一个info字段,是JSON类型,而User实体类中是String类型,这样一来,我们要读取info中的属性时就非常不方便。这时就可以使用JSON处理器
5.5.1 定义UserInfo实体类
package com.itheima.mp.domain.po;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
// 提供了静态方法,用于创建对象
@AllArgsConstructor(staticName = "of")
public class UserInfo {
private Integer age;
private String intro;
private String gender;
}这里的 "of" 注解可以实现:UserInfo.of(24, "英文老师", "female") 直接创建对象
5.5.2 修改User和UserVO实体类
@Data
[+] @TableName(value = "user", autoResultMap = true)
public class User {
...
[+] @TableField(typeHandler = JacksonTypeHandler.class)
private UserInfo info;
...
}六、插件功能
MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:
PaginationInnerInterceptor:分页插件TenantLineInnerInterceptor:多租户DynamicTableNameInnerInterceptor:动态表名OptimisticLockerInnerInterceptor:乐观锁IllegalSQLInnerInterceptor:sql 性能规范BlockAttackInnerInterceptor:防止全表更新与删除
6.1 分页插件
分页功能是业务开发中最常见的功能,MybatisPlus 没有限制分页的方式。这意味着我们可以使用 PageHelper 来实现。当然 MybatisPlus 也提供了分页实现,我们将在下文探索。
6.1.1 配置分页插件
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 MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 1、初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 2、初始化分页插件
PaginationInnerInterceptor pageInterceptor =
new PaginationInnerInterceptor(DbType.MYSQL);
pageInterceptor.setMaxLimit(1000L); // 单页最大数量
// 3、添加分页插件到核心插件(如需使用其他插件,同理,new出来然后add即可)
interceptor.addInnerInterceptor(pageInterceptor);
return interceptor;
}
}MybatisPlus 3.5.9 版本及以上需要添加 mybatis-plus-jsqlparser 依赖
6.1.2 测试分页查询
...
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@SpringBootTest
class IUserServiceTest {
...
@Test
void testPageQuery() {
int pageNo = 1, pageSize = 2;
// 1、准备分页条件
// 1.1 分页条件
Page<User> page = Page.of(pageNo, pageSize);
// 1.2 排序条件
// 条件1:余额排序
page.addOrder(new OrderItem("balance", true));
// 条件2:条件1满足后再按id排
page.addOrder(new OrderItem("id", true));
// 2、利用条件查询
Page<User> resultPage = userService.page(page);
// 3、解析查询结果(其实page和resultPage是同一个对象)
long total = resultPage.getTotal();
System.out.println("total = " + total);
long pages = resultPage.getPages();
System.out.println("pages = " + pages);
List<User> records = resultPage.getRecords();
records.forEach(System.out::println);
}
}运行结果:
==> Preparing: SELECT COUNT(*) AS total FROM user
==> Parameters:
<== Total: 1
==> Preparing: SELECT id, username, password, phone, info, status, balance, create_time, update_time FROM user ORDER BY balance ASC, id ASC LIMIT ?
==> Parameters: 2(Long)
<== Total: 2
total = 5
pages = 3
User(id=5, username=Lucy, password=123, phone=18688990011, info=UserInfo(age=24, intro=英文老师, gender=female), status=NORMAL, balance=200, createTime=2025-05-06T10:40:04, updateTime=2025-05-06T10:40:04)
User(id=2, username=Rose, password=123, phone=13900112223, info=UserInfo(age=19, intro=青涩少女, gender=female), status=NORMAL, balance=800, createTime=2023-05-19T21:00:23, updateTime=2025-05-06T10:25:37)6.2 通用分页实体
分页接口规范如下:
| 参数 | 说明 |
| 请求参数 见:6.2.1、6.2.2 | |
结果参数
| |
| 特殊说明 | 如果排序字段为空,默认按照更新时间排序 |
6.2.1 新建查询条件实体
package com.itheima.mp.domain.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 用于接收前端的传参
*/
@ApiModel(description = "分页查询条件实体")
@Data
public class PageQuery {
@ApiModelProperty("当前页码")
private Integer pageNo;
@ApiModelProperty("每页记录数")
private Integer pageSize;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否正序")
private Boolean isAsc;
}
6.2.2 用户查询实体扩展
package com.itheima.mp.domain.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 用户查询条件实体
*/
[+] @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;
}@EqualsAndHashCode(callSuper = true) 的核心价值:在继承场景中,确保子类的 equals() 和 hashCode() 方法能够正确比较父类字段,避免因字段遗漏导致的逻辑错误。例如以下示例:
@Data @EqualsAndHashCode(callSuper = false) // 默认值
class SubClass extends SuperClass {
private String field;
}
@Data
class SuperClass {
private int id;
}如果 SubClass 的 equals() 和 hashCode() 未调用父类方法,则比较仅基于 field,而忽略 id。这可能导致两个 id 不同的 SubClass 对象被错误地认为相等。
6.2.3 请求结果实体
package com.itheima.mp.domain.dto;
import ...
/**
* 分页查询结果
*/
@Data
@ApiModel(description = "分页查询结果")
public class PageResultDTO<T> {
@ApiModelProperty("总记录数")
private Long total;
@ApiModelProperty("总页数")
private Long pages;
@ApiModelProperty("当前页数据")
private List<T> list;
}
6.2.4 业务逻辑编写
public class UserController {
...
@ApiOperation("根据条件分页查询用户")
@GetMapping("/page")
public List<UserVO> queryUsersPage(UserQuery query) {
return userService.queryUsersWithPage(query);
}
}
package com.itheima.mp.service.impl;
import ...
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
...
@Override
public PageResultDTO<UserVO> queryUsersWithPage(UserQuery query) {
Integer status = query.getStatus();
String name = query.getName();
// 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> pageResult = lambdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.page(page);
// 3、封装分页结果
PageResultDTO<UserVO> dto = new PageResultDTO<>();
List<User> result = pageResult.getRecords();
if (CollUtil.isEmpty(result)) {
dto.setList(Collections.emptyList());
return dto;
}
dto.setPages(pageResult.getPages());
dto.setTotal(pageResult.getTotal());
dto.setList(BeanUtil.copyToList(result, UserVO.class));
// 4、返回
return dto;
}
}6.3 分页代码封装
6.3.1 改造分页查询实体
package com.itheima.mp.domain.query;
import ...
/**
* 用于接收前端的传参
*/
@ApiModel(description = "分页查询条件实体")
@Data
public class PageQuery {
@ApiModelProperty("当前页码")
private Integer pageNo = 1;
@ApiModelProperty("每页记录数")
private Integer pageSize = 5;
@ApiModelProperty("排序字段")
private String sortBy;
@ApiModelProperty("是否正序")
private Boolean isAsc = true;
public <T> Page<T> toMpPage(OrderItem ... items) {
// 1、构建分页条件
// 1.1 分页条件
Page<T> page = Page.of(pageNo, pageSize);
// 1.2 排序条件
if (StrUtil.isNotBlank(sortBy)) {
// 如果条件不为空
page.addOrder(new OrderItem(sortBy, isAsc));
} else if (items != null) {
// 为空,则按传入OrderItem参数排序
page.addOrder(items);
}
return page;
}
public <T> Page<T> toMpPage(String defaultSortBy, Boolean defaultAsc) {
return toMpPage(new OrderItem(defaultSortBy, defaultAsc));
}
public <T> Page<T> toMpPageOrderByCreateTime() {
return toMpPage(new OrderItem("create_time", false));
}
public <T> Page<T> toMpPageOrderByUpdateTime() {
return toMpPage(new OrderItem("update_time", false));
}
}
6.3.2 改造分页结果实体
方法1:PO转VO的逻辑写死为 BeanUtil.copyToList(数据结果集, 目标类);
方法2:使用函数式接口,由调用者提供PO转VO的逻辑。
package com.itheima.mp.domain.dto;
import ...
/**
* 分页查询结果
*/
@Data
@ApiModel(description = "分页查询结果")
public class PageResultDTO<T> {
@ApiModelProperty("总记录数")
private Long total;
@ApiModelProperty("总页数")
private Long pages;
@ApiModelProperty("当前页数据")
private List<T> list;
public <PO, VO> PageResultDTO<VO> getPageResult(Page<PO> page,
Class<VO> clazz) {
// 3、封装分页结果
PageResultDTO<VO> dto = new PageResultDTO<>();
List<PO> result = page.getRecords();
if (CollUtil.isEmpty(result)) {
dto.setList(Collections.emptyList());
return dto;
}
dto.setPages(page.getPages());
dto.setTotal(page.getTotal());
dto.setList(BeanUtil.copyToList(result, clazz));
return dto;
}
public <PO, VO> PageResultDTO<VO> getPageResult(Page<PO> page,
Function<PO, VO> convertor) {
// 3、封装分页结果
PageResultDTO<VO> dto = new PageResultDTO<>();
List<PO> result = page.getRecords();
if (CollUtil.isEmpty(result)) {
dto.setList(Collections.emptyList());
return dto;
}
dto.setPages(page.getPages());
dto.setTotal(page.getTotal());
dto.setList(result.stream().map(convertor).collect(Collectors.toList()));
return dto;
}
}6.3.3 改造业务逻辑
package com.itheima.mp.service.impl;
import ...
@Service
public class UserServiceImpl extends ... implements ... {
...
@Override
public PageResultDTO<UserVO> queryUsersWithPage(UserQuery query) {
Integer status = query.getStatus();
String name = query.getName();
// 使用重构的方法:
Page<User> page = query.toMpPage(query.getSortBy(), query.getIsAsc());
// 2、分页查询
Page<User> pageResult = lambdaQuery()
.like(name != null, User::getUsername, name)
.eq(status != null, User::getStatus, status)
.page(page);
// 调用方法1:直接复制属性
//PageResultDTO<UserVO> dto = new PageResultDTO<>()
// .getPageResult(pageResult, UserVO.class);
// 调用方法2:使用转换器,处理复杂转换
PageResultDTO<UserVO> dto = new PageResultDTO<>()
.getPageResult(pageResult, user -> {
// 拷贝基础属性
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
userVO.setUsername(user.getUsername()
.substring(0, user.getUsername().length() - 2) + "**");
return userVO;
});
return dto;
}
}当前封装的两个方法(①根据前端查询条件构建Page ②根据page()的查询结果构建PageResultDTO)都是放到了实体的内部,和实体耦合在一起,有些情况下可能会出问题,例如公司里面用的不是MybatisPlus。可以采用单独另外封装工具类,而不是直接写在Query和VO的实体里面。
完结撒花 🎉
