MybatisPlus

MP引入

Mp的使用:

1.mapper层:继承BasMapper接口

2.service层:service接口实现IService接口,service实现类继承IServiceImpl实现类

Mp将实体类和数据库表明对应的约定:

  • MybatisPlus会把PO实体的类名驼峰转下划线作为表名
  • MybatisPlus会把PO实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型
  • MybatisPlus会把名为id的字段作为主键

常见注解:

@TableName 当实体类名称和数据库名称不一致时,使用该注解可以指定实体类对应的数据库表名

@TableId 一个作用是指定id,另一个作用是规定id的增长逻辑,没有指定默认是雪花算法:

描述
AUTO 数据库 ID 自增
NONE 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
INPUT insert 前自行 set 主键值
ASSIGN_ID 分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法)
ASSIGN_UUID 分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法)

@TableField 指定字段名,当类中某个属性并不在表中的时候,通过 exist 参数设置为 false 来排除在外,注意必须这么做,不然会报错

MP核心功能

条件构造器

1
2
3
4
5
6
AbstractWrapper
├── QueryWrapper (查询条件封装)
├── UpdateWrapper (更新条件封装)
└── LambdaWrapper
├── LambdaQueryWrapper (Lambda语法查询)
└── LambdaUpdateWrapper (Lambda语法更新)
  • 链式调用:支持链式编程,代码更简洁
  • Lambda支持:避免字段名硬编码,编译时检查
  • 防SQL注入:自动处理参数,防止SQL注入
  • 多条件组合:支持AND、OR等复杂逻辑组合

Query Wrapper

1
2
3
4
5
6
7
8
9
10
void testQueryWrapper(){
//构造查询条件,传入泛型
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 1000);

//这里什么也不用干,因为selectList(wrapper)也是MP定义好的,是mapper层继承自BaseMapper获得的方法
List<User> userList = userMapper.selectList(wrapper);
}

UpdateWrapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void testUpdateWrapper(){

//这句代码利用 Java 9 的 List.of() 工厂方法创建了一个包含 1L、2L 和 4L 这三个 Long 类型元素的不可变列表 ids
List<Long> ids = List.of(1L,2L,3L);

Integer fangzhuru = 200;

//构造更新条件
UpdateWrapper<User> wrapper = new UpdateWrapper()
//set("balance", 100)会将余额直接覆盖为 100,而不是在现有值基础上增加 100
.setSql("balance = balance - {0}",fangzhuru) //setSql直接拼接set语句 {0},{1}...是MP的占位符
.in("id",ids);

//第一个参数 null 表示不使用实体对象的字段进行更新,而是完全依赖于 UpdateWrapper 中设置的条件和 setSql 语句
userMapper.update(null,wrapper);
//如果这里没有用setSql,而是用简单的更新一个balance为user对象中现成的属性值的话,第一个参数就传入User,如:
userMapper.update(User,new UpdateWrapper<User>().in("id",ids));
}

LambdaQueryWrapper

1
2
3
4
5
6
7
8
9
10
11
//利用lambda表达式将quarymapper改造如下:
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);
}

自定义Sql

上面的案例中的Update那里的setSql方法实际上是在service层写了Sql语句,这是不符合规范的

但是,我们的目的是使用IService中的好用条件语句,所以我们可以这么做:

把条件语句封装到wrapper中,然后传递下去:

1
2
3
4
5
6
7
8
9
10
11
12
//service层:

void testUpdateWrapper(){
List<Long> ids = List.of(1L,2L,3L);

//封装一个查询wrapper
QueryWrapper<User> QueryWrapper = new QueryWrapper
.in("id",ids);

//调用mapper层我们自己的自定义方法
List<User> userList = userMapper.deductBalanceByIds(200,wrapper);
}
1
2
3
4
//mapper层
//我们的自定义方法
@Update("update user set balance = balance - #{money} ${ew.customWrapperSegment}") //刀乐符及以后的语法硬性约定
List<User> deductBalanceByIds(@Param("money")Integer money, @Param("ew") QueryWrapper<User> QueryWrapper); //ew 硬性约定

多表联查

