一、快速入门

1、入门案例(使用MybatisPlus的基本步骤)

1.1 引入MybatisPlus的起步依赖

        引入MybatisPlus的起步依赖官方提供了starter,集成了MybatisMybatisPlus的所有功能,并且实现了自动装配效果。

        因此我们可以用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来构建查询条件,然后手写SELECTFROM部分,实现多表查询。

查询条件这样来构建:

@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中对LambdaQueryWrapperLambdaUpdateWrapper的用法进一步做了简化,无需new一个Wrapper,而是直接调用lambdaQuerylambdaUpdate方法:

@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支持配置文件的加密和解密功能。

我们以数据库的用户名和密码为例。

6.1 生成秘钥

首先,我们利用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.通用分页实体

实现一个用户分页查询的接口,接口规范如下:

  1. 请求方式:GET

  2. 请求路径:/users/page

  3. 请求参数:

    {
        "pageNo": 1,
        "pageSize": 5,
        "sortBy": "balance",
        "isAsc": false,
        "name": "o",
        "status": 1
    }
    
  4. 返回值:
    {
        "total": 100006,
        "pages": 50003,
        "list": [
            {
                "id": 1685100878975279298,
                "username": "user_9****",
                "info": {
                    "age": 24,
                    "intro": "英文老师",
                    "gender": "female"
                },
                "status": "正常",
                "balance": 2000
            }
        ]
    }
    
  5. 特殊说明:
  • 如果排序字段为空,默认按照更新时间排序
  • 排序字段不为空,则按照排序字段排序

这里需要定义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实体

在刚才的代码中,从PageQueryMybatisPlusPage之间转换的过程还是比较麻烦的。

完全可以在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);
    }
}

这样就可以省去从PageQueryPage的转换:

// 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;
    });

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