# MybatisPlus教程-黑马

官网: https://www.baomidou.com/

教程文档: https://ai.feishu.cn/wiki/PsyawI04ei2FQykqfcPcmd7Dnsc

目录:

  • 快速入门
  • 核心功能
  • 扩展功能
  • 插件功能

# 1. 快速入门

目录:

  • 入门案例
    • 学会 MP 的基本用法
    • 体会 MP 的无侵入和方便快捷的特点
  • 常见注解
  • 常见配置

# 1.1. 入门案例

使用 MybatisPlus 的基本步骤:

  1. 引入 MybatisPlus 依赖,代替 Mybatis 依赖
  2. 定义 Mapper 接口并继承 BaseMapper

# 1.1.1. 引入依赖

MyBatisPlus 官方提供了 starter,其中集成了 Mybatis 和 MybatisPlus 的所有功能,并且实现了自动装配效果。

因此我们可以用MybatisPlus的starter代替Mybatis的starter:

<!--
<dependency>
   <groupId>org.mybatis.spring.boot</groupId>
   <artifactId>mybatis-spring-boot-starter</artifactId>
   <version>2.3.1</version>
</dependency>
-->
<dependency>
   <groupId>com.baomidou</groupId>
   <artifactId>mybatis-plus-boot-starter</artifactId>
   <version>3.5.3.1</version>
</dependency>

# 1.1.2. 定义 Mapper 接口

自定义的 Mapper 继承 MybatisPlus 提供的 BaseMapper 接口:

public interface UserMapper extends BaseMapper<User> {
   
}

继承 BaseMapper 后,就有如下 增删改查方法:

// 增
insert(T entity): int

// 删
deleteById(Serializable id): int
deleteById(T entity): int
deleteByMap(Map<String, Object> columnMap): int
delete(Wrapper<T> queryWrapper): int
deleteBatchIds(Collection<?> idList): int

// 改
updateById(T entity): int
update(T entity, Wrapper<T> updateWrapper): int

// 查
selectById(Serializable id): T
selectBatchIds(Collection<? extends Serializable> idList): List<T>
selectByMap(Map<String, Object> columnMap): List<T>
selectOne(Wrapper<T> queryWrapper): T

selectCount(Wrapper<T> queryWrapper): Long
selectList(Wrapper<T> queryWrapper): List<T>
selectMaps(Wrapper<T> queryWrapper): List<Map<String, Object>>
selectObjs(Wrapper<T> queryWrapper): List<Object>
selectPage(P page, Wrapper<T> queryWrapper): P
selectMapsPage(P page, Wrapper<T> queryWrapper): P

exists(Wrapper<T> queryWrapper): boolean

# 1.2. 常见注解

# 1.2.1. 原理

MyBatisPlus 通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。

1.根据 BaseMapper<User> 中的 User 实体类信息

public interface UserMapper extends BaseMapper<User> { }

@Data
public class User {
    private Long id;
    private String username;
    private String password;
    private String phone;
    private String info;
    private Integer status;
    private Integer balance;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

2.解析实体类获取表信息

  • 表名: 类名 驼峰转下划线
  • 主键: 名为 id 的成员变量
  • 字段名: 成员变量名 驼峰转下划线

以上是按约定来,如果不按约定来则需要显式设置

# 1.2.2. 基础注解

MybatisPlus 中比较常用的几个注解如下:

  • @TableName : 指定表名
  • @TableId : 指定表中的主键字段信息
  • @TableField : 指定表中的普通字段信息

@TableName:

  • 当 类名转下划线 后,与数据库表名不一致,则需要显式设置

@TableId:

  • 设置主键字段及类型(type)
  • 类型的值为 IdType 枚举类型
  • IdType.AUTO : 数据库自增长
  • IdType.INPUT : 通过set方法自行输入
  • IdType.ASSIGN_ID : 默认。分配 ID,接口 IdentifierGenerator 的方法 nextId 来生成 id,默认实现类为 DefaultIdentifierGenerator 雪花算法

@TableField:

  • 成员变量名与数据库字段名不一致
  • 成员变量名以 is 开头,且是布尔值。(反射时通常会去掉 is)
    • MySQL不原生支持布尔类型,但 bit(1) 或‌ TINYINT(1) 常作为替代方案
    • bit(1) 更直接映射二进制逻辑,而 TINYINT(1) 兼容性更广
    • bit 类型存储二进制值,每个位只能是 0 或 1,基础长度为 1 位,默认长度为 1。‌‌
  • 成员变量名与数据库关键字冲突。(需要转义)
  • 成员变量不是数据库字段

示例:

@TableName("tb_user")
public class User {
   @TableId(value="id", type=IdType.AUTO)
   private Long id;

   @TableField("username")
   private String name;

   @TableField("is_married")
   private Boolean isMarried; // 对应的数据库字段类型 bit(1)

   @TableField("`order`")
   private Integer order;

   @TableField(exist = false)
   private String address;
}

# 1.2.3. 总结

MybatisPlus 是如何获取实现CRUD的数据库表信息的?

  • 默认以类名驼峰转下划线作为表名
  • 默认把名为id的字段作为主键
  • 默认把变量名驼峰转下划线作为表的字段名

MybatisPlus 的常用注解有哪些?

  • @TableName:指定表名称及全局配置
  • @TableId:指定id字段及相关配置
  • @TableField:指定普通字段及相关配置

IdType的常见类型有哪些?

  • AUTO
  • ASSIGN_ID
  • INPUT

使用 @TableField 的常见场景是?

