MybatisPlus(常见注解、常见配置、条件构造器、自定义SQL、Service接口、代码生成、静态工具、逻辑删除、枚举及JSON处理器、配置加密、分页插件)


该文章为学习笔记,在线文档 https://b11et3un53m.feishu.cn/wiki/FYNkwb1i6i0qwCk7lF2caEq5nRe

一、快速入门

1、使用的基本步骤

(1)引入MybatisPlus 依赖,代替 Mybatis依赖

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

(2)定义 Mapper 接口并继承 BaseMapper

public interface UserMapper extends BaseMapper<User> {
}

(3)在实体类上添加注解声明 表信息

  • 详情见《常见注解》

(4)在application.yml中根据需要添加配置

  • 详情见《常见配置》

2、常见注解

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

符合约定的如下解析

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

不符合约定的添加注解

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

  • @TableName 用来指定表名及全局配置
  • @TableId 用来指定表中的主键字段信息及相关配置
  • @TableField 用来指定表中的普通字段信息及相关配置
@TableName("user")
public class User {

    @TableId(value="id", type=IdType.AUTO)
    private Long id;
    
    private String name;
    private Integer age;
    
    @TableField("isMarried")
    private Boolean isMarried;
    @TableField("concat")
    private String concat;
}

IdType 支持的常见类型有:

  • AUTO 数据库 ID 自增
  • INPUT insert 前自行 set 主键值
  • ASSIGN_ID 分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法)
 @TableId(value="id", type=IdType.AUTO)

使用TableField 的常见场景:

  • 成员变量名与数据库字段名不一致
  • 成员变量名以is开头,且是布尔值
  • 成员变量名与数据库关键字冲突
  • 成员变量不是数据库字段
 // 成员变量名与数据库字段名不一致
 @TableField("username")
 private String name;
 // 成员变量名义is开头,且是布尔值
 @TableField("is_married")
 private Boolean isMarried;
 // 成员变量名与数据库关键字冲突
 @TableField("`order`")
 private Integer order;
 // 成员变量不是数据库字段
 @TableField(exist = false)
 private String address;

3、常见配置

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

mybatis-plus:
  type-aliases-package: com.itheima.mp.domain.po # 别名扫描包
  mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,默认值
  configuration:
    map-underscore-to-camel-case: true # 是否开启下划线和驼峰的映射
    cache-enabled: false # 是否开启二级缓存
  global-config:
     db-config:
	     id-type: assign_id # id为雪花算法生成
	     update-strategy: not_null # 更新策略:只更新非空字段

二、核心功能

1、条件构造器

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

(1)QueryWrapper

  • 无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件。
  • 查询出名字中带o的,存款大于等于1000元的人。代码如下:
@Test
void testQueryWrapper() {
    // 1.构建查询条件 where name like "%o%" AND balance >= 1000
    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);
}
  • 更新用户名为jack的用户的余额为2000,代码如下:
@Test
void testUpdateByQueryWrapper() {
    // 1.构建查询条件 where name = "Jack"
    QueryWrapper<User> wrapper = new QueryWrapper<User>()
    		.eq("username", "Jack");
    // 2.更新数据,user中非null字段都会作为set语句
    User user = new User();
    user.setBalance(2000);
    userMapper.update(user, wrapper);
}

(2)UpdateWrapper

  • 基于BaseMapper中的update方法更新时只能直接赋值,对于一些复杂的需求就难以实现。
  • 例如:更新id为1,2,4的用户的余额,扣200,对应的SQL应该是:
  • UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)
  • SET的赋值结果是基于字段现有值的,这个时候就要利用UpdateWrapper中的setSql功能了:
@Test
void testUpdateWrapper() {
    List<Long> ids = List.of(1L, 2L, 4L);
    // 1.生成SQL
    UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
            .setSql("balance = balance - 200") // SET balance = balance - 200
            .in("id", ids); // WHERE id in (1, 2, 4)
    // 2.更新,注意第一个参数可以给null,也就是不填更新字段和数据,
    // 而是基于UpdateWrapper中的setSQL来更新
    userMapper.update(null, wrapper);
}

