mybatis-plus

教程来自黑马SpringCloud视频

Mybatis-plus

实际上mybatis-plus不是用来取代mybatis的,而是用来加强mybatis的。mybatisplus和mybatis合作是正道。

mbatis-plus对对myabtis进行了兼容,引入mybatis-plus不会对原有的mybatis项目有任何影响。

mybatis-plus的最常用功能

简单配置后,快速进行简单的单表CRUD操作。

提供代码生成,自动分页,逻辑删除,自动填充等功能。

myabtis-plus快速入门

引入依赖

1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>

这个依赖集成了myabtis和mybatis-plus的所有。

继承baseMapper

myabtis-plus中定义了一个接口baseMapper,这个接口提供了基础的增删改查方法。

insert

update

delete

select

开头的方法

1
interface UserMapper extends BaseMapper<User>

当我们的接口继承了BaseMapper之后,mybatis-plus在扫描到这个mapper的时候,会自动帮我们创建一个该接口的实现类到Spring容器中。我们可以直接@autowired注入接口。

默认约定参数

baseMapper中提供的方法有默认实现。也就是说我们不需要实现这些方法。

那么这些方法的具体实现是怎样的呢?该如何配置这些方法的实现呢?

配置参数:

  1. 需要提供BaseMapper<需要的类>的泛型

通过泛型类的信息得到数据库的信息:

所以我们的实体类需要拥有以下信息。

  1. 类名的驼峰转下划线变成表名 例如UserInfo 对应 user_info 表
  2. 实体类中的id成员变量就是主键
  3. 实体类中的成员变成驼峰转下划线是字段名

如果不想要用默认约定

@TableName

@TableId 注意可以指定mybatisplus的id策略,重要的有雪花算法生成id,这个是默认的策略

@TableField 注意在is开头的字段,以及关键字字段,或者标记成非数据库字段有用

这几个注解是用在实体类中,加在一些不符合默认约定的地方,所以实际上主体还是默认约定。

常见配置

在springboot的配置文件中

image-20240730195959071

myabtis语法简化

在sql语句中,实际上最为常用的是查询条件的改变,以及查询字段的改变

如果我们能通过一种更简单的方法直接根据条件来生成条件sql就好了

在mybatis-plus中使用Wrapper这个类来代表查询条件,这是一个抽象类,具体的实现类有queryWrapper,updateWrapper,LambdaWrapper。。。

1
2
3
4
5
6
7
8
9
10
11
12
//例子1 select条件查询,根据username,balance字段查询id,username,info,balance等字段
QueryWrapper<User> wrapper = new QueryWrapper<>().select("id","username","info","balance").like("usename","admin").ge("balance",1000);
mapper.selectList(wrapper);
//例子2 update条件语句,根据条件更新数据
UpdateWrapper<User> wrapper = new UpdateWrapper<>().eq("username","jack");
User user = new User().setBalance(1000);
mapper.update(user,wrapper);
//例子3 update条件语句,在原来基础上-200balance
UpdateWrapper<User> wrapper = new UpdateWrapper<>().setSql("balance = balance-200").in("id",List.of(1,2,3,4));
mapper.update(null,wrapper);
//例子4 lambdaWrapper,为的是让我们不再直接写死字段名,而是通过寻找实体类中的字段方法来确定数据库中的字段名
wrapper.select(User::getId,User::getUsername...).like(User::getUsername,"admin");

自定义Sql

和mybatis种不同的是,这里的自定义sql可以使用wrapper来构建核心的部分,剩下的部分我们再来自己编写

实际上之前的mybatis语法简化就用到了wrapper和自定义sql,但是这样是不行的,因为那是在业务代码中,sql语句混合在了业务逻辑中,这对于维护来说是很糟糕的,所以我们需要提取出来到mapper层

解决方案:

1.

在mapper中自定义一个接口方法,这个方法需要接受一个参数wrapper,以及其他的参数

1
void updateBalanceByIds(@Param("ew") LambdaQueryWrapper Wrapper,@Param("mount") int mount);

注意传入的**wrapper指定的参数名一定要是@Param(“ew”)**实际上是Constants.Wrapper的值

  1. 在xml中

image-20240730204838852

使用ew对象的customSqlSegment也就是传入sql片段,where。。。

这里直接是使用$符号更快,使用#也不会更安全,因为这是通过程序生成的,一开始就有防止sql注入的环节,所以生成的sql语句不会有sql注入的风险,直接是使用$没问题

继承IService

在mapper层提供了baseMapper,而在service层也提供了一个IService接口。

在mapper层对于单表更近,我们不宜进行过多业务操作,而在service层中,为了实现了业务操作,更多的逻辑以及mapper层的交互使用,才是service的主要职责。

save新增

update更新

remove移除

list查询多个

get查一个,lambda用于提供方便的wrapper构造不用new了,page分页查询,count。。。

image-20240730205730765

IService中这么多的方法,我们不可能自己一个一个实现的,所以我们的实现类需要继承提供的默认实现类叫做ServiceImpl,这个实现类一个根据传入的泛型来生成对应IService接口的实现