  • 成员变量名与数据库字段名不一致
  • 成员变量名以is开头,且是布尔值
  • 成员变量名与数据库关键字冲突
  • 成员变量不是数据库字段

# 1.3. 常见配置

MyBatisPlus 的配置项继承了 MyBatis 原生配置和一些自己特有的配置。例如:

mybatis-plus:
  # 别名扫描包  
  # 在 mapper.xml 文件中使用实体类时,可以省略全路径
  type-aliases-package: com.itheima.mp.domain.po 

  # Mapper.xml文件地址,默认值
  # MP 擅长单表增删改查,复杂 SQL 语句还是要在 mapper.xml 文件中写
  mapper-locations: "classpath*:/mapper/**/*.xml" 
  
  configuration:    
    # 是否开启下划线和驼峰的映射    
    map-underscore-to-camel-case: true 
  
    # 是否开启二级缓存
    # 一般不开启
    cache-enabled: false 
  
  global-config:    
    db-config:      
      # id 为雪花算法生成      
      id-type: assign_id 
      # 更新策略:只更新非空字段
      update-strategy: not_null 

配置文档: https://www.baomidou.com/reference/

# 1.4. 总结

MyBatisPlus使用的基本流程是什么?

  • 引入起步依赖
  • 自定义 Mapper 基础 BaseMapper
  • 在实体类上添加注解声明 表信息
  • 在 application.yml 中根据需要添加配置

# 2. 核心功能

目录:

  • 条件构造器
  • 自定义 SQL
  • Service 接口

# 2.1. 条件构造器

# 2.1.1. 介绍

MyBatisPlus 支持各种复杂的 where 条件,可以满足日常开发的所有需求。

Wrapper 类的继承结构(部分):

Wrapper
  AbstractWrapper
    QueryWrapper
    UpdateWrapper
    AbstractLambdaWrapper
      LambdaUpdateWrapper
      LambdaQueryWrapper

AbstractWrapper 里存在各种构造条件方法: (构造 where 语句)

allEq(boolean condition, Map<R, V> params, boolean null2IsNull)

eq(boolean condition, R column, Object val)
ne(boolean condition, R column, Object val)
gt(boolean condition, R column, Object val)
ge(boolean condition, R column, Object val)
lt(boolean condition, R column, Object val)
le(boolean condition, R column, Object val)
like(boolean condition, R column, Object val)
likeLeft(boolean condition, R column, Object val)
likeRight(boolean condition, R column, Object val)
notLike(boolean condition, R column, Object val)
notLikeLeft(boolean condition, R column, Object val)
notLikeRight(boolean condition, R column, Object val)

// ......

QueryWrapper 查询相关的方法: (构造 SELECT 语句)

// SELECT 语句

// 比如: SELECT id,username,info,balance 
select(String... columns)
select(List<String> columns)
select(Class<T> entityClass, Predicate<TableFieldInfo> predicate)

UpdateWrapper 更新相关的方法: (构造 SET 语句)

// SET 语句

// 比如: SET balance = balance - 200    
setSql(boolean condition, String sql)

AbstractLambdaWrapper/LambdaUpdateWrapper/LambdaQueryWrapper

  • 基于 lambda 语法。推荐使用
  • 使用方式与 非lambda 一致,可以避免硬编码
  • QueryWrapper.lambda() 返回 LambdaQueryWrapper
  • UpdateWrapper.lambda() 返回 LambdaUpdateWrapper

# 2.1.2. 案例: 基于 QueryWrapper 的查询

需求1: 查询出名字中带 o 的,存款大于等于 1000 元的人的 id、username、info、balance 字段:

/*
SELECT id,username,info,balance 
FROM user 
WHERE username LIKE ? AND balance >= ?
*/

// 1. 构造查询条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
        .select("id","username","info","balance" )
        .like("username", "o")
        .ge("balance", 1000);

// lambda 方式避免硬编码
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);

需求2: 更新用户名为 jack 的用户的余额为 2000

/*
UPDATE user     
SET balance = 2000     
WHERE (username = "jack")
*/

// 1. 要更新的数据
User user = new User();
user.setBalance(2000);

// 2. 更新的条件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
        .eq("username", "jack");

// 3. 执行更新
int updateCount = userMapper.update(user, wrapper);

System.out.println("updateCount = " + updateCount); //=> 1

需求3: 更新 id 为 1,2,4 的用户的余额,扣 200

/*
UPDATE user     
SET balance = balance - 200     
WHERE id in (1, 2, 4)
*/

List<Long> ids = List.of(1L, 2L, 4L);

UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
        .setSql("balance = balance - 200   ")
        .in("id", ids);

int updateCount = userMapper.update(null, wrapper);

System.out.println("updateCount = " + updateCount); //=> 3

# 2.1.3. 总结

条件构造器的用法:

  • QueryWrapperLambdaQueryWrapper 通常用来构建 select、delete、update 的 where 条件部分
  • UpdateWrapperLambdaUpdateWrapper 通常只有在 set 语句比较特殊才使用
  • 尽量使用 LambdaQueryWrapperLambdaUpdateWrapper ,避免硬编码

# 2.2. 自定义 SQL

# 2.2.1. 介绍

利用 MyBatisPlus 的 Wrapper 来构建复杂的 Where 条件,然后自己定义 SQL 语句中剩下的部分。

需求:将 id 在指定范围的用户(例如 1、2、4 )的余额扣减指定值

<!-- 直接在 mapper.xml 文件中写,WHERE 相对来说很繁琐 -->
<update id="updateBalanceByIds">    
  UPDATE user     
  SET balance = balance - #{amount}    
  WHERE id IN    
    <foreach collection="ids" separator="," item="id" open="(" close=")">        
    #{id}    
    </foreach>