(3)LambdaQueryWrapper

  • 无论是QueryWrapper还是UpdateWrapper在构造条件的时候都需要写死字段名称,会出现字符串魔法值。这在编程规范中显然是不推荐的。
  • 其中一种办法是基于变量的gettter方法结合反射技术
  • 因此我们只要将条件对应的字段的getter方法传递给MybatisPlus,它就能计算出对应的变量名了。
  • 而传递方法可以使用JDK8中的方法引用和Lambda表达式。
  • 因此MybatisPlus又提供了一套基于LambdaWrapper,包含两个:
  • LambdaQueryWrapperLambdaUpdateWrapper
  • 分别对应QueryWrapperUpdateWrapper
  • 查询出名字中带o的,存款大于等于1000元的人。代码如下:
@Test
void testLambdaQueryWrapper() {
    // 1.构建条件 WHERE username LIKE "%o%" AND balance >= 1000
    QueryWrapper<User> wrapper = new QueryWrapper<>();
    wrapper.lambda()
            .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、自定义SQL

  • 我们可以利用MyBatisPlusWrapper来构建复杂的Where条件
  • 然后自己定义SQL语句中剩下的部分

(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);

(2)在mapper方法参数中用Param注解声明 wrapper 变量名称,必须是 ew

void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);

(3)自定义SQL,并使用 Wrapper 条件

  • UserMapper.xml
<update id="updateBalanceByIds">
    UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment}
</update>
  • mapper方法参数前添加 Select 注解
  • UserMapper.java
@Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")

3、Service接口

(1)使用流程

  • 自定义 Service 接口继承 IService 接口
public interface IUserService extends IService<User> {
}
  • 自定义 Service 实现类,实现自定义接口并继承 ServiceImpl
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}

(2)接口实现

实现以下接口

在这里插入图片描述

  • 引入依赖 swaggerweb
<!--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.yaml 中配置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
  • 实体:UserFormDTO:代表新增时的用户表单
package com.itheima.mp.domain.dto;

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 io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Api(tags = "用户管理接口")
@RequestMapping("/users")
@RestController
@RequiredArgsConstructor
public class UserController {

    private  final IUserService userService;

    @ApiOperation("新增用户接口")
    @PostMapping
    public void saveUser(@RequestBody UserFormDTO userDTO){
        User user = BeanUtil.copyProperties(userDTO, User.class);
        userService.save(user);
    }

    @ApiOperation("删除用户接口")
    @DeleteMapping("{id}")
    public void deleteUserById(@ApiParam("用户id") @PathVariable("id") Long id){
        userService.removeById(id);
    }

    @ApiOperation("根据id查询用户接口")
    @GetMapping("{id}")
    public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long id){
        User user = userService.getById(id);
        return BeanUtil.copyProperties(user,UserVO.class);
    }

    @ApiOperation("根据id批量查询用户接口")
    @GetMapping
    public List<UserVO> queryUserById(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids){
        List<User> users = userService.listByIds(ids);
        return BeanUtil.copyToList(users,UserVO.class);
    }
}
  • 上述接口都直接在controller即可实现,无需编写任何service代码
  • 不过,一些带有业务逻辑的接口则需要在service中自定义实现

(3)根据id扣减用户余额

  • UserController中定义一个方法
@PutMapping("{id}/deduction/{money}")
@ApiOperation("扣减用户余额")
public void deductBalance(@PathVariable("id") Long id, @PathVariable("money")Integer money){
    userService.deductBalance(id, money);
}
  • 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);
}
  • 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.deductBalance(id,money);

    }
}

  • mapper
@Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")
void deductMoneyById(@Param("id") Long id, @Param("money") Integer money);

(4) 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方法:
@ApiOperation("复杂条件查询用户列表")
@GetMapping("/list")
public List<UserVO> queryUsers(UserQuery userQuery){
   List<User> users = userService.queryUsers(userQuery.getName(),userQuery.getStatus(),userQuery.getMinBalance(),userQuery.getMaxBalance());
   return BeanUtil.copyToList(users,UserVO.class);
}
  • UserService接口
