什么是MybatisPlus?
一个Mybatis的增强工具,简化单表操作,对Mybatis的功能有很多的增强但不改变,让开发更加的简单,高效
使用MybatisPlus的准备工作:
依赖注入:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
此starter包含对mybatis的自动装配,因完全可以替换掉Mybatis的starter
定义xxxMapper接口继承BaseMapper接口
示例:
public interface UserMapper extends BaseMapper<User> {
}
然后就可以使用该xxxMapper进行基本的CRUD操作了
BaseMapper中的方法都很简单易懂,一目了然,select就是查询系列,update就是修改,delete就是删除,insert就是增加
示例:
@Test
void testInsert() { //增
User user = new User();
user.setId(5L);
user.setUsername("Lucy");
user.setPassword("123");
user.setPhone("18688990011");
user.setBalance(200);
user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
userMapper.insert(user);
}
@Test
void testSelectById() { //查询
User user = userMapper.selectById(5L);
System.out.println("user = " + user);
}
@Test
void testQueryByIds() { //批量查询
List<User> users = userMapper.selectBatchIds(List.of(1L, 2L, 3L, 4L));
users.forEach(System.out::println);
}
@Test
void testUpdateById() { //修改
User user = new User();
user.setId(5L);
user.setBalance(20000);
userMapper.updateById(user);
}
@Test
void testDeleteUser() { //删
userMapper.deleteById(5L);
}
MybatisPlus的常见注解:
MybatisPlus是根据PO实体的信息来推断出表的信息,从而生成SQL的。默认情况下:
MybatisPlus会把PO实体的类名驼峰转下划线作为表名
MybatisPlus会把PO实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型
MybatisPlus会把名为id的字段作为主键
但很多情况下,默认的实现与实际场景不符,因此MybatisPlus提供了一些注解便于我们声明表信息:
@TableName
@Data
@TableName("user") //标注数据库中的表名,未标注默认以类名驼峰转下划线作为默认表名
public class User {
如果po实体类的类名不对应表名,可以使用@TableName注解的value值绑定表名
@TableId
@TableId(value = "id",type = IdType.AUTO) //标注数据库中的主键字段名,未标注默认以名为id的字段为主键,IdType.AUTO 类型为自增长,未赋值默认为雪花算法
private Long id;
如果po实体类中的主键名不为id,可以使用@TableId注解的value值绑定主键,type属性可指定主键的类型,IdType.AUTO为自增,不填默认为雪花算法(即随机生成一串数字),type比较重要的属性值还有INPUT型,意为用户自己输入主键值
@TableField
@TableField("username")
private String username; //标注变量对应数据库的字段的字段名,默认是变量名驼峰形式转下划线作为对应字段名
如果po实体的变量名不对应数据库的字段名,可以使用@TableField注解的value值绑数据库的字段
还有俩种特殊情况也需要使用到@TableField注解:
-
成员变量是以
isXXX
命名,按照JavaBean
的规范,MybatisPlus
识别字段时会把is
去除,这就导致与数据库不符。 -
成员变量名与数据库一致,但是与数据库的关键字冲突。使用
@TableField
注解给字段名添加转义字符:``
@TableField注解比较常用的属性还有exist,声明该成员变量是否为数据库字段
当然关于这三个注解还有很多属性没有说明,详细可参考官方文档:注解 | MyBatis-Plus (baomidou.com)
MybatisPlus的常见配置:
MybatisPlus也支持基于yaml文件的自定义配置,不过大部分配置都有默认值,这里介绍几个比较常用的:
mybatis-plus:
type-aliases-package: 实体类的别名扫描包(例:com.zeyu.mp.domain.po)
global-config:
db-config:
id-type: auto # 全局id类型(这里设为自增长)
mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,当前这个是默认值。
MyBatisPlus也支持手写SQL的,而mapper文件的读取地址可以自己配置,即上面的mapper-locations
其它的配置详见官方文档:使用配置 | MyBatis-Plus (baomidou.com)
条件构造器:
除了新增以外,修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法除了以id
作为where
条件以外,还支持更加复杂的where
条件
参数中的Wrapper
就是条件构造的抽象类,其下有很多默认实现,继承关系如图:
Wrapper
的子类AbstractWrapper
提供了where中包含的所有条件构造方法
而QueryWrapper在AbstractWrapper的基础上拓展了一个select方法,允许指定查询字段
而UpdateWrapper在AbstractWrapper的基础上拓展了一个set方法,允许指定SQL中的SET部分:
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")//查询"id","username","info","balance"字段
.like("username", "o")//"username"字段 包含 “o” 即 username like "%o%"
.ge("balance", 1000);//balance 值 >= 1000
// 2.查询数据
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
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 testUpdateWrapper2(){
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200")
.in("id",List.of(1L,2L,4L));
userMapper.update(null,wrapper);
}
LambdaQueryWrapper
无论是QueryWrapper还是UpdateWrapper在构造条件的时候都需要写死字段名称,会出现字符串。这在编程规范中显然是不推荐的。 那怎么样才能不写字段名,又能知道字段名呢?
其中一种办法是基于变量的get
方法结合反射技术。因此我们只要将条件对应的字段的get
方法传递给MybatisPlus,它就能计算出对应的变量名了。而传递方法可以使用JDK8中的方法引用
和Lambda
表达式。 因此MybatisPlus又提供了一套基于Lambda的Wrapper,包含两个:
-
LambdaQueryWrapper
-
LambdaUpdateWrapper
分别对应QueryWrapper和UpdateWrapper
其使用方式如下:
@Test
void testQueryWrapper(){
//构建查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
.select(User::getId,User::getUsername,User::getInfo,User::getBalance) //查询"id","username","info","balance"字段
.like(User::getUsername,"o") //"username"字段 包含 “o” 即 username like "%o%"
.ge(User::getBalance,1000); //balance 值 >= 1000
//查询
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
更多的条件构造方法使用详见官方文档:条件构造器 | MyBatis-Plus (baomidou.com)
自定义SQL
@Test
void testUpdateWrapper2(){
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200")
.in("id",List.of(1L,2L,4L));
userMapper.update(null,wrapper);
}
上述代码中,如 setSql("balance = balance - 200")这种写法是不符合开发规范的,因为SQL语句最好都维护在持久层,而不是业务层,所以,MybatisPlus提供了自定义SQL功能,可以让我们利用Wrapper生成查询条件,再结合Mapper.xml编写SQL
就比如上面这个例子,我们可以换一种写法:
首先业务层去除sql语句部分,只负责传递条件:
@Test
void testCustomSqlUpdate(){
List<Long> ids = List.of(1L,2L,4L); //要操作的用户id组
int amount = 200; //要扣除的balance值
QueryWrapper<User> wrapper = new QueryWrapper<User>() //条件
.in("id",ids);
userMapper.updateBalanceByIds(wrapper,amount);
}
然后在UserMapper中自定义SQL:
@Update(" update user set balance = balance - #{amount} ${ew.customSqlSegment}")
void updateBalanceByIds(@Param("ew") QueryWrapper<User> wrapper,@Param("amount") int amount);
这样就省去了编写复杂查询条件的烦恼了
lambdaQuery()和lambdaUpdate()
在对数据库进行操作时,动态构建SQL是一个常见的需求。MyBatis Plus作为一个增强版的MyBatis,提供了强大的条件构造器,尤其是Lambda条件构造器功能,使动态SQL的编写变得既安全又优雅
例,使用lambdaQuery()方法进行条件查询:
// 创建一个LambdaQueryWrapper实例,使用Lambda表达式构建查询条件
LambdaQueryWrapper<User> wrapper = new QueryWrapper<User>().lambda()
// 如果username不为null,则添加用户名模糊查询条件
.like(username != null, User::getUsername, username)
// 如果status不为null,则添加用户状态查询条件
.eq(status != null, User::getStatus, status)
// 如果minBalance不为null,则添加查询用户余额大于等于minBalance的条件
.ge(minBalance != null, User::getBalance, minBalance)
// 如果maxBalance不为null,则添加查询用户余额小于等于maxBalance的条件
.le(maxBalance != null, User::getBalance, maxBalance);
// 使用构建好的wrapper作为查询条件,调用list方法获取符合条件的用户列表
List<User> users = userService.list(wrapper);
按照之前的思路就是先创建一个wrapper,然后将wrapper塞到userService.list里面去,其实可以省略下面那步,直接使用Lambda条件构造器完成对数据的查询:
// 使用lambdaQuery()方法开始构建Lambda查询
List<User> users = lambdaQuery()
// 如果name不为null,则添加用户名模糊查询条件
.like(name != null, User::getUsername, name)
// 如果status不为null,则添加用户状态查询条件
.eq(status != null, User::getStatus, status)
// 如果minBalance不为null,则添加查询用户余额大于minBalance的条件
.gt(minBalance != null, User::getBalance, minBalance)
// 如果maxBalance不为null,则添加查询用户余额小于maxBalance的条件
.lt(maxBalance != null, User::getBalance, maxBalance)
// 执行查询,返回满足条件的用户列表
.list();
无需再定义wrapper,使用lambdaQuery()直接一步到位,当然,有lambdaQuery()也有lambdaUpdate(),除了各自提供的方法有所区别,使用方法基本一致:
使用lambdaUpdate()方法更新用户的余额和状态:
// 使用lambdaUpdate()方法开始构建Lambda更新
boolean success = lambdaUpdate()
// 设置用户的balance属性为remainBalance
.set(User::getBalance, remainBalance)
// 如果remainBalance为0,设置用户状态为冻结状态
.set(remainBalance == 0, User::getStatus, UserStatus.FROZEN)
// 增加更新条件,只更新id属性为id的用户
.eq(User::getId, id)
// 增加更新条件,只更新balance属性为user.getBalance()的用户
.eq(User::getBalance, user.getBalance())
// 执行更新操作,成功返回true,失败返回false
.update();
Service接口
MybatisPlus不仅提供了BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的service模板方法。
通用接口为IService
,默认实现为ServiceImpl
,其中封装的方法可以分为以下几类:
save
:新增
save
是新增单个元素saveBatch
是批量新增saveOrUpdate
是根据id判断,如果数据存在就更新,不存在则新增saveOrUpdateBatch
是批量的新增或修改
remove
:删除
removeById
:根据id删除removeByIds
:根据id批量删除removeByMap
:根据Map中的键值对为条件删除remove(Wrapper<T>)
:根据Wrapper条件删除
update
:更新
updateById
:根据id修改update(Wrapper<T>)
:根据UpdateWrapper
修改,Wrapper
中包含set
和where
部分update(T,Wrapper<T>)
:按照T
内的数据修改与Wrapper
匹配到的数据updateBatchById
:根据id批量修改
get
:查询单个结果
getById
:根据id查询1条数据getOne(Wrapper<T>)
:根据Wrapper
查询1条数据getBaseMapper
:获取Service
内的BaseMapper
实现,某些时候需要直接调用Mapper
内的自定义SQL
时可以用这个方法获取到Mapper
list
:查询集合结果
listByIds
:根据id批量查询list(Wrapper<T>)
:根据Wrapper条件查询多条数据list()
:查询所有
count
:计数
count()
:统计所有数量count(Wrapper<T>)
:统计符合Wrapper
条件的数据数量
getBaseMapper():当我们在service中要调用Mapper中自定义SQL时,就必须获取service对应的Mapper,就可以通过这个方法
准备:
自定义Service
接口,继承IService
以拓展方法。同时,让自定义的Service实现类
继承ServiceImpl,
这样就不用自己实现IService
中的接口了(ServiceImpl中有
)
示例:
public interface IUserService extends IService<User> { //自定义接口继承IService接口
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { //实现类继承ServiceImpl
}
然后在controller类中注入对应Service实体类,就可以使用Service接口准备的那些方便的方法了
例:
@ApiOperation("新增用户接口")
@PostMapping
public void saveUser(@RequestBody UserFormDTO userDto){
//1、把DTO拷贝到PO
User user = BeanUtil.copyProperties(userDto, User.class);
//2、新增
userService.save(user);
}
@ApiOperation("删除用户接口")
@DeleteMapping("{id}")
public void deleteUserById(@ApiParam("用户id") @PathVariable("id") Long id){
userService.removeById(id);
}
通过Service接口中的方法,再对数据库进行简单的CRUD操作时甚至不需要在逻辑层和持久层编写一句代码,直接在控制层就完成了,优雅!
当然这不代表所有的操作都可以在控制层完成,当需要进行逻辑操作,就需要自定义逻辑层方法了,当需要使用自定义sql时,就需要定义自定义持久层方法了
注:要使用IService中的批量新增功能的话,最好在application.yml
中的数据库连接配置里添加&rewriteBatchedStatements=true
参数。这个参数对于提高批量插入或更新的性能至关重要,因为它允许JDBC驱动将单次批量请求重写为少数几个请求
示例:
spring:
datasource:
url: jdbc:mysql://localhost:3306/your_database?rewriteBatchedStatements=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: your_username
password: your_password
saveBatch
方法(批量新增方法)允许指定一个batchSize
参数,表示每次批量插入的记录数。调整这个大小可以帮助平衡性能和内存使用
插件推荐:MybatisPlus
MybatisPlus
基于图形化界面完成MybatisPlus
的代码生成,非常简单
使用:
在Idea顶部菜单中,找到other
,选择Config Database
填写数据库连接的基本信息并保存
再次点击Idea顶部菜单中的other,然后选择Code Generator
填写相关信息:
点击提交,代码生成,妙哉
静态工具类:Db
有的时候Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db,
结合lambdaQuery()
和lambdaUpdate()
方法,我们可以以更流畅、类型安全的方式进行动态sql操作
循环依赖:
循环依赖是在软件开发中一个模块(可以是类、文件或组件)依赖另一个模块,而被依赖的模块同时也依赖于依赖它的那个模块的情况。这样形成一个闭环,没有一个可以独立于另一个存在,导致了依赖关系的循环,循环依赖通常是不健康的设计,其可能导致多种问题,如无法编译、代码难以理解和维护、难以实施变更等
而为了避免在逻辑层互相的依赖注入,就可以选择使用Db工具类,在不注入相关依赖的情况下执行相关查询
语法:
Db.lambdaQuery(类的字节码对象)
.相关查询语句
.相关查询语句
......
举例:
查询用户信息,而用户的地址信息封装在另一个address表中,用外键user_id关联主表的id主键,此时要连同用户信息和用户地址信息一起返回的话,就需要在查询用户信息的同时,去查询address表中符合address.user_id == user.id的数据,并封装到用户信息中一同返回:
逻辑层代码:
@Override
public UserVO queryUserAndAddressById(Long id) {
//1、查询用户
User user = getById(id);
if(user == null || user.getStatus() == UserStatus.FROZEN){
throw new RuntimeException("用户状态异常!");
}
//2、查询地址
List<Address> addresses = Db.lambdaQuery(Address.class).eq(Address::getUserId, id).list();
//3、封装VO
//转User的po为vo
UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
//转地址vo
if(CollUtil.isNotEmpty(addresses)){
userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
}
return userVO;
}
如此,就在不注入相关依赖的情况下完成了对其它表的访问查询操作,避免了循环依赖的问题
逻辑删除
逻辑删除,顾名思义,只做逻辑上的删除,不做物理上的删除
逻辑删除,实际上是在数据库中保留被删除记录的数据,通过一个字段来标记该记录已被“删除”。在查询操作时,通常会忽略这些标记为已删除的记录,使其在应用层面上"看不见",但在物理存储层面上,这些记录仍然存在
既然逻辑上已经不存在,那么对标记为删除的数据肯定也无法做相应的查询删除等操作
假设标记字段为delete,那么难道每次查询或修改的时候都要在where条件最后加上一个 and delete = 0 吗?
当然不,为了解决这个问题,MybatisPlus添加了对逻辑删除的支持
只需要在配置文件中加上以下配置,MybatisPlus就会帮我们做好相关的逻辑删除操作
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)
注意,只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。
测试:
@Test
void testLogicDelete(){
//1、删除
addressService.removeById(59L);
//2、查询
Address address = addressService.getById(59L);
System.out.println("address = " + address);
}
可以看到,MybatisPlus自动就帮忙加上了AND deleted = 0,👍
通用枚举
在定义各种各样的类时,难免会遇到一些表示状态的字段,比如1表示正常,2表示冻结......之类的,这个时候可以选择在相应实体类中将其类型定义为enum(枚举)型,更加便于代码的可维护性、可读性
就拿刚才说的表示正常,2表示冻结举例,可以定义这么一个枚举类来表示相关字段:
@Getter
public enum UserStatus {
NORMAL(1,"正常"),
FROZEN(2,"冻结"),
;
private final int value;
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
但是数据库采用的是int
类型,对应的PO也是Integer
。因此业务操作时必须手动把枚举
与Integer
转换,非常麻烦
此时就可以通过在配置文件中配置枚举处理器,并且通过@EnumValue指定对应状态码的相应字段,就可以达到枚举类与数据库的int类型相互转换的效果,并且还可以通过@JsonValue字段指定枚举类的返回值
配置枚举处理器:
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
添加注解:
@Getter
public enum UserStatus {
NORMAL(1,"正常"),
FROZEN(2,"冻结"),
;
@EnumValue //与数据库对应值关联的枚举类成员变量
private final int value;
@JsonValue //指定枚举类返回值(默认为枚举项名字,NORMAL、FROZEN)
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
测试:
JSON类型处理器
数据库存储的JSON类型数据都是字符串,在Java中,一般用实体类或集合存储JSON类型数据,那么从数据库读取JSON类型数据或将JSON类型写入数据库的时候就需要进行一个转换操作,将字符串与相应实体类类型互相转换,为了处理这种情况,MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler
处理器
使用方法非常简单,直接使用MybatisPlus常用注解中的@TableField注解,配置其中的typeHandler = JacksonTypeHandler.class,然后将注解添加到相应的字段之上即可
然后MybatisPlus就会自动进行相关转换了:
分页插件
在未引入分页插件的情况下,MybatisPlus
是不支持分页功能的,IService
和BaseMapper
中的分页方法都无法正常起效。 所以,我们必须配置分页插件
配置步骤
1、在项目中新建一个配置类
2、编写配置类:
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();
//1、创建分页插件
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); //数据库类型
paginationInnerInterceptor.setMaxLimit(1000L); //查询最大上限
//2、添加分页插件
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
一旦完成了上述的分页插件配置后,我们就可以在MyBatis Plus项目中轻松实现分页查询功能。接下来,我将展示如何在实际应用中使用这个分页插件
首先,请确保你已经将上述分页插件配置成功集成到你的项目中
在Service层使用分页查询
MyBatis Plus提供了Page<>
类作为分页查询的返回类型,这使得分页查询变得极其简单。以下是一个使用分页插件进行分页查询的示例:
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
public interface UserService extends IService<User> {
default IPage<User> findUserByPage(int currentPage, int pageSize) {
// 创建Page对象
Page<User> page = new Page<>(currentPage, pageSize);
// 调用MyBatis Plus提供的分页查询方法
return this.page(page);
}
}
在上面的例子中,UserService
是扩展自IService<User>
的一个接口,在其中我们定义了一个findUserByPage
方法,有两个参数:currentPage
(当前页)和pageSize
(页大小)。在方法中,我们首先创建了一个Page<User>
对象,并将其作为参数传递给page
方法,后者是IService
接口中定义的一个分页查询方法
添加排序条件的方法
使用Page
对象的addOrder
方法可以添加排序条件。以下是如何在分页查询中添加排序条件的示例代码:
// 添加排序条件,例如按照用户ID降序排列
page.addOrder(OrderItem.desc("user_id"));
// 也可以添加多个排序条件
// page.addOrder(OrderItem.desc("another_column"));