</update>
// setSql() 直接在 业务层写 SQL 语句,违反开发规范

List<Long> ids = List.of(1L, 2L, 4L);
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
        .setSql("balance = balance - 200   ")
        .in("id", ids);
int updateCount = userMapper.update(null, wrapper);
<!-- 这种复杂的 SELECT 子句如果通过 MP 的 select() 直接写 SQL 就会违反开发规范 -->
<update id="selectXxx">
  SELECT status, COUNT(id) AS total
  FROM tb_user
  <where>    
    <if test="name != null">AND username LIKE #{name}</if>    
    <if test="ids != null">        
      AND id IN        
        <foreach collection="ids" open="(" close=")" item="id" separator=",">            
          #{id}        
        </foreach>    
    </if>
  </where>
  GROUP BY status
</update>

# 2.2.2. 步骤

步骤1: 基于 Wrapper 构建 WHERE 条件

List<Long> ids = List.of(1L, 2L, 4L);
int amount = 200;

// 1.构建条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId, ids);

// 2.自定义 SQL 方法调用
userMapper.updateBalanceByIds(wrapper, amount);

/*
==>  Preparing: UPDATE user SET balance = balance - ? WHERE (id IN (?,?,?))
==> Parameters: 200(Integer), 1(Long), 2(Long), 4(Long)
<==    Updates: 3
  */

步骤2: 在 mapper 方法参数中用 @Param("ew") 声明 wrapper 变量名称

public interface UserMapper extends BaseMapper<User> {
    // 建议使用 @Param(Constants.WRAPPER) 替代 @Param("ew")  
    // com.baomidou.mybatisplus.core.toolkit.Constants.Constants.WRAPPER = "ew"
    void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);
}

步骤3: 自定义SQL,并使用 Wrapper 条件

<update id="updateBalanceByIds">
    UPDATE user
    SET balance = balance - #{amount}
    <!-- 下面就是 wrapper 构造的 WHERE 子句 -->
    ${ew.customSqlSegment}
</update>

# 2.3. IService 基本用法

# 2.3.1. 介绍

增:

save(T entity): boolean
saveBatch(Collection<T> entityList)   : boolean
saveBatch(Collection<T> entityList)   : boolean

删:

remove(Wrapper<T> queryWrapper)                   : boolean

removeById(Serializable id)                       : boolean
removeById(Serializable id, boolean useFill)      : boolean
removeById(T entity)                              : boolean

removeByMap(Map<String, Object> columnMap)        : boolean

// 单个 DELETE 语句,where id in (?,?,?),适合一千个以内
removeByIds(Collection<?> list)                   : boolean
removeByIds(Collection<?> list, boolean useFill)  : boolean

// 多个 DELETE 语句,批处理,数据量大时 效率高
removeBatchByIds(Collection<?> list)    : boolean
removeBatchByIds(Collection<?> list, boolean useFill) : boolean
removeBatchByIds(Collection<?> list, int batchSize)   : boolean
removeBatchByIds(Collection<?> list, int batchSize, boolean useFill)  : boolean

改:

update(Wrapper<T> updateWrapper)            : boolean
update(T entity, Wrapper<T> updateWrapper)  : boolean

updateById(T entity)    : boolean

updateBatchById(Collection<T> entityList)                 : boolean
updateBatchById(Collection<T> entityList, int batchSize)  : boolean

增或改:(无 ID 则 INSERT,有 ID 则 UPDATE)

saveOrUpdate(T entity)                          : boolean
saveOrUpdate(T entity, Wrapper<T> updateWrapper): boolean

saveOrUpdateBatch(Collection<T> entityList)                 : boolean
saveOrUpdateBatch(Collection<T> entityList, int batchSize)  : boolean

查(单个):

getById(Serializable id)                          : T

getOne(Wrapper<T> queryWrapper)                   : T
getOne(Wrapper<T> queryWrapper, boolean throwEx)  : T

查(列表):

// 无参,查所有
list()                            : List<T>
list(Wrapper<T> queryWrapper)     : List<T>

listByIds(Collection<? extends Serializable> idList)  : List<T>
listByMap(Map<String, Object> columnMap)              : List<T>

listMaps()                        : List<Map<String, Object>>
listMaps(Wrapper<T> queryWrapper) : List<Map<String, Object>>

listObjs()                        : List<Object>
listObjs(Wrapper<T> queryWrapper) : List<Object>
listObjs(Function<? super Object, V> mapper)                          : List<V>
listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper) : List<V>

查(分页):

page(E page)                          : E
page(E page, Wrapper<T> queryWrapper) : E

pageMaps(E page)                          : E
pageMaps(E page, Wrapper<T> queryWrapper) : E

查(总数):

count()                         : long
count(Wrapper<T> queryWrapper)  : long

Lambda:(查询或更新 时构造复杂条件)

lambdaQuery()         : LambdaQueryChainWrapper<T>
lambdaQuery(T entity) : LambdaQueryChainWrapper<T>
lambdaUpdate()        : LambdaUpdateChainWrapper<T>

# 2.3.2. 使用

步骤1: 自定义接口继承 IService<实体类>

public interface IUserService extends IService<User> {
}

步骤2: 自定义接口的实现类 继承 IService 的实现类(ServiceImpl<Mapper,实体类>)

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}

public interface UserMapper extends BaseMapper<User> {
}

测试:

@SpringBootTest
class UserServiceImplTest {
    @Autowired
    private IUserService userService;

    @Test
    void testList() {
        List<User> list = userService.list();
        list.forEach(System.out::println);
    }
}

# 2.3.3. 总结

MP 的 Service 接口使用流程是怎样的?