1
2
3
4
5
6
7
8
9
10
11
12
13
//假设有用户表和地址表,我们要查用户id在一定范围内,且地址在某一处的用户
//service层
void testJoinWrapper(){
List<Long> ids = List.of(1L,2L,3L);
QueryWrapper<User> wrapper = new QueryWrapper()
.in("u.id",ids);
.eq("a.city","北京");
List<User> userlist = userMapper.getUsersByIdAndAddress(wrapper);
}

//mapper层
@Select("Select u.* from user u Left join address a on a.user_id = u.id ${ew.customWrapperSegment}")
List<User> getUsersByIdAndAddress (@Param("ew")QueryWrapper<User> QueryWrapper);

Service接口

service接口可以让很多简单的单表curd,直接在controller里就能完成

Lambda

直接看例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//未使用Lambda
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);


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

这个例子可以看出,lambda省略了自定义wrapper和调用service查询接口传入wrapper的过程

直接用IService接口中的lambdaQuery方法,最后一个.list()把返回值也获得了

得到结果的一步不仅可以用list(),可选的方法有:

  • .one():最多1个结果
  • .list():返回集合结果
  • .count():返回计数结果

MybatisPlus会根据链式编程的最后一个方法来判断最终的返回结果

批处理

在jdbc的url后面添加参数&rewriteBatchedStatements=true

1
2
3
4
5
6
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: MySQL123

扩展功能

静态工具

service之间如果相互调用,就可能出现问题(也可以通过@Lazy注解解决)

MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与 IService 中方法签名基本一致

相比于使用IService接口中的方法,使用Db中的方法不同之处就在于,要多传入一个参数:目标实体对象的类的字节码,因为静态方法不知道你要用哪个类来和数据库映射

逻辑删除

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

  • 在表中添加一个字段标记数据是否被删除
  • 当删除数据时把标记置为true
  • 查询时过滤掉标记为true的数据
  • 注意,只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除

在application.yml中加入以下配置:

1
2
3
4
5
6
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)

通用枚举

定义一个枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.itheima.mp.enums;

import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;

@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结")
;
@EnumValue //使用这个注解告诉MP用int value字段和数据库匹配
private final int value;
@JsonValue //使用这个注解,就可以让VO对象在json序列化的时候,使用这个String desc字段
private final String desc;

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

这样就可以把用户类中的status字段的类型由Integer改为 UserStatus ,更加直观

那么如何让MP能把数据库中的实际类型为整数的status和枚举类型对上?

第一步:在枚举类型中使用注解:@EnumValue,标注的属性是实际和数据库相匹配的属性

第二部,配置枚举处理器,依旧添加application.yml

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

同时,我们也可以把返回值VO中的status字段也改成枚举类型,那么在实际返回的时候,如何指定json序列化时,使用的实际属性?

使用 @JsonValue 注解,(这个注解是springmvc的,不是MP的)

JSON类型处理器

假设现在user表中有一个info字段,是json类型,user中却是String类型,那拆分数据就会很麻烦

MP提供了json类型处理器:

第一步,我们自己定义一个实体类UserInfo,属性和json字段相对应

第二步,把user类中的info字段改为UserInfo类型的,并加上注解 @TableField(typeHandler = JacksonTypeHandler.class)

第三步,在user类上加一个注解,声明自动映射 @TableName(value = “user”, autoResultMap = true)

插件功能

分页插件

自定义一个配置类,配置分页插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

这个过程相当于有两层,注入了一个MP的拦截器总对象,这个总对象还可以add小对象,这里就是add了一个分页插件对象

1
2
3
4
5
6
7
int pageNo = 1, pageSize = 5;
// 分页参数
Page<User> page = Page.of(pageNo, pageSize);
// 排序参数, 通过OrderItem来指定
page.addOrder(new OrderItem("balance", false));

userService.page(page); //page方法直接修改参数对象

通用分页实体

pageQuery:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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;
private Integer pageSize;
private String sortBy;
private Boolean isAsc;

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

pageDTO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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);
}
}

业务层便可简化为:

1
2
3
4
5
6
7
8
9
@Override
public PageDTO<UserVO> queryUserByPage(PageQuery query) {
// 1.构建条件
Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
// 2.查询
page(page);
// 3.封装返回
return PageDTO.of(page, UserVO.class);
}

自定义最后到vo的转换过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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;
});
}

Stream流

Lambda表达式