【 MyBatis-Plus | 精讲 】
MyBatis-Plus是MyBatis的增强工具,提供更便捷的数据库操作功能。文章介绍了其核心特点:润物无声的增强、高效的单表CRUD操作及丰富的扩展功能。主要内容包括:1) 环境准备,需继承BaseMapper接口;2) 常用注解如@TableName、@TableId等;3) 条件构造器的使用;4) Service接口扩展;5) 代码生成、逻辑删除等扩展功能;6) 分页插件等实用工具。通过约
目录
前言:
该篇文章总结了MyBatis-Plus的环境准备,使用事项,特点,核心功能,扩展功能,插件功能。
1.基本介绍:
MyBatis对于大家并不陌生,它是一个持久性框架用于对数据库进行增删改查的操作,而MyBatis-Plus本质就是对MyBatis进行一个扩展。
MyBatis-Plus的特点:
- 润物无声:Plus只对MyBatis的基础上做增强,不会对其改变,引入它不会对现有工程产生影响(两者并不影响)
- 效率至上:Plus只需要简单的配置,即可快速进行单表CRUD操作,节省大量时间
- 丰富功能:可以进行代码生成,自动分页,逻辑删除,自动填充等功能
2.快速入门:
2.1.环境准备
条件:
- 引入MaBatis-Plus对应依赖
- 自定义Mapper接口,继承Plus提供的BaseMapper接口
1.引入依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
2.自定义且继承BaseMapper接口
1.BaseMapper接口里面已经定义好了对于单表操作的一系列的增删改查的方法,如:insert(),deleteById(),updateById(),selectById()
----
2.细节:该接口使用的是泛型该定义对应方法,那么继承该接口需要指定具体类型(就是你的实体类类型)
package com.itzhanghada.mp.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itzhanghada.mp.domain.po.User;
public interface UserMapper extends BaseMapper<User> {
}
2.2.问题解决
为什么要指定泛型(具体类型)?
Mp可以通过扫描实体类,并且基于反射来获取实体类信息作为数据库的信息。
为什么可以通过实体类具体到数据库的信息呢?
通过一些约定和实体类字段名与数据库字段一致来实现
约定:
- 类名驼峰转下划线作为表名(类名为tbUser,数据库表名为tb_user,可以对应上)
- 类中字段名为id,直接默认为数据库中的主键字段
- 变量名驼峰转下划线作为表中的字段名(类中字段createTime ,数据库字段creat_time,对应)
那如果不符合约定呢?
那么需要自己使用注解指定对应字段,让Mp识别
2.3.常见注解
常见注解:@TableName:指定表名,@TableId:指定主键,@TableFieId:指定字段信息
@TableName:
- 描述:表名注解,标识实体类对应的表
- 使用位置:实体类
@TableName("user")
public class User {
private Long id;
private String name;
}
TableName注解除了指定表名以外,还可以指定很多其它属性:
属性 |
类型 |
必须指定 |
默认值 |
描述 |
value |
String |
否 |
"" |
表名 |
schema |
String |
否 |
"" |
schema |
keepGlobalPrefix |
boolean |
否 |
false |
是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时) |
resultMap |
String |
否 |
"" |
xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定) |
autoResultMap |
boolean |
否 |
false |
是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入) |
excludeProperty |
String[] |
否 |
{} |
需要排除的属性名 @since 3.3.1 |
@TableId:
- 描述:主键注解,标识实体类中的主键字段
- 使用位置:实体类的主键字段
@TableName("user")
public class User {
@TableId
private Long id;
private String name;
}
属性 |
类型 |
必须指定 |
默认值 |
描述 |
---|---|---|---|---|
value |
String |
否 |
"" |
表名 |
type |
Enum |
否 |
IdType.NONE |
指定主键类型 |
value指定对应表名,而type指定id的生成策略(不指定,默认雪花算法生成id)
IdType(种类):
- AuTo:自增长,每次增加一
- INPUT:自己手动输入
- ASSIGN_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 方法) |
@TableFieId:
-
描述:普通字段注解
-
使用位置:类中字段位置
@TableName("user")
public class User {
@TableId
private Long id;
private String name;
private Integer age;
@TableField(is_married")
private Boolean isMarried;
@TableField("`concat`")
private String concat;
}
一般情况下我们并不需要给字段添加@TableField注解,一些特殊情况除外:
- 成员变量名与数据库字段名不一致
- 成员变量是以isXXX命名,按照JavaBean的规范,MybatisPlus识别字段时会把is去除,这就导致与数据库不符。比如:isMarried => married
- 成员变量名与数据库一致,但是与数据库的关键字冲突。使用@TableField注解给字段名添加转义字符:``。比如:order => `order`
- 实体类有该变量,但是数据库没有该字段(不属于数据库的字段),使用exist = false 来标记它不属于数据库字段
属性 |
类型 |
必填 |
默认值 |
描述 |
value |
String |
否 |
"" |
数据库字段名 |
exist |
boolean |
否 |
true |
是否为数据库表字段 |
condition |
String |
否 |
"" |
字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s},参考(opens new window) |
update |
String |
否 |
"" |
字段 update set 部分注入,例如:当在version字段上注解update="%s+1" 表示更新时会 set version=version+1 (该属性优先级高于 el 属性) |
insertStrategy |
Enum |
否 |
FieldStrategy.DEFAULT |
举例:NOT_NULL insert into table_a(<if test="columnProperty != null">column</if>) values (<if test="columnProperty != null">#{columnProperty}</if>) |
updateStrategy |
Enum |
否 |
FieldStrategy.DEFAULT |
举例:IGNORED update table_a set column=#{columnProperty} |
whereStrategy |
Enum |
否 |
FieldStrategy.DEFAULT |
举例:NOT_EMPTY where <if test="columnProperty != null and columnProperty!=''">column=#{columnProperty}</if> |
fill |
Enum |
否 |
FieldFill.DEFAULT |
字段自动填充策略 |
select |
boolean |
否 |
true |
是否进行 select 查询 |
keepGlobalFormat |
boolean |
否 |
false |
是否保持使用全局的 format 进行处理 |
jdbcType |
JdbcType |
否 |
JdbcType.UNDEFINED |
JDBC 类型 (该默认值不代表会按照该值生效) |
typeHandler |
TypeHander |
否 |
类型处理器 (该默认值不代表会按照该值生效) |
|
numericScale |
String |
否 |
"" |
指定小数点后保留的位数 |
2.4.常见配置
MybatisPlus也支持基于yaml文件的自定义配置,详见官方文档:使用配置 | MyBatis-Plus
大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如:实体类的别名扫描包
mybatis-plus:
type-aliases-package: com.itzhanghada.mp.domain.po
global-config:
db-config:
id-type: auto # 全局id类型为自增长
注意:全局id默认id策略是雪花算法来生成id,但是如果你使用了注解指定了id生成策略,那么会优先注解(注解的优先级比全局配置高)
3.核心功能:
3.1.条件构造器
简介:支持构造各种条件来满足开发需求
类型如下:
推荐使用Lambda开头的条件构造器:它使用了方法引用的方式,而其他条件构造器需要指定字符串(硬编码模式)(不推荐)
QueryWrapper:
查询:
@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);
}
更新:
@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);
}
UpdateWrapper:
@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);
}
LambdaQueryWrapper:
@Test
void testLambdaQueryWrapper() {
// 1.构建条件 WHERE username LIKE "%o%" AND balance >= 1000
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper
.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);
}
3.2.Service接口
和继承BaseMapper一样,我们也可以继承IService,里面也实现了一系列方法:save(),remove(),update(),getById(),listById(),LambdaX(),page()
实现方式:
由于Service中经常需要定义与业务有关的自定义方法,因此我们不能直接使用IService,而是自定义Service接口,然后继承IService以拓展方法。同时,让自定义的Service实现类继承ServiceImpl,这样就不用自己实现IService中的接口了。
1.自定义接口继承IService接口(指定泛型的具体类型)
2.自定义实现类实现自定义接口,并且继续ServiceImpe(指定两个参数,第一个为自定义的Mapper,实体类与上面相同)(自定义Mapper接口,实体类)
4.扩展功能:
4.1.代码生成
在使用MybatisPlus以后,基础的Mapper、Service、PO代码相对固定,重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成PO、Mapper、Service等相关代码。只不过代码生成器同样要编码使用,也很麻烦。
Mp的方式:
- 自己写对应生成代码来生成代码
- 根据官网提供的IDEA的插件来生成代码
这里博主推荐一个更好用的插件:在IDEA的plugins市场中搜索并安装MyBatisPlus
插件:
4.2.静态工具
MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能
细节:由于静态方法不能指定泛型,所以需要指定字节码class(当然你传一个对象就不需要传字节码了,直接通过发射获取即可)
解决方案:Service之间相互注入会出现循环依赖问题,如果Service要相互调用可以使用Db静态工具
4.3.逻辑删除
一些重要的数据我们需要进行数据保持,那么需要进行逻辑删除
而MP提供了对应功能,无需改变方法调用,你还是可以之间调用删除方法(MP会先判断逻辑字段,最终实际上执行的是更新语句)
全局配置:
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)
注意: 逻辑删除本身也有自己的问题,比如:
-
会导致数据库表垃圾数据越来越多,从而影响查询效率
-
SQL中全都需要对逻辑删除字段做判断,影响查询效率
解决方案:可以采用把删除数据迁移到其它表
4.4.通用枚举
假如我们实体类需要使用枚举字段,但是数据库对应字段是一个int类型,因此我们需要进行类型转换,而MP提供了对应的转换功能
使用方式:
- 告诉MP,实体类中的枚举类型中的哪个字段需要转换(用@EnumValue注解标记即可)
- 在yml中配置全局枚举处理器
举例:
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
package com.itzhanghada.mp.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;
@Getter
public enum UserStatus {
NORMAL(1, "正常"),
FREEZE(2, "冻结")
;
@EnumValue
private final int value;
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
细节:使用枚举时,返回给前端的字段信息会是自己定义的常量名,如NORML
那么如果你需要返回枚举内部的数据(使用@JsonValue注解标记)
4.5.JSON类型处理器
如果你需要将一个对象作为字段存入数据库中,那么你需要指定对应的类型处理器
MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler处理器
使用方式:
- 在要转JSON对应的实体类字段上加@TableField(typeHandler = JacksonTypeHandler.class)
- 在类上加@TableName(autoResultMap = true) =》自动装配映射
5.插件功能:
5.1.分页插件
MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:
- PaginationInnerInterceptor:自动分页
- TenantLineInnerInterceptor:多租户
- DynamicTableNameInnerInterceptor:动态表名
- OptimisticLockerInnerInterceptor:乐观锁
- IllegalSQLInnerInterceptor:sql 性能规范
- BlockAttackInnerInterceptor:防止全表更新与删除
注意:
使用多个分页插件的时候需要注意插件定义顺序,建议使用顺序如下:
- 多租户,动态表名
- 分页,乐观锁
- sql 性能规范,防止全表更新与删除
这里介绍的是分页插件:
使用步骤:
- 配置类中注册插件,同时添加分页插件
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
@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);
}
这里用到了分页参数,Page,即可以支持分页参数,也可以支持排序参数。(当然你可以添加多个排序字段,会根据先后来进行排序,比如:按先年龄排,后名称排,那么只有当两者年龄相同时才会采取按名称排序)
int pageNo = 1, pageSize = 5;
// 分页参数
Page<User> page = Page.of(pageNo, pageSize);
// 排序参数, 通过OrderItem来指定
page.addOrder(new OrderItem("balance", false));
userService.page(page);
5.2.封装分页工具
@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);
}
}
@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);
}
}
更多推荐
所有评论(0)