1.自定义 Service 接口继承 IService 接口

public interface IUserService extends IService<User> {}

2.自定义 Service 实现类,实现自定义接口并继承 ServiceImpl 类

public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {}

# 2.4. IService 开发基础业务接口

BaseMapper<User>IService<User> 中的很多方法功能是一致的,实际业务开发时到底使用哪个?

需求:基于 Restful 风格实现下面的接口

编号 接口 请求方式 请求路径 请求参数 返回值
1 新增用户 POST /users 用户表单实体DTO
2 删除用户 DELETE /users/{id} 用户ID
3 根据id查询用户 GET /users/{id} 用户ID 用户VO
4 根据id批量查询 GET /users 用户ID集合 用户VO集合
5 根据id扣减余额 PUT /users/{id}/deduction/{money} 用户ID/扣减金额

说明:

  • PO: Persistent Object, 持久化对象, 如 User
  • DTO: Data Transfer Object, 数据传输对象; 控制层 接收参数使用
  • VO: View Object, 控制层; 返回结果使用

引入依赖: (web 和 swagger)

<!--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>

配置:(swagger)

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.controller

访问 swagger:

  • http://localhost:8080/doc.html

接口需要两个实体:

  • UserFormDTO:代表新增时的用户表单
  • UserVO:代表查询的返回结果
@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;
}


@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 接口方法:

@Api(tags = "用户管理接口")
@RequestMapping("users")
@RestController
// 针对加了 final 的成员变量,会添加到构造函数的形参中,进而会被 spring 注入依赖
@RequiredArgsConstructor
public class UserController {

    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 removeUserById(@ApiParam("用户id") @PathVariable("id") Long userId){
        userService.removeById(userId);
    }

    @ApiOperation("根据id查询用户")
    @GetMapping("/{id}")
    public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long userId){
        // 1.查询用户
        User user = userService.getById(userId);
        // 2.处理vo
        return BeanUtil.copyProperties(user, UserVO.class);
    }

    @ApiOperation("根据id集合查询用户")
    @GetMapping
    public List<UserVO> queryUserByIds(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids){
        // 1.查询用户
        List<User> users = userService.listByIds(ids);
        // 2.处理vo
        return BeanUtil.copyToList(users, UserVO.class);
    }
}

可以看到上述接口都直接在 controller 即可实现,无需编写任何 service 代码,非常方便。

# 2.5. IService 开发复杂业务接口

说明:

  • 在 UserServiceImpl 中已经自动注入了 UserMapper (baseMapper), 通过 this.baseMapper 可以直接调用 UserMapper 的方法

示例:

@RequestMapping("users")
@RestController
@RequiredArgsConstructor
public class UserController {
    private final IUserService userService;

    @PutMapping("/{id}/deduction/{money}")
    public void deductionBalance( @PathVariable("id") Long userId, @PathVariable("money") Integer money){
        userService.deductionBalance(userId, money);
    }
}


public interface IUserService extends IService<User> {
    void deductionBalance(Long userId, Integer money);
}


@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    @Override
    public void deductionBalance(Long id, Integer money) {
        // 1. 查询用户
        User user = this.getById(id);

        // 2. 校验用户状态
        if (user == null || user.getStatus() == 2) {
            throw new RuntimeException("用户状态异常!");
        }

        // 3. 校验余额是否充足
        Integer balance = user.getBalance();
        if (balance < money) {
            throw new RuntimeException("用户余额不足!");
        }

        // 4. 扣减余额
        // this.baseMapper 指向 UserMapper
        this.baseMapper.deductionBalance(id, money);
    }
}


public interface UserMapper extends BaseMapper<User> {
    @Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")
    void deductionBalance(@Param("id") Long id, @Param("money") Integer money);
}

# 2.6. IService 的 Lambda 方法

IService 中还提供了 Lambda 功能来简化我们的复杂查询及更新功能

  • lambdaQuery(): LambdaQueryChainWrapper<T>
  • lambdaUpdate(): LambdaUpdateChainWrapper<T>

# 2.6.1. lambdaQuery

案例 1:实现一个根据复杂条件查询用户的接口,查询条件如下

  • name:用户名关键字,可以为空
  • status:用户状态,可以为空
  • minBalance:最小余额,可以为空
  • maxBalance:最大余额,可以为空

实现:

// 查询条件实体,UserQuery 实体
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
    @ApiModelProperty("用户名关键字")
    private String name;
    @ApiModelProperty("用户状态:1-正常,2-冻结")
    private Integer status;
    @ApiModelProperty("余额最小值")
    private Integer minBalance;
    @ApiModelProperty("余额最大值")
    private Integer maxBalance;
}


// controller 
@ApiOperation("根据复杂条件查询用户")
@GetMapping("/list")
public List<UserVO> deductionBalance(UserQuery query){
    // 1.查询用户
    List<User> users = userService.queryUsers(
            query.getName(),
            query.getStatus(),
            query.getMinBalance(),
            query.getMaxBalance()
    );

    // 2.处理vo
    return BeanUtil.copyToList(users, UserVO.class);
}

// service
@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() 后,无需 new LambdaQueryWrapper()
  • lambdaQuery() 构建完成条件,在最后需要执行查询操作

常用的查询操作: (查看 ChainQuery)

  • .one(): T:最多1个结果
  • .list(): List<T>:返回集合结果
  • .page(E page): E:返回集合结果
  • .count(): Long:返回计数结果
  • .exists(): boolean:是否存在

# 2.6.2. lambdaUpdate