image-20240730210422673

注意想要ServiceImpl实现对应的接口,我们必须给定对应的Mapper extends BaseMapper ,并且要给定Enity类型

1
2
3
public class UserServiceImpl extends ServerImpl<UserMapper,User> implements UserService{

}

在ServiceImpl中mapper放在protected baseMapper的成员变量中。

LambdaQuery和LambdaUpdate方法

在之前的Wrapper部分,我们推荐了使用lambda构建。在ServiceImpl中为了方便我们进行lambda的快速查询,提供了LambdaWrapper结合query或者udpate的对象(通过对应的方法获得)。这些对象以构建lambdaWrapper为主体,然后提供List,one,count等常用的查询操作,我们构建完lambdawrapper即可直接调用对象自身的这些方法进行查询得到结果。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
//@NoRequiredArgsContructor
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService{
@Override
public List<User> queryUsers(String name, Integer minBalance, Integer maxBalance){
return lambdaQuery()
.like(name!= null, User::getUsername, name)
.ge(minBalance!=null,User::getMinBalance, minBalance)
.le(maxBalance!=null,User::getMaxBalance, maxBalance)
.list();
}
}

lambdaUpdate方法也大差不差,同样的思想,在构建LambdaWrapper的同时提供执行方法。

批处理方法

在ServiceImpl中提供了了批处理方法。

批处理之所以快,快在三个地方

  1. 预编译发送编译后的东西
  2. 网络请求数目减少
  3. sql语句数目减少

我们通过原来的saveBatch之类的批处理语句可以减少网络请求的数量,但是实际上在数据库端还是一条一条执行的sql语句。

如果我们能将多条sql语句整合成一条sql语句,那么数据库系统可以利用到内部的优化机制,使得更快速的执行大型sql语句。

所以个sql语句的整合,我们需要实现。

  1. mybatis中通过sql动态拼接可以做到

只要在mysql连接中指定rewriteBatchedStatements=true即可开启自动拼接重写。

实际上是调用的mysql驱动的方法,提供的批处理重写方法。

1
2
3
//在application.yml中
dataSource:
url:#{原本的url}&rewriteBatchedStatements=true

扩展功能

代码生成器

也就是通过数据库生成mybatis-plus的代码,这里直接推荐使用idea的插件-mybatisplus直接鼠标点一点生成即可。

静态工具Db类

有时候我们不想要建立这么多完全的数据库操作,我们只想临时,快速的查询一下。

或者在涉及多表查询的时候,我们想使用其他表的几个查询方法。

寻常的方法都是注入,容易导致相互注入,循环依赖。

这个时候用静态工具是最方便的的。

在使用静态方法时候需要解决这个问题:

  1. 如何知道数据库表信息?

这个问题在之前我们mybatis-plus的过程中就知道了,只需要给一个实体类的信息,mybatis-plus就可推断出数据库表的信息。

所以在静态方法中,需要传入一个Class对象,也就是实体类,数据库表的对应实体类。

其他的传参都和IService一样。

1
2
// 加入现在在某个service的实现方法内部,下面就是Db的用法
return Db.lambdaQuery(User.class).ge(minBalance!=null,User::getBalance,minBalance).list();

逻辑删除

也就是将用户想要删除数据区别出来,不让用户看到。

一般就是通过数据库中的一个状态字段的一位来记录。

如果添加了这个状态字段,所有的查询,以及修改的一些操作都需要添加上条件只允许操作未删除的数据。这样的修改就很大了。

mybatis-plus考虑到了这种情况,提供了拓展。

我们只需要配置数据库中表示删除与否的字段,删除状态,未删除状态,即可实现只会操作未删除字段的修改。

在application.yml配置文件中:

image-20240802144336318

缺点

被逻辑删除的数据实际上和原来的数据在同一张表里,在执行查询,修改操作的时候,相当于是站着茅坑,完全没有作用,就像冗余数据一样。

最好的做法是将被删除的数据迁移到别的表中,这样就不会影响原本表的效率。

另外逻辑删除会导致所有操作会多一个删除状态的判断,影响效率。

枚举处理器

在数据库中,很多的状态都是通过int等数值类型存储的,但是在java业务逻辑代码中,使用数值常量会导致代码的可读性和可维护性下降。所以在java逻辑代码中我们使用枚举类型来替代数据库中的数值类型。

最后问题就集中在了mybatis将java中的枚举类型和数据库中的数值类型相互转换这个过程了。

mybatis有类型处理器TypeHandler接口,BaseTypeHandler抽象类,众多的实现类。但是枚举类型的处理器做的不是很好。mybatis-plus添加了自己的枚举类型处理器MybatisEnumTypeHandler

通过在配置文件application.yml中添加配置,即可启用这个枚举处理器

1
2
3
4
mybatis-plus:
configuration:
default-enum-type-handler: 默认处理器类全路径名‘com.baomidou...MybatisEnumTypeHandler

