前面我们了解过的ORM[Object Relational Mapping(对象关系映射)]持久层框架MyBatis,它可以帮助我们完成单表、多表、动态SQL的业务逻辑处理,与之平级的还有Hibernate,和我们要了解的MyBatisPlus了。
首先我们根据提供的资料导入一下工程案例,一起看下实现下列功能的代码逻辑:
代码执行流程如下:
当我们打开:UserMapper_20231023_150307.xml 文件后,可以看到熟悉的代码
但是我们不禁要思考一个问题,简单的单表的增删改查,他的SQL语句基本固定,这种是否可以优化呢?当然是可以的,这就是这一小节MP要做的事情。而要引入MP框架并应用起来,遵循下面的步骤即可。
MyBatisPlus官方提供了starter,其中集成了Mybatis和MybatisPlus的所有功能,并且实现了自动装配效果。因此我们可以用MybatisPlus的starter代替Mybatis的starter:
<!--MybatisPlus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
然后我们在自己的mapper接口中继承BaseMapper,并声明泛型对应的实体类就可以有常见的CRUD代码了。
接下来我们就用MP依次替代上述增删改查的原始代码
整体改造完之后,我们就可以把之前:UserMapper_20231023_150307.xml 中的代码都删掉了,是不是很简洁
通过上面的案例我们可以发现mp的使用很便捷,用户基本是无感知的,那他是如何做到表跟属性值的映射的呢?MyBatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。
但是上述的都是正常情况,如果存在一些异常:如表名跟实体类不一样、实体类部分字段表中没有等,就可以通过一些常见注解来完成映射了,MybatisPlus中比较常用的几个注解如下:
假设表结构如下
则对应的实体类可以映射如下
其中
IdType枚举:
雪花算法使用@TableField的常见场景:
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 # 更新策略:只更新非空字段
具体可参考官方文档:使用配置 | MyBatis-Plus (baomidou.com)
MyBatisPlus支持各种复杂的where条件,可以满足日常开发的所有需求。当我们继承BaseMapper之后,就具备了一些常见的接口方法
这里的Wrapper就是各种条件构造器,他的子类继承体系如下
在这个体系中,AbstractWrapper定义了各种查询的条件,如:eq、in、between、like、gt、lt等。也就是说借助于他可以完成单表各种场景的查询条件组装。那为什么他又有QueryWrapper、UpdateWrapper呢?这是因为我们一个完整的查询语句是:SELECT 字段 FROM 表名 WHERE 条件
QueryWrapper可以帮助我们声明要查询的具体字段,避免查询全部字段,导致IO开销过大,从而影响SQL性能的问题。同理UpdateWrapper一样,可以帮我们更新指定字段。
接下来我们就通过几个案例,来实际体验一下吧
上述案例的SQL大概如下
SELECT id, username, info, balance
FROM user
WHERE
username like '%o%'
AND balance >= 1000
分析:因为是查询,且查询指定字段,所以这里考虑QueryWrapper
@Test
void testQueryWrapper() {
QueryWrapper<User> queryWrapper = new QueryWrapper<User>();
queryWrapper
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", "1000");
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}
上述案例的SQL大概如下
UPDATE user
SET balance = 2000
WHERE username = "jack"
分析:因为更新的where条件中带查询,所以可以用QueryWrapper
@Test
void testQueryWrapper2() {
// 1-数据
User user = new User();
user.setBalance(2000);
// 2-条件
QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "jack");
// 3-更新
userMapper.update(user, wrapper);
}
上述案例的SQL大概如下
UPDATE user
SET balance = balance - 200
WHERE id in (1, 2, 4)
分析:这个SQL更新条件不需要做额外的查询,所以UpdateWrapper可以直接搞定,并且这里是账户余额扣减200不是直接设置成两百,所以我们的更新参数第一个user给null就好
@Test
void testUpdateWrapper() {
UpdateWrapper<User> updateWrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200")
.in("id", Arrays.asList(1L, 2L, 3L));
userMapper.update(null, updateWrapper);
}
如果这里同时需要设置用户姓名拼接:***,就可以初始化一个User对象,然后做拼接,最后传递进去就可以
这个我们在项目二会使用到
我们可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分。
需求:将id在指定范围的用户(例如1、2、4 )的余额扣减指定值
正常我们的SQL语句如下
如果我们采用自定义的,就会将整个Where条件抽取,实现细节如下
①基于Wrapper构建where条件
@Test
void testUserFormatSql() {
List<Long> ids = Arrays.asList(1L, 2L, 4L);
int amount = 200;
// 1.构建条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
.in(User::getId, ids);
// 2.自定义SQL方法调用
userMapper.updateBalanceByIds(wrapper, amount);
}
②创建接口方法,在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew
void updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper,
@Param("amount") int amount);
③自定义SQL,并使用Wrapper条件,这里可以二选一
一、动态SQL
<update id="updateBalanceByIds">
UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment}
</update>
二、注解形式(直接在接口方法增加@Update注解)
@Update("UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment}")
void updateBalanceByIds(LambdaQueryWrapper<User> wrapper, int amount);
前面我们的MP接口都是在mapper层编写的,MP也为我们提供了一些更高效的service层方法
比如上述的批量新增、批量更新、查询单个、批量删除、分页等都是非常实用的。而要去实现这个功能就不能像原来Mapper层一样直接继承BaseMapper了,因为接口是有实现类的,所以我们遵循下面这一套规范:
public interface UserService extends IService<User> {
}
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
需求:实现一个根据复杂条件查询用户的接口,查询条件如下:
根据这个要求,我们不难分析出它的SQL
接下来我们在service层做一个简单实现
public List<User> queryUserList(String name,
Integer status,
Integer minBalance,
Integer maxBalance) {
return lambdaQuery()
.like(null != name, User::getUsername, name)
.eq(null != status, User::getStatus, status)
.between(null != minBalance && null != maxBalance,
User::getBalance, minBalance, maxBalance)
.list();
}
需求:根据id修改用户余额的接口,要求如下
代码实现大致如下
public void updateUser(Integer id, String userName, Integer balance) {
lambdaUpdate()
.setSql("balance = balance - " + balance)
.set( balance == 0, User::getStatus, 2)
.eq(null != id, User::getId, id)
.eq(null != userName, User::getUsername, userName)
.update();
}
需求:批量插入10万条用户数据,并作出对比:
批处理方案分析:
所以这里我们先配置一下链接参数:
然后编写一个批量测试的方法
public void batchSave() {
// 1-准备数据
List<User> userList = initData();
// 批量新增
saveBatch(userList);
}
private List<User> initData() {
List<User> userList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
User user = new User();
user.setUsername(i + "-测试用户");
user.setBalance(100);
user.setInfo("测试数据");
userList.add(user);
}
return userList;
}
前面我们写的这一大堆代码都是自己手动写出来的,这小节我们将借助于一个插件帮助开发们自动生成下面的代码
首先我们安装一个插件
安装之后,可以在other点击:config Database,配置数据库
然后点击:code generator,生成代码,规则如下
前面我们在mapper层或者service完成了各种CRUD,但是MP也提供了一个静态工具类:Db,这里面也封装了日常需要使用的接口方法,以解决:service之间相互引入,嵌套引入导致的循环依赖问题。其API如下
当我们需要使用时,如下图即可:
逻辑删除就是基于代码逻辑模拟删除效果,但并不会真正删除数据。思路如下:
如逻辑删除字段为deleted:
删除操作:
查询操作:
MybatisPlus提供了逻辑删除功能,无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。我们要做的就是在application.yaml文件中配置逻辑删除的字段名称和值即可:
注意:
逻辑删除本身也有自己的问题,比如:
因此,生产环境采用逻辑删除功能比较多,但如果数据不能删除,也可以把数据迁移到其它表中。
当数据库是status是int类型,为了实现PO类中的枚举类型变量与数据库字段的转换,我们就可以借助:@EnumValue 注解将其做映射,无需特殊处理
其实现步骤共两步
①给枚举中的与数据库对应value值添加@EnumValue注解
②在application.yml中配置全局枚举处理器:
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
当数据库中有一个JSON类型字段时,MP依然做了支持,只需要在对应属性值追加下面截图注解即可。
MP提供的内置拦截器如下,借助于这些拦截器我们就可以实现一些比较有意思的功能了。这里我们给大家讲解一下使用频率最高的分页插件。【项目二会使用到动态表名插件-做分库分表】
首先,要在配置类中注册MyBatisPlus的核心插件,同时添加分页插件:
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 1.初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 2.添加分页插件
PaginationInnerInterceptor pageInterceptor =
new PaginationInnerInterceptor(DbType.MYSQL);
pageInterceptor.setMaxLimit(1000L); // 设置分页上限
interceptor.addInnerInterceptor(pageInterceptor);
return interceptor;
}
}
接着,就可以使用分页的API了:
大概实现代码如下:
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@Test
void testPageQuery() {
// 1.查询
int pageNo = 1, pageSize = 5;
// 1.1.分页参数
Page<User> page = Page.of(pageNo, pageSize);
// 1.2.排序参数, 通过OrderItem来指定
page.addOrder(new OrderItem("balance", false));
// 1.3.分页查询
Page<User> p = userService.page(page);
// 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);
}
前面我们做了简单的分页查询之后接下来通过一个案例做一下我们今天整体功能的收尾。需求如下
参数 | 说明 |
请求方式 | GET |
请求路径 | /users/page |
请求入参 | { |
响应出参 | { "total":1005, "pages":201, "list":[ { "id":1, "username":"Jack", "info":{ "age":21, "gender":"male", "intro":"佛系青年" }, "status":"正常", "balance":2000 }, { "id":2, "username":"Rose", "info":{ "age":20, "gender":"female", "intro":"文艺青年" }, "status":"冻结", "balance":1000 } ] } |
特殊说明 |
|
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.20</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
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);
}
}
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);
}
}
package com.itheima.mp.domain.dto;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
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;
}
package com.itheima.mp.domain.po;
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;
}
package com.itheima.mp.controller;
import com.itheima.mp.domain.po.PageDTO;
import com.itheima.mp.domain.po.PageQuery;
import com.itheima.mp.domain.po.UserVO;
import com.itheima.mp.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("pages")
public PageDTO<UserVO> queryUserByPage(PageQuery query) {
return userService.queryUserByPage(query);
}
}
package com.itheima.mp.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.PageDTO;
import com.itheima.mp.domain.po.PageQuery;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.po.UserVO;
public interface UserService extends IService<User> {
PageDTO<UserVO> queryUserByPage(PageQuery query);
}
package com.itheima.mp.service;
import cn.hutool.core.collection.CollectionUtil;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.PageDTO;
import com.itheima.mp.domain.po.PageQuery;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.po.UserVO;
import com.itheima.mp.mapper.UserMapper;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public PageDTO<UserVO> queryUserByPage(PageQuery query) {
// 1-构建分页条件
Page<User> p = Page.of(query.getPageNo(), query.getPageSize());
// 2-构建排序条件
if (StringUtils.isBlank(query.getSortBy())) {
p.addOrder(new OrderItem("update_time", false));
} else {
p.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));
}
// 3-查询
page(p);
// 4-获取结果
List<User> records = p.getRecords();
// 4.1 非空判断
if (CollectionUtil.isEmpty(records)) {
// 4.2 没数据返回空
return PageDTO.empty(p);
}
// 4.3 有数据返回具体集合
return PageDTO.of(p, UserVO.class);
}
}
@Override
public PageDTO<UserVO> queryUserByPage(PageQuery query) {
// 1-构建分页条件
Page<User> p = query.toMpPageDefaultSortByUpdateTimeDesc();
// 2-查询
page(p);
// 3-返回结果
return PageDTO.of(p, UserVO.class);
}
至此,我们就完成了MP的理论学习了,MP作为Mybatis的增强版本,企业中用到的还是比较多的,尤其是QueryWrapper,希望大家可以熟练掌握。