需求:改造根据id修改用户余额的接口,要求如下

  • 如果扣减后余额为 0,则将用户 status 修改为冻结状态(2

实现:

// 加上事务: 可重复读
@Transactional
@Override
public void deductionBalance(Long id, Integer money) {
    // 1. 查询用户
    User user = this.getById(id);

    // 2. 校验用户状态
    if (user == null || user.getStatus() == 2) {
        throw new RuntimeException("用户状态异常!");
    }

    // 3. 校验余额是否充足
    Integer balance = user.getBalance();
    if (balance < money) {
        throw new RuntimeException("用户余额不足!");
    }

    // 4. 扣减余额
    int remainBalance = balance - money;

    lambdaUpdate()
            // 扣减余额: SET balance = ?
            .set(User::getBalance, remainBalance)
            // 如果余额为 0 则改用户状态: SET status = 2
            .set(remainBalance == 0, User::getStatus, 2)
            // WHERE 条件: id = ?
            .eq(User::getId, id)
            // WHERE 条件: balance = ?
            // 乐观锁,多个人同时修改,只有第一个修改成功
            .eq(User::getBalance, balance)
            // 执行更新语句
            .update();
}

# 2.7. IService 的批量新增

需求:批量插入 10 万条用户数据,并作出对比

方式:

  1. 逐条插入
  2. MP 批量插入: 默认情况
  3. MP 批量插入: 开启重写批处理参数后

示例:

@Test
void testSaveOneByOne() {
    long start = System.currentTimeMillis();

    for (int i = 0; i < 10_0000; i++) {
        userService.save(buildUser(i));
    }

    long end = System.currentTimeMillis();

    System.out.println("耗时(秒):" + TimeUnit.MILLISECONDS.toSeconds(end - start));
    //=> 78 秒
}

@Test
void testSaveBatch() {
    // 1. 准备一个容量为 1000 的列表
    List<User> list = new ArrayList<>(1000);

    long start = System.currentTimeMillis();

    for (int i = 1; i <= 10_0000; i++) {
        // 2. 往列表添加 user
        list.add(buildUser(i));

        // 3. 每 1000 条批量插入一次
        if (i % 1000 == 0) {
            userService.saveBatch(list);
            // 4. 清空集合,准备下一批数据
            list.clear();
        }
    }

    long end = System.currentTimeMillis();

    System.out.println("耗时(秒):" + TimeUnit.MILLISECONDS.toSeconds(end - start));
    //=> 未开启批处理优化: 8 秒
    //=> 已开启批处理优化: 8 秒
}


private User buildUser(int i) {
    String num = String.valueOf(i);
    LocalDateTime now = LocalDateTime.now();

    User user = new User();

    user.setUsername("user_" + num);
    user.setPassword(num);
    user.setPhone(num);
    user.setBalance(2000);
    user.setInfo("{\"age\": 18, \"intro\": \"美女\", \"gender\": \"female\"}");
    user.setCreateTime(now);
    user.setUpdateTime(now);

    return user;
}

# 2.7.1. 逐条插入

说明:

  • 每次请求都只发送一个 INSERT 语句
  • 性能极差

原理:

-- 第 1 次请求
INSERT INTO user (col1,...) VALUES(val1,...)


-- ...


-- 第 100000 次请求
INSERT INTO user (col1,...) VALUES(val00000,...)

# 2.7.2. MP 批量插入: 默认情况

说明:

  • 每次网络请求,携带的数据有限,因此批量插入时,将 10 万条数据进行拆分,每次批量处理 1000 条
  • 减少了网络请求次数,从 10万次 优化为 100次
  • SQL 语句仍旧是逐条执行的

原理:

-- 第 1 次请求
INSERT INTO user (col1,...) VALUES(val1,...)
-- ...
INSERT INTO user (col1,...) VALUES(val1000,...)


-- ...


-- 第 100 次请求
INSERT INTO user (col1,...) VALUES(val99001,...)
-- ...
INSERT INTO user (col1,...) VALUES(val100000,...)

# 2.7.3. MP 批量插入: 开启重写批处理参数后

说明:

  • 在MySQL中,rewriteBatchedStatements 是一个 JDBC 连接属性,它用于优化批量更新操作
  • 当你执行大量 INSERTUPDATEDELETE 操作时,将这些操作合并为较少数量的语句可以显著提高性能。
  • 比如将多个 INSERT INTO ... VALUES(...) 合并为一个 INSERT INTO ... VALUES(...),(...)

设置:

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true

原理:

-- 第 1 次请求
INSERT INTO user (col1,...) VALUES(val1,...), ..., (val1000,...)


-- ...


-- 第 100 次请求
INSERT INTO user (col1,...) VALUES(val99001,...), ..., (val100000,...)

补充:

  • 如果不开启 rewriteBatchedStatements,通过在 mapper.xml 中拼 SQL 也可以到达上述效果

# 3. 扩展功能

目录:

  • 代码生成
  • 静态工具
  • 逻辑删除
  • 枚举处理器
  • JSON 处理器

# 3.1. 代码生成

代码生成方式:

  1. 引入代码生成器依赖,编写配置,执行代码来生成
  2. 使用 IDEA 插件 MyBatisX 进行,通过界面来生成
  3. 使用 IDEA 插件 MyBatisPlus 进行,通过界面来生成 【推荐】

# 3.1.1. MyBatisPlus

IDEA 2025.2.5

菜单:

  • 配置数据库: Tools -> Config Database
  • 代码生成器: Tools -> Code Generator

示例:

image-20251207110528691

# 3.2. 静态工具类 Db

Db:

  • MP 提供的静态工具类,具备 IService 的所有功能
  • 该类没有泛型,使用方法时需要指定 实体类的字节码

应用场景:

  • 当两个 service 互相调用时,形成循环依赖,此时可以使用 Db 来解决

示例:

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    @Override
    public UserVO getUserAndAddressById(Long userId) {
        // 1. 查 用户PO
        User user = getById(userId);

        if (user == null || user.getStatus() == 2) {
            throw new RuntimeException("用户状态异常!");
        }

        // 2. 根据 用户ID 查 地址PO集合
        List<Address> addresses = Db.lambdaQuery(Address.class).eq(Address::getUserId, userId).list();

        // 3. 用户PO 转 用户VO
        UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);

        if (CollUtil.isNotEmpty(addresses)) {
            // 4. 地址PO集合 转 地址VO集合
            List<AddressVO> addressVoList = BeanUtil.copyToList(addresses, AddressVO.class);

            // 5. 将 地址PO集合 塞入 用户VO
            userVO.setAddresses(addressVoList);
        }

        return userVO;
    }
}