List<User> queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance);
  • UserServiceImpl实现类
@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 )
            .gt(minBalance != null,User::getBalance,minBalance)
            .le(maxBalance!=null,User::getBalance,maxBalance)
            .list();
}

可以发现lambdaQuery方法中除了可以构建条件,还需要在链式编程的最后添加一个list(),这是在告诉MP我们的调用结果需要是一个list集合。这里不仅可以用list(),可选的方法有:

  • .one():最多1个结果
  • .list():返回集合结果
  • .count():返回计数结果
  • lambdaQuery方法类似,IService中的lambdaUpdate方法可以非常方便的实现复杂更新业务

案例二:改造 “根据id扣减用户余额” 的接口,要求如下

  • 如果扣减后余额为0,则将用户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、扣减余额
    // baseMapper.deductBalance(id, money);

    // 5、如果扣减后余额为0,则将用户status修改为冻结状态(2)
    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();
}

4、批量新增

  • 普通for循环新增,逐条插入速度极差,不推荐
  • MP的批量新增,基于预编译的批处理,性能不错
  • 配置jdbc参数,开启rewriteBatchedStatements=true,性能最好

修改项目中的application.yml文件,在jdbcurl后面,
添加参数&rewriteBatchedStatements=true:

在这里插入图片描述

三、扩展功能

1、代码生成

(1)安装插件

  • idea开发工具的plugins市场中搜索并安装MyBatisPlus插件,安装后idea重启

在这里插入图片描述

  • 工具重启后在Tools下出现以下选项【旧版idea直接在导航栏增加 “other” 选项卡了】
    在这里插入图片描述

(2)配置

  • Tools下选择Config Database,出现弹窗,
  • 填写数据库连接的基本信息:端口后mp是数据库表名,后是时区

在这里插入图片描述

  • 点击test connect 测试连接,连接成功如下图
    在这里插入图片描述
  • 连接成功,点击ok即可

(3)生成代码

  • Tools下选择Code Generator,出现弹窗
  • 选择相关数据库表,填写相关信息,如下图

在这里插入图片描述

  • 点击Code Generator生成代码
    在这里插入图片描述

2、静态工具

  • 有的时候Service之间也会相互调用,
  • 为了避免出现循环依赖问题,
  • MybatisPlus提供一个静态工具类:Db

(1)案例一

  • 需求:改造根据id用户查询的接口,查询用户的同时返回用户收货地址列表
  • 添加收货地址的VO对象 AddressVO
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,添加一个地址属性
@ApiModelProperty("收货地址列表")
private List<AddressVO> addresses;

在这里插入图片描述

  • 修改UserController中根据id查询用户的业务接口:
@ApiOperation("根据id查询用户接口")
@GetMapping("/{id}")
public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long id){
    // User user = userService.getById(id);
    // return BeanUtil.copyProperties(user,UserVO.class); 
    return userService.queryUserAndAddressById(id);
}
  • 由于查询业务复杂,所以要在service层来实现。首先在IUserService中定义方法:
UserVO queryUserAndAddressById(Long id);
  • UserServiceImpl中实现该方法
@Override
public UserVO queryUserAndAddressById(Long id) {
    // 1、查询用户
    User user = getById(id);
    // 2、判断用户状态
    if (user == null || user.getStatus() == 2) {
        throw new RuntimeException("用户状态异常");
    }
    // 3.查询收货地址
    List<Address> address = Db.lambdaQuery(Address.class)
    	.eq(Address::getUserId, id)
    	.list();
    UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
    if(CollUtil.isNotEmpty(address)){
        List<AddressVO> addressVO = BeanUtil.copyToList(address, AddressVO.class);
        userVO.setAddresses(addressVO);
    }
    return userVO;
}
  • 在查询地址时,我们采用了Db的静态方法,因此避免了注入AddressService,减少了循环依赖的风险。

(2)案例二

  • 需求:根据id批量查询用户,并查询出用户对应的所有地址
  • 修改UserController中根据id查询用户的业务接口:
@ApiOperation("根据id批量查询用户接口")
@GetMapping
public List<UserVO> queryUserById(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids){
    // List<User> users = userService.listByIds(ids);
    // return BeanUtil.copyToList(users,UserVO.class);
    return userService.queryUserAndAddressByIds(ids);
}
  • service层来实现,IUserService中定义方法:
UserVO queryUserAndAddressById(Long id);
  • UserServiceImpl中实现该方法
@Override
public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {
    // 查询用户
    List<User> users = listByIds(ids);
    if (CollUtil.isEmpty(users)) {
        return Collections.emptyList();
    }
    // 根据id查询地址
    List<Address> addressList = Db.lambdaQuery(Address.class).in(Address::getUserId, ids).list();
    List<AddressVO> addressVOList = BeanUtil.copyToList(addressList, AddressVO.class);
    // 根据userId给地址分组
    Map<Long, List<AddressVO>> addressVoMap = new HashMap<>();
    if (CollUtil.isNotEmpty(addressVOList)) {
        addressVoMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
    }
    // 遍历插入user
    List<UserVO> userVOS = new ArrayList<>(users.size());
    for (User user : users) {
        UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
        userVO.setAddresses(addressVoMap.get(user.getId()));
        userVOS.add(userVO);
    }
    return userVOS;
}

3、逻辑删除

对于一些比较重要的数据,我们往往会采用逻辑删除的方案,即:

  • a、在表中添加一个字段标记数据是否被删除
  • b、当删除数据时把标记置为1
  • c、查询时过滤掉标记为1的数据

一旦采用了逻辑删除,所有的查询和删除逻辑都要跟着变化,非常麻烦。
为了解决这个问题,MybatisPlus就添加了对逻辑删除的支持。

注意,只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。

  • 例如逻辑删除字段为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:
            logic-delete-field: deleted # 全局逻辑删除的实体字段名,字段类型可以是boolean、integer
            logic-delete-value: 1 # 逻辑已删除值(默认为 1)
            logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
  • 注意:

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

  • 会导致数据库表垃圾数据越来越多,影响查询效率
  • SQL中全都需要对逻辑删除字段做判断,影响查询效率
  • 因此,我不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。

4、枚举处理器

(1)定义枚举

  • 定义一个用户状态的枚举:
    在这里插入图片描述
package com.itheima.mp.enums;

import lombok.Getter;

@Getter
public enum UserStatus {
    NORMAL(1, "正常"),
    FREEZE(2, "冻结");

    private final int value;
    private final String desc;

    UserStatus(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }
}
  • User类中的status字段改为UserStatus 类型:
/**
 * 使用状态(1正常 2冻结)
 */
// private Integer status;
private UserStatus status;

(2)枚举与数据库类型自动转换

  • 要实现PO类中的枚举类型变量与数据库字段的转换,需要2步:
  • 第一,给枚举中的与数据库对应value值添加@EnumValue注解

在这里插入图片描述

  • 第二,在application.yaml文件中添加配置:
mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler

(3)测试、@JsonValue

  • 为了使页面查询结果也是枚举格式,我们需要修改UserVO中的status属性

在这里插入图片描述

  • 使用的用户状态,改成枚举形式
// 判断用户状态
// if (user == null || user.getStatus() == 2) {
if (user == null || user.getStatus() == UserStatus.FREEZE) {
    throw new RuntimeException("用户状态异常");
}
  • UserStatus枚举中通过@JsonValue注解标记JSON序列化时展示的字段

在这里插入图片描述

  • 在页面查询,结果如下:
    在这里插入图片描述

5、JSON处理器

(1)定义实体

  • 定义一个单独实体类来与info字段的属性匹配

在这里插入图片描述

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

(2)使用类型处理器

  • User类的info字段修改为UserInfo类型,并声明类型处理器
/**
 * 详细信息
 */
@TableField(typeHandler = JacksonTypeHandler.class)
private UserInfo info;
  • 开启自动结果集映射autoResultMap = true

在这里插入图片描述

