该文章为学习笔记,在线文档
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、条件构造器
QueryWrapper
和LambdaQueryWrapper
通常用来构建select
、delete
、update
的where
条件部分UpdateWrapper
和LambdaUpdateWrapper
通常只在set
语句比较特殊才使用- 尽量使用
LambdaQueryWrapper
和LambdaUpdateWrapper
,避免硬编码
(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
又提供了一套基于Lambda
的Wrapper
,包含两个: LambdaQueryWrapper
和LambdaUpdateWrapper
- 分别对应
QueryWrapper
和UpdateWrapper
- 查询出名字中带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
- 我们可以利用
MyBatisPlus
的Wrapper
来构建复杂的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)接口实现
实现以下接口
- 引入依赖
swagger
、web
<!--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
文件,在jdbc
的url
后面,
添加参数&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
是不支持分页功能的,IService
和BaseMapper
中的分页方法都无法正常起效。 - 所以,我们必须配置分页插件。
(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);
- 如果是希望自定义
PO
到VO
的转换过程,可以这样做 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;
}
- 自定义
PO
到VO
的应用
// 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;
});