说明:

  • Db.lambdaQuery(Address.class) 等价于 addressService.lambdaQuery()

需求:

  • 批量查询用户和地址

分析:

  1. 先根据 userIds 查出所有的用户
  2. 根据查出来的用户列表,获取有效的 userIds
  3. 在根据 userIds 查出所有的地址
  4. 对所有的地址进行分组(Map),key 为 userId, value 为该用户的地址列表
  5. 遍历查出来的用户,将该用户的地址列表塞进去

实现:

@Override
public List<UserVO> getUserAndAddressByIds(List<Long> ids) {
    // 1. 先根据 userIds 查出所有的用户
    List<User> userPOs = listByIds(ids);

    if (CollUtil.isEmpty(userPOs)) {
        return Collections.emptyList();
    }

    List<UserVO> userVOs = BeanUtil.copyToList(userPOs, UserVO.class);

    // 2. 根据查出来的用户列表,获取有效的 userIds
    List<Long> userIds = userPOs.stream().map(User::getId).collect(Collectors.toList());

    // 3. 在根据 userIds 查出所有的地址
    List<Address> addressPOs = Db.lambdaQuery(Address.class).in(Address::getUserId, userIds).list();
    List<AddressVO> addressVOs = BeanUtil.copyToList(addressPOs, AddressVO.class);

    if (CollUtil.isEmpty(addressPOs)) {
        return userVOs;
    }

    // 4. 对所有的地址进行分组(Map),key 为 userId, value 为该用户的地址列表
    Map<Long, List<AddressVO>> addressMap  = addressVOs.stream().collect(Collectors.groupingBy(AddressVO::getUserId));

    // 5. 遍历查出来的用户,将该用户的地址列表塞进去
    userVOs.forEach((userVO) -> userVO.setAddresses(addressMap.get(userVO.getId())));

    return userVOs;
}

# 3.3. 逻辑删除

逻辑删除就是基于代码逻辑模拟删除效果,但并不会真正删除数据。

思路如下:

  • 在表中添加一个字段标记数据是否被删除
  • 当删除数据时把标记置为 1
  • 查询时只查询标记为 0 的数据

例如,逻辑删除字段为 deleted

  • 删除操作: UPDATE user SET deleted = 1 WHERE id = 1 AND deleted = 0
  • 查询操作: SELECT * FROM user WHERE deleted = 0

MybatisPlus 提供了逻辑删除功能,无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。

我们要做的就是在 application.yaml 文件中配置逻辑删除的字段名称和值即可:

mybatis-plus:
  global-config:
    db-config:
      # 全局逻辑删除的实体字段名
      # 只要实体类上有该变量,则默认会标记为逻辑删除字段,启用逻辑删除功能
      # 数据库字段类型可以是 boolean 、integer
      # PO 变量的类型可以是 Boolean
      logic-delete-field: deleted 
      
      # 逻辑已删除值(默认为 1)
      logic-delete-value: 1 
      
      # 逻辑未删除值(默认为 0)
      logic-not-delete-value: 0 

address 表添加一个逻辑删除字段:

-- 使用 bit 类型,默认会当成 true/false
alter table address add deleted bit default b'0' null comment '逻辑删除';

逻辑删除本身也有自己的问题,比如:

  • 会导致数据库表垃圾数据越来越多,影响查询效率
  • SQL 中全都需要对逻辑删除字段做判断,影响查询效率

因此,不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。

测试:

@Test
void testLogicDelete() {
    boolean flag = addressService.removeById(59L);
    System.out.println("逻辑删除结果: " + flag);
    //=> true
    //=> UPDATE address SET deleted=1 WHERE id=? AND deleted=0

    Address address = addressService.getById(59L);
    System.out.println("address = " + address);
    //=> null
    //=> SELECT id,user_id,...,deleted FROM address WHERE id=? AND deleted=0
}

# 3.4. 枚举处理器

PO 中枚举类型与数据库字段的相互转换

PO 中的字段类型与数据库中的字段类型,由 MyBatis 的 TypeHandler 进行转换

MP 又拓展了两种处理器:

TypeHandler
    BaseTypeHandler
        MybatisEnumTypeHandler
        AbstractJsonTypeHandler
            GsonTypeHandler
            JacksonTypeHandler
            FastjsonTypeHandler

实体类中使用枚举:

@Data
public class UserVO {
    // ...

    // 数据库字段: `status` int DEFAULT '1' COMMENT '使用状态(1正常 2冻结)',
    private UserStatus status;
}