(3)测试

  • 赋值改成相对应的,使用UserInfo.of(),实体类 UserInfo 需加注解@AllArgsConstructor(staticName = "of")才生效

在这里插入图片描述

// user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
user.setInfo(UserInfo.of(24,"英文老师","female"));
  • 为了让页面返回的结果也以对象格式返回,我们要修改UserVO中的info字段:

在这里插入图片描述

  • 在页面查询结果如下:

在这里插入图片描述

(4)启动报错

  • 报错内容如下图所示
  • Type handler was null on parameter mapping for property 'info'. It was either not specified and/or could not be found for the javaType (com.itheima.mp.domain.po.UserInfo) : jdbcType (null) combination.

在这里插入图片描述

  • 解决方法:在UserMapper.xml中按如下方法修改
  • sql语句中的info后面添加,typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler
  • 示例代码如下
<insert id="saveUser" parameterType="com.itheima.mp.domain.po.User" >
    INSERT INTO `user` (`id`, `username`, `password`, `phone`, `info`, `balance`)
    VALUES
    (#{id}, #{username}, #{password}, #{phone}, #{info,typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler}, #{balance});
</insert>

6、配置加密

  • MyBatisPlus从3.3.2版本开始提供了一个基于AES算法的加密工具,帮助我们对配置中的敏感信息做加密处理。
  • 我们以数据库的用户名和密码为例

(1)生成密钥

  • 利用AES工具生成一个随机密钥,然后对用户名、密码加密
  • 在测试启动类中MpDemoApplicationTests添加如下代码:
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("123456", randomKey);
        System.out.println("password = " + password);

    }

}

在这里插入图片描述

  • 打印结果如下:
randomKey = 71391879eab77579
username = tCjwDtBH7Yi27ZJV9ZNhdA==
password = PQygG9N05RLfoiyKppeLFw==

(2)修改配置

  • 修改application.yaml文件,把jdbc的用户名、密码修改为刚刚加密生成的密文:
  • 密文要以 mpw:开头,代码如下
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:tCjwDtBH7Yi27ZJV9ZNhdA== # 密文要以 mpw:开头
    password: mpw:PQygG9N05RLfoiyKppeLFw== # 密文要以 mpw:开头

(3)测试

  • 在项目启动的时候,添加AES的秘钥,这样MyBatisPlus就可以解密数据了
  • --mpw.key=71391879eab77579
  • 步骤如下
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 添加密钥

在这里插入图片描述

  • 重启项目后可以成功查询

(4)单元测试

  • 单元测试的时候不能添加启动参数,所以要在测试类的注解上配置
  • args = "--mpw.key=71391879eab77579"

在这里插入图片描述

四、插件功能

  • MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有
  • PaginationInnerInterceptor :自动分页
  • TenantLineInnerInterceptor :多租户
  • DynamicTableNameInnerInterceptor:动态表名
  • OptimisticLockerInnerInterceptor:乐观锁
  • IllegalSQLInnerInterceptor:sql 性能规范
  • BlockAttackInnerInterceptor:防止全表更新与删除

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

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

这里我们以分页插件为里来学习插件的用法。

1、分页插件

  • 在未引入分页插件的情况下,MybatisPlus是不支持分页功能的,IServiceBaseMapper中的分页方法都无法正常起效。
  • 所以,我们必须配置分页插件。

(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 MybatisConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        // 1、创建分页插件
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        paginationInnerInterceptor.setMaxLimit(1000L);
        // 2、添加分页插件
        mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
        return mybatisPlusInterceptor;
    }
}

(2)分页API

  • 编写一个分页查询的测试
@Test
void testPageQuery() {
    int pageNo = 1, pageSize = 2;
    // 1、准备分页条件
    // 1.1 分页条件
    Page<User> page = Page.of(pageNo, pageSize);
    // 1.2 排序条件
    page.addOrder(new OrderItem().setColumn("balance").setAsc(true));
    page.addOrder(new OrderItem().setColumn("id").setAsc(true));

    // 2、分页查询
    Page<User> p = userService.page(page);

    // 3、解析
    long total = p.getTotal();
    System.out.println("total: " + total);
    long pages = p.getPages();
    System.out.println("pages: " + pages);
    List<User> records = p.getRecords();
    records.forEach(System.out::println);
}
  • 运行结果如下