这样在mybatis解析的时候就会使用到这个枚举类型处理器。

我们在枚举上添加注解,这样枚举处理器就知道我们想要将枚举类中的那个字段作为处理结果。

1
2
3
4
5
6
7
8
9
10
11
@Getter
public enum UserStatus{
NORMALL(1,"正常"),ERROR(2,"异常");
@EnumValue // 当读取到这个注解,注解处理器就懂了,将这个字段作为结果替换
private final int value;
private final String descript;
UserStatus(int value,String des){
this.value = value;
this.descript = des;
}
}

这样从mybatis-plus的实现中,返回的结果就是枚举对象。我们的实体类中成员变量就改为枚举类型即可。

这里补充一下SpringMVC返回JSON时,使用JacksonJSON来处理对象,如果我们想要枚举对象的某个字段而不是枚举对象的名字作为值,我们可以在枚举类的对应字段上添加@JsonValue

JSON处理器

和之前的枚举处理器一样,都是解决数据库和java代码中数据的不同表现类型的问题。

JSON在数据库中实际上是是字符串形式保存的,但是在java业务代码中,我们希望使用一个实体entity类型来存储这个数据。

mybatis-plus提供了三种实现,Gson,Jackson和fastjson三种底层依赖不同也就是三种实现。

如何转换?

在我们定义的table实体类中,对应的json属性entity成员变量上,加上@TableField注解

其中有一个typeHandler参数我们给三个实现类中的一个,例如@TableField(typehandler = JacksonTypeHandler.class)

这样只能实现将该字段的对象转成json字符串,不能实现将数据库json字符串转换成对应的类对象中

我们需要开启@TableName(autoResultMap=true),这样就可以将json处理器解析到的对象自动映射到对象中。

1
2
3
4
5
@TableName(name=“username”,autoResultMap=true
public class User{
@TableField(typeHandler=JacksonTypeHandler.class)
private UserInfo info;
}

为什么不像枚举处理器一样配置?

因为json类型在数据库中就是字符串,mybatis在接收到数据的时候可以判断这个是一个枚举类型,但是无法区分是普通字符串还是json字符串。

我们指定了对应字段使用json处理器解析才行。

另外解析之后,我们也面临属性映射的问题。

插件功能

实际上mybatisplus提供的,通过拦截器核心插件主体MybatisPlusInterceptor,拦截mybatis执行过程,然后实现对应的插件功能。

  1. 将MybatisPlusInterceptor对象添加到spring容器中。
1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class Mybatisconfig{
@Bean
public MybatisPlusInterceptor mybatisplusInterceptor(){
//1. 初始化核心插件主体
MybatisPlusInterceptor inter = new MybatisPlusInterceptor();
//2. 添加插件
inter.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
return inter;
}
}

InnerInterceptor 接口

所有 MyBatis-Plus 提供的插件都实现了 InnerInterceptor 接口,这个接口定义了插件的基本行为。目前,MyBatis-Plus 提供了以下插件:

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

分页插件的使用

  1. 添加分页插件到主体
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class MybatisPlusConfig {
/**
* 添加分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
return interceptor;
}
}
  1. 调用Service中的page方法

page方法需要IPage类型的参数,这是一个接口。

提供分页所需的页码,分页大小,排序方法等描述分页的方法。

IPage有两个实现类,最长常用的就是Page,另外一个不常用的就是PageDTO

然后调用Service的page方法,传入page对象即可

Page类型中有一个列表,用于存储查询结果中的这一页的数据,所以泛型参数就是数据库表对应的结果实体对象。

1
2
3
4
5
6
Page<User> page = Page.of(pageNum, pageSize);
page.addOrder(new OrderItem("数据库字段",升序或降序的boolean)).addOrder...
LambdaQuery().ge(User::getBalance,balance).page(page);
// 从page中获取查询结果,总条数,总页数,当前页结果
page.getCount();

Page 类

Page 类继承了 IPage 类,实现了简单分页模型。如果你需要实现自己的分页模型,可以继承 Page 类或实现 IPage 类。

属性名 类型 默认值 描述
records List emptyList 查询数据列表
total Long 0 查询列表总记录数
size Long 10 每页显示条数,默认 10
current Long 1 当前页
orders List emptyList 排序字段信息
optimizeCountSql boolean true 自动优化 COUNT SQL
optimizeJoinOfCountSql boolean true 自动优化 COUNT SQL 是否把 join 查询部分移除
searchCount boolean true 是否进行 count 查询
maxLimit Long 单页分页条数限制
countId String XML 自定义 count 查询的 statementId

通用分页查询

一般编写一个pageQuery类,包含分页查询所需参数,和对应data方法。

将其他查询类继承pageQuery类,这样其他查询就带有分页查询的参数。

将其他查询的结果都替换成PageVO也就是page传递给前端的类。


mybatis-plus
https://wainyz.online/wainyz/2024/07/30/mybatis-plus/
作者
wainyz
发布于
2024年7月30日
许可协议