使用枚举的步骤:

  1. 配置枚举处理器
    • 在配置文件里,设置全局枚举处理器
  2. 数据库字段 转换
    • PO 中的枚举类型变量 与 数据字段 的转换
    • 在枚举类里,通过 @EnumValue 注解来标记枚举属性
  3. JSON 转换
    • Java 对象中的枚举类型变量 与 JSON 属性 的转换
    • 枚举转 JSON 时,默认转化为 枚举的名称(的字符串),即 "NORMAL" / "FREEZE"
    • 在枚举里,使用 @JsonValue 标记转换为哪个属性的值
    • SpringMVC 默认使用 Jackson 处理 JSON,使用 @JsonValue 标记转换时取哪个属性

步骤 1. 配置枚举处理器:

mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler

步骤 2. 在枚举里标记字段属性:

@Getter
public enum UserStatus {
    NORMAL(1, "正常"),
    FREEZE(2, "冻结")
    ;
    @JsonValue // 使用 Jackson ,枚举 与 JSON 转化时,使用 value
    @EnumValue // 使用 MyBatis ,枚举 与 普通值 转化时,使用 value
    private final int value;
    private final String desc;

    UserStatus(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }
}

# 3.5. JSON 处理器

说明:

  • 数据库中 json 类型的字段 与 PO 中属性的转换
  • json 类型 本质上存的还是 JSON 字符串,PO 中使用 String 类型的变量就可以接收

需求:

  • PO 中使用 实体来接收 json 类型的数据

步骤:

  1. 设置 JSON 转换器
    • 在 PO 的对象类型变量上通过 @TableField(typeHandler = JacksonTypeHandler.class) 设置转换器
  2. 开启 autoResultMap
    • 在 PO 的类上通过 @TableName(value = 数据库表名, autoResultMap = true) 开启嵌套对象的自动映射处理

示例:

package com.itheima.mp.domain.po;

@Data
@NoArgsConstructor
// 可以通过 UserInfo.of(...) 来创建对象
@AllArgsConstructor(staticName = "of")
public class UserInfo {
    private Integer age;
    private String intro;
    private String gender;
}


@Data
// MyBatis 中,对象中嵌套对象,需要写 resultMap
// 如果不想写,开启 autoResultMap
@TableName(value = "user", autoResultMap = true)
public class User {
    // ......

    // 该字段使用 JacksonTypeHandler 处理器来进行转换
    @TableField(typeHandler = JacksonTypeHandler.class)
    private UserInfo info;
}

# 4. 插件功能

MP 已有的插件有:

  • PaginationInnerInterceptor:自动分页
  • TenantLineInnerInterceptor:多租户
  • DynamicTableNameInnerInterceptor:动态表名
  • OptimisticLockerInnerInterceptor:乐观锁
  • IllegalSQLInnerInterceptor:sql 性能规范
  • BlockAttackInnerInterceptor:防止全表更新与删除

使用多个插件的时候需要注意插件定义顺序,建议使用顺序如下:

  • 多租户,动态表名
  • 分页,乐观锁
  • sql 性能规范,防止全表更新与删除

# 4.1. 分页插件

配置插件:

package com.itheima.mp.config;

@Configuration
public class MybatisConfig {
    // 要在配置类中注册MyBatisPlus的核心插件,同时添加分页插件
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // 1. 初始化核心插件
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        // 2. 创建 分页插件 拦截器
        PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);        
        // 设置分页上限,避免数据量过大        
        pageInterceptor.setMaxLimit(1000L); 

        // 3. 添加 分页插件 拦截器
        interceptor.addInnerInterceptor(pageInterceptor);

        return interceptor;
    }
}

分页 API:

// page: 分页信息 和 数据
// queryWrapper: 查询条件
<E extends IPage<T>> E page(E page, Wrapper<T> queryWrapper)

<E extends IPage<T>> E page(E page)

public class Page<T> implements IPage<T> {
    protected List<T> records;
    protected long total;
    protected long size;
    protected long current;

    protected List<OrderItem> orders;
    protected boolean optimizeCountSql;
    protected boolean searchCount;
    protected boolean optimizeJoinOfCountSql;
    protected Long maxLimit;
    protected String countId;

    public Page() {
        this.records = Collections.emptyList();
        this.total = 0L;
        this.size = 10L;
        this.current = 1L;
        this.orders = new ArrayList();
        this.optimizeCountSql = true;
        this.searchCount = true;
        this.optimizeJoinOfCountSql = true;
    }
}

示例:

@Test
void testPageQuery() {
    // 页码从 1 开始
    int pageNo = 1;
    int pageSize = 2;

    // 1. 准备分页条件
    // 分页
    Page<User> page = Page.of(pageNo, pageSize);

    // 排序
    // true - 升序; false - 降序
    page.addOrder(new OrderItem("balance", false));
    page.addOrder(new OrderItem("id", true));

    // 2. 分页查询
    userService.page(page);

    // 3. 解析
    // 总条数
    long total = page.getTotal();
    // 总页数
    long pages = page.getPages();
    // 数据
    List<User> records = page.getRecords();

    System.out.println("total = " + total);
    System.out.println("pages = " + pages);
    records.forEach(System.out::println);

    /*
    ==>  Preparing: SELECT COUNT(*) AS total FROM user
    ==>  Preparing: SELECT id, ... FROM user ORDER BY balance DESC, id ASC LIMIT ?
    total = 5
    pages = 3
    */
}

# 4.2. 通用分页实体

通用分页查询实体:

@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
    @ApiModelProperty("页码")
    private Long pageNo;

    @ApiModelProperty("页大小")
    private Long pageSize;

    @ApiModelProperty("排序字段")
    private String sortBy;

    @ApiModelProperty("是否升序")
    private Boolean isAsc;
}

通用分页结果实体:

@Data
@ApiModel(description = "分页结果")
public class PageDTO<T> {
    @ApiModelProperty("总条数")
    private Long total;