在这里插入图片描述

2、通用分页实体

(1)实体

  • 分页条件不仅仅用户分页查询需要,以后其它业务也都有分页查询的需求。
  • 因此建议将分页查询条件单独定义为一个PageQuery实体:

在这里插入图片描述

package com.itheima.mp.domain.query;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
    @ApiModelProperty("页码")
    private Long pageNo;
    @ApiModelProperty("每页数据条数")
    private Long pageSize;
    @ApiModelProperty("排序字段")
    private String sortBy;
    @ApiModelProperty("是否升序")
    private Boolean isAsc;
}

  • 让我们的UserQuery继承这个实体:

在这里插入图片描述

  • 返回值的用户实体沿用之前定义的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("分页结果")
public class PageDTO<T> {
    @ApiModelProperty("总条数")
    private Long total;
    
    @ApiModelProperty("总页数")
    private Long pages;
    
    @ApiModelProperty("集合")
    private List<T> list;
}

(2)开发接口

  • UserController中定义分页查询用户的接口
@ApiOperation("条件分页查询用户列表")
@GetMapping("/page")
public PageDTO<UserVO> queryUsersPage(UserQuery userQuery){
    return userService.queryUsersPage(userQuery);
}
  • 然后在IUserService中创建queryUsersPage方法:
PageDTO<UserVO> queryUsersPage(UserQuery userQuery);
  • UserServiceImpl中实现该方法
@Override
public PageDTO<UserVO> queryUsersPage(UserQuery userQuery) {
    String name = userQuery.getName();
    Integer status = userQuery.getStatus();
    // 1、构建分页条件
    // 1.1 分页条件
    Page<User> page = Page.of(userQuery.getPageNo(), userQuery.getPageSize());
    // 1.2 排序条件
    if (StrUtil.isNotBlank(userQuery.getSortBy())) {
        boolean isAsc = userQuery.getIsAsc() != null? userQuery.getIsAsc() : true;
        page.addOrder(new OrderItem().setColumn(userQuery.getSortBy()).setAsc(isAsc));
    } else {
        // 默认按照更新时间排序
        page.addOrder(new OrderItem().setColumn("update_time").setAsc(false));
    }
    // 2、分页查询
    Page<User> p = lambdaQuery()
            .like(name != null, User::getUsername, name)
            .eq(status != null, User::getStatus, status)
            .page(page);
    // 3、封装VO结果
    PageDTO<UserVO> pageDTO = new PageDTO<>();
    pageDTO.setTotal(p.getTotal());
    pageDTO.setPages(p.getPages());
    List<User> records = p.getRecords();
    if (CollUtil.isEmpty(records)) {
        pageDTO.setList(Collections.emptyList());
        return pageDTO;
    }
    pageDTO.setList(BeanUtil.copyToList(records, UserVO.class));
    return pageDTO;
}
  • 启动项目,在页面查看

在这里插入图片描述

(3)改造PageQuery实体

  • PageQuery中定义方法,将PageQuery对象转为MyBatisPlus中的Page对象
  • a. 给页码、条数等字段赋初值
  • b.定义方法
  • c. 应用
package com.itheima.mp.domain.query;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

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

    public <T> Page<T> toMpPage(OrderItem... items) {
        // 1 分页条件
        Page<T> page = Page.of(pageNo, pageSize);
        // 2 排序条件
        if (StrUtil.isNotBlank(sortBy)) {
            page.addOrder(new OrderItem().setColumn(sortBy).setAsc(isAsc));
        } else if (items != null) {
            // 默认按照更新时间排序
            page.addOrder(items);
        }
        return page;
    }
    public <T> Page<T> toMpPage(String defaultSortBy, Boolean defaultAsc) {
        return toMpPage(new OrderItem().setColumn(defaultSortBy).setAsc(defaultAsc));
    }
    public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {
        return toMpPage(new OrderItem().setColumn("create_time").setAsc(false));
    }
    public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {
        return toMpPage(new OrderItem().setColumn("update_time").setAsc(false));
    }

}

在这里插入图片描述

Page<User> page = userQuery.toMpPageDefaultSortByUpdateTimeDesc();

(4)改造PageDTO实体

  • PageDTO中定义方法,将MyBatisPlus中的Page结果转为PageDTO结果
public static <PO, VO> PageDTO<VO> of(Page<PO> p, Class<VO> voClass) {
    PageDTO<VO> pageDTO = new PageDTO<>();
    // 总条数
    pageDTO.setTotal(p.getTotal());
    // 总页数
    pageDTO.setPages(p.getPages());
    // 当前页数据
    List<PO> records = p.getRecords();
    if (CollUtil.isEmpty(records)) {
        pageDTO.setList(Collections.emptyList());
        return pageDTO;
    }
    pageDTO.setList(BeanUtil.copyToList(records, voClass));
    return pageDTO;
}

在这里插入图片描述

  • 应用
return PageDTO.of(p, UserVO.class);

在这里插入图片描述

  • 如果是希望自定义POVO的转换过程,可以这样做
  • Function<PO,VO>,其中 PO是参数类型,VO 是返回值类型
public static <PO, VO> PageDTO<VO> of(Page<PO> p, Function<PO,VO> convertor) {
    PageDTO<VO> pageDTO = new PageDTO<>();
    // 总条数
    pageDTO.setTotal(p.getTotal());
    // 总页数
    pageDTO.setPages(p.getPages());
    // 当前页数据
    List<PO> records = p.getRecords();
    if (CollUtil.isEmpty(records)) {
        pageDTO.setList(Collections.emptyList());
        return pageDTO;
    }
    // 数据转换
    pageDTO.setList(records.stream().map(convertor).collect(Collectors.toList()));
    return pageDTO;
}

在这里插入图片描述

  • 自定义POVO的应用
// 3、封装VO结果
/*
    return PageDTO.of(p, User->BeanUtil.copyProperties(user, UserVO.class));
 */
return PageDTO.of(p, user -> {
    // 拷贝属性
    UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
    // 处理特殊逻辑
    String username = userVO.getUsername();
    userVO.setUsername(username.substring(0, username.length() - 2) + "**");
    return userVO;
});

在这里插入图片描述

Mybatisplus 自定义sql 使用条件构造器可以在自定义 SQL 语句中使用 Mybatisplus 的条件构造器,方便快捷地构造查询条件。 使用步骤: 1. 在 Mapper 接口中定义自定义 SQL 语句的方法,方法返回值为 List 或者其他需要返回的结果类型。 2. 在自定义 SQL 语句中使用 ${} 占位符来引用条件构造器生成的 SQL 片段。 3. 在方法参数中使用 @Param 注解来指定条件构造器生成的 SQL 片段的参数名称和类型,同时在自定义 SQL 语句中使用 #{参数名} 占位符来引用参数。 4. 在方法中使用 QueryWrapper 类来构造查询条件,然后将 QueryWrapper 对象作为参数传递给自定义 SQL 语句方法即可。 示例代码如下: ``` @Mapper public interface UserMapper extends BaseMapper<User> { @Select("SELECT * FROM user ${ew.customSqlSegment}") List<User> selectByCustomSql(@Param(Constants.WRAPPER) QueryWrapper<User> wrapper); } ``` 其中,${ew.customSqlSegment} 是 Mybatisplus 条件构造器生成的 SQL 片段,@Param(Constants.WRAPPER) 指定了 wrapper 参数的名称和类型。 调用示例: ``` QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.eq("age", 18).like("name", "张"); List<User> userList = userMapper.selectByCustomSql(wrapper); ``` 以上代码中,使用 QueryWrapper 构造了查询条件,然后将 QueryWrapper 对象作为参数传递给 selectByCustomSql 方法,该方法会根据传入的 QueryWrapper 对象生成自定义 SQL 语句,并返回符合条件的用户列表。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值