    @ApiModelProperty("总页数")
    private Long pages;
    
    @ApiModelProperty("数据集合")
    private List<T> list;
}

示例:

// 查询实体
public class UserQuery extends PageQuery {
    // ......
}


// controller
@ApiOperation("根据分页条件查询用户")
@GetMapping("/page")
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
    return userService.queryUsersPage(query);
}


// serviceImpl
@Override
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
    // 1. 分页条件
    Page<User> page = Page.of(query.getPageNo(), query.getPageSize());

    String sortField = query.getSortBy();
    boolean isAsc = BooleanUtil.isTrue(query.getIsAsc());

    if (StrUtil.isNotBlank(sortField)) {
        page.addOrder(new OrderItem(sortField, isAsc));
    } else {
        // 默认按更新时间倒序
        page.addOrder(new OrderItem("update_time", false));
    }


    // 2. 过滤条件
    String name = query.getName();
    Integer status = query.getStatus();

    // 3. 执行分页查询
    lambdaQuery()
            .like(name != null, User::getUsername, name)
            .eq(status != null, User::getStatus, status)
            .page(page);

    // 4. 封装 PageDTO
    PageDTO<UserVO> dto = new PageDTO<>();

    // 分页信息
    dto.setTotal(page.getTotal());
    dto.setPages(page.getPages());

    // 数据
    List<User> users = page.getRecords();

    if (CollUtil.isEmpty(users)) {
        dto.setList(Collections.emptyList());
        return dto;
    }

    List<UserVO> userVOs = BeanUtil.copyToList(users, UserVO.class);

    dto.setList(userVOs);

    return dto;
}

# 4.3. 通用分页实体与 MP 转换

需求:

  • PageQuery => MP.Page
  • MP.Page => PageDTO

分页查询实体:

@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
    @ApiModelProperty("页码")
    private Long pageNo = 1L;
    @ApiModelProperty("页大小")
    private Long pageSize = 10L;
    @ApiModelProperty("排序字段")
    private String sortBy;
    @ApiModelProperty("是否升序")
    private Boolean isAsc = true;

    // -- 通用的方法
    public <T> Page<T> toMpPage(OrderItem ... defaultOrderItems) {
        // 1. 分页条件
        Page<T> page = Page.of(pageNo, pageSize);

        // 2. 排序条件
        if (StrUtil.isNotBlank(sortBy)) {
            // sortBy 不为空
            page.addOrder(new OrderItem(sortBy, isAsc));
        } else if (defaultOrderItems != null) {
            // sortBy 为空,则使用默认排序
            page.addOrder(defaultOrderItems);
        }

        return page;
    }

    // -- 常用的方法

    // 传参数时,不用 new 对象
    public <T> Page<T> toMpPage(String defaultSortBy, boolean defaultAsc) {
        return toMpPage(new OrderItem(defaultSortBy, defaultAsc));
    }

    // 默认按 创建时间 倒排
    public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {
        return toMpPage("create_time", false);
    }

    // 默认按 更新时间 倒排
    public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {
        return toMpPage("update_time", false);
    }
}

分页结果实体:

@Data
@ApiModel(description = "分页结果")
public class PageDTO<T> {
    @ApiModelProperty("总条数")
    private Long total = 0L;
    @ApiModelProperty("总页数")
    private Long pages = 0L;
    @ApiModelProperty("数据集合")
    private List<T> list = Collections.emptyList();

    // PO 转 VO 时,直接属性拷贝
    public static <PO, VO> PageDTO<VO> of(Page<PO> page, Class<VO> clazz) {
        PageDTO<VO> dto = getPageDTO(page);

        // 3. 当前页数据
        List<PO> poList = page.getRecords();

        if (CollUtil.isEmpty(poList)) {
            dto.setList(Collections.emptyList());
            return dto;
        }

        // 4. PO 转 VO
        dto.setList(BeanUtil.copyToList(poList, clazz));

        return dto;
    }

    // PO 转 VO 时,自定义转换器
    public static <PO, VO> PageDTO<VO> of(Page<PO> page, Function<PO, VO> convertor) {
        PageDTO<VO> dto = getPageDTO(page);

        // 3. 当前页数据
        List<PO> poList = page.getRecords();

        if (CollUtil.isEmpty(poList)) {
            dto.setList(Collections.emptyList());
            return dto;
        }

        // 4. PO 转 VO
        List<VO> voList = poList.stream().map(convertor).collect(Collectors.toList());

        dto.setList(voList);

        return dto;
    }

    private static <PO, VO> PageDTO<VO> getPageDTO(Page<PO> page) {
        PageDTO<VO> dto = new PageDTO<>();

        // 1. 总条数
        dto.setTotal(page.getTotal());

        // 2. 总页数
        dto.setPages(page.getPages());

        return dto;
    }
}

使用:

// serviceImpl
@Override
public PageDTO<UserVO> queryUsersPage(UserQuery query) {
    // 1. 分页条件
    Page<User> page = query.toMpPageDefaultSortByUpdateTimeDesc();

    // 2. 执行分页查询
    String name = query.getName();
    Integer status = query.getStatus();
    lambdaQuery()
            .like(name != null, User::getUsername, name)
            .eq(status != null, User::getStatus, status)
            .page(page);

    // 3. 封装 PageDTO

    return PageDTO.of(page, po -> {
        // 拷贝基础属性
        UserVO vo = BeanUtil.copyProperties(po, UserVO.class);

        // 处理特殊属性
        String username = vo.getUsername();
        username = username.substring(0, username.length() - 2) + "**";
        vo.setUsername(username);

        return vo;
    });
}
本章目录