MybatisPlus 快速回忆
🐦

MybatisPlus 快速回忆

Created
Apr 30, 2024 06:55 AM
Tags

快速入门

如果只使用 Mybatis 需要自己编写基础的增删改查👇,使用 MybatisPlus 可以减少工作量
notion image
notion image
 
导入依赖
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.6</version> </dependency>
继承 BaseMapper<实体类> 即可使用即成的方法:
notion image
notion image

原理

约定大于配置
利用反射,通过在 BaseMapper 中传入的实体类确定数据库字段,所以要求传入的实体类要遵守如下约定:
  • id 字段作为主键
  • 驼峰对应下划线:实体类类名 UserInfo 对应数据库表名 user_info,实体类属性中 updateTime 对应数据库字段 update_time
如果不能符合上面约定,则需要进行相应的自定义配置

注解

通过如下注解进行自定义配置
  • @TableName 指定表名
  • @TableId 指定主键
  • @TableField 指定普通字段

条件构造器

条件构造器就是可以帮助我们使用面向对象的方式实现数据库操作的where条件,在MyBatisPlus中将它封装成了一个 Wrapper 对象。
我们在使用时常用的具体实现是 QueryWrapper  和  UpdateWrapper,其实在使用过程中无论修改还是查询用的都是 QueryWrapper反正直接用QueryWrapper就完事
@Test public void testDelete() { // 创建QueryWrapper对象 QueryWrapper<User> queryWrapper = new QueryWrapper<>(); // 直接.即可其中isNull = where name IS NULL,查询名字部位空的 queryWrapper .isNull("name") // 如果继续.说明条件使用 AND 连接,ge为大于等于的意思sql为:and age >= 12 .ge("age", 12) // isNotNull就和isNull相反啦 .isNotNull("email"); // 调用delete方法说明要根据条件进行数据删除 int result = userMapper.delete(queryWrapper); System.out.println("delete return count = " + result); }
DELETE FROM tb_user WHERE name IS NULL AND age >= 12 AND email IS NOT NULL;
@Test public void testSelectList() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); Map<String, Object> map = new HashMap<>(); map.put("id", 2); map.put("name", "Jack"); map.put("age", 20); queryWrapper.allEq(map); List<User> users = userMapper.selectList(queryWrapper); users.forEach(System.out::println); } // 等同于 @Test public void testSelectList() { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("id", 2).eq("name", "Jack").eq("age", 20); List<User> users = userMapper.selectList(queryWrapper); users.forEach(System.out::println); }
more👇

自定义sql

IService

notion image
在 service 层同样可以使用 MybatisPlus 提供的通用的一些实现基础业务的方法
如图,service 包括抽象的 UserServiceUserServiceImpl
public interface UserService { }
public class UserServiceImpl implements UserService { }
和 mapper 层上继承 BaseMapper 一样,在UserService 上我们继承 IService 并指定范型为对应的实体类,此时在接口上具备了调用这些基础业务方法的能力
public interface UserService extends IService<User>{ }
但是在实现类UserServiceImpl 上我们需要实现这些方法,如果自己实现很麻烦,我们使用MybatisPlus 提供的 ServiceImpl 接口,在范型上提供对应继承了 BaseMapper 的 Mapper 和实体类
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }
关于使用
上面四个视频中批量插入需要启动mysql数据库的一些配置

逻辑删除

对于一些比较重要的数据,我们往往会采用逻辑删除的方案,即:
  • 在表中添加一个字段标记数据是否被删除
  • 当删除数据时把标记置为true
  • 查询时过滤掉标记为true的数据
一旦采用了逻辑删除,所有的查询和删除逻辑都要跟着变化,非常麻烦。
为了解决这个问题,MybatisPlus就添加了对逻辑删除的支持。
注意,只有 MybatisPlus 生成的 SQL 语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。
我们给address表添加一个逻辑删除字段,然后给Address实体添加deleted字段:
alter table address add deleted bit default b'0' null comment '逻辑删除';
notion image
接下来,我们要在application.yml配置逻辑删除字段
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)
测试:
@Test void testDeleteByLogic() { // 删除方法与以前没有区别 addressService.removeById(59L); }
notion image
@Test void testQuery() { List<Address> list = addressService.list(); list.forEach(System.out::println); }
notion image
开启了逻辑删除功能以后,我们就可以像普通删除一样做CRUD,基本不用考虑代码逻辑问题。还是非常方便的。

通用枚举

注意到User类中有一个用户状态字段:
notion image
像这种字段我们一般会定义一个枚举,做业务判断的时候就可以直接基于枚举做比较。但是我们数据库采用的是 int 类型,对应的PO也是Integer。因此业务操作时必须手动把枚举Integer转换,非常麻烦。
因此,MybatisPlus 提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换
我们定义一个用户状态的枚举,然后把User类中的status字段改为UserStatus 类型::
@Getter public enum UserStatus { NORMAL(1, "正常"), FREEZE(2, "冻结") ; private final int value; private final String desc; UserStatus(int value, String desc) { this.value = value; this.desc = desc; } }
notion image
要让MybatisPlus处理枚举与数据库类型自动转换,我们必须告诉MybatisPlus,枚举中的哪个字段的值作为数据库值。 MybatisPlus提供了@EnumValue注解来标记枚举属性:
@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; } }
结合配置
mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
测试
@Test void testService() { List<User> list = userService.list(); list.forEach(System.out::println); }
notion image
同时,为了使页面查询结果也是枚举格式,我们需要修改UserVO中的status属性:
notion image
并且,在 UserStatus 枚举中通过@JsonValue注解标记 JSON 序列化时展示的字段,最后,在页面查询:
@Getter public enum UserStatus { NORMAL(1, "正常"), FREEZE(2, "冻结") ; @EnumValue private final int value; @JsonValue private final String desc; UserStatus(int value, String desc) { this.value = value; this.desc = desc; } }
notion image

JSON 类型处理器

如果数据库的 user 表中有一个info字段,是 JSON 类型,目前User实体类中却是String类型:
notion image
 
notion image
我们要读取 info 中的属性时就非常不方便。如果要方便获取,info 最好是一个Map或者实体类,而一旦我们把 info 改为对象类型,就需要在写入数据库时手动转为String,读取数据库时手动转为对象,非常麻烦。因此 MybatisPlus 提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理 JSON 可以使用JacksonTypeHandler处理器。
首先定义实体类
import lombok.Data; @Data public class UserInfo { private Integer age; private String intro; private String gender; }
info 改为 UserInfo 类型,并声明类型处理器,测试发现所有数据都正确封装:
notion image
notion image
注意:对象嵌套需要在表名注解 @TableName开启自动映射:
notion image

分页插件

在未引入分页插件的情况下,MybatisPlus 是不支持分页功能的,IServiceBaseMapper中的分页方法都无法正常起效。所以,我们必须配置分页插件。
@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); }
notion image
支持排序
int pageNo = 1, pageSize = 5; // 分页参数 Page<User> page = Page.of(pageNo, pageSize); // 排序参数, 通过OrderItem来指定 page.addOrder(new OrderItem("balance", false)); userService.page(page);

通用分页实体

现在要实现一个用户分页查询的接口,接口规范如下:
参数
说明
请求方式
GET
请求路径
/users/page
请求参数
{ "pageNo": 1, "pageSize": 5, "sortBy": "balance", "isAsc": false, "name": "o", "status": 1 }
返回值
{ "total": 100006, "pages": 50003, "list": [ { "id": 1685100878975279298, "username": "user_9****", "info": { "age": 24, "intro": "英文老师", "gender": "female" }, "status": "正常", "balance": 2000 } ] }
特殊说明
• 如果排序字段为空,默认按照更新时间排序 • 排序字段不为空,则按照排序字段排序
这里需要定义3个实体:
  • UserQuery:分页查询条件的实体,包含分页、排序参数、过滤条件
  • PageDTO:分页结果实体,包含总条数、总页数、当前页数据
  • UserVO:用户页面视图实体
UserQuery 如下
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @Data @ApiModel(description = "用户查询条件实体") public class UserQuery { @ApiModelProperty("用户名关键字") private String name; @ApiModelProperty("用户状态:1-正常,2-冻结") private Integer status; @ApiModelProperty("余额最小值") private Integer minBalance; @ApiModelProperty("余额最大值") private Integer maxBalance; }
UserQuery
其中缺少的仅仅是分页条件,而分页条件不仅仅用户分页查询需要,以后其它业务也都有分页查询的需求。因此建议将分页查询条件单独定义为一个PageQuery实体:
@Data @ApiModel(description = "分页查询实体") public class PageQuery { @ApiModelProperty("页码") private Long pageNo; @ApiModelProperty("页码") private Long pageSize; @ApiModelProperty("排序字段") private String sortBy; @ApiModelProperty("是否升序") private Boolean isAsc; }
PageQuery
然后,让我们的UserQuery继承这个实体:
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; @EqualsAndHashCode(callSuper = true) @Data @ApiModel(description = "用户查询条件实体") public class UserQuery extends PageQuery { @ApiModelProperty("用户名关键字") private String name; @ApiModelProperty("用户状态:1-正常,2-冻结") private Integer status; @ApiModelProperty("余额最小值") private Integer minBalance; @ApiModelProperty("余额最大值") private Integer maxBalance; }
UserQuery
然后是 PageDTO
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.util.List; @Data @ApiModel(description = "分页结果") public class PageDTO<T> { @ApiModelProperty("总条数") private Long total; @ApiModelProperty("总页数") private Long pages; @ApiModelProperty("集合") private List<T> list; }
PageDTO
接口
import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("users") @RequiredArgsConstructor public class UserController { private final UserService userService; @GetMapping("/page") public PageDTO<UserVO> queryUsersPage(UserQuery query){ return userService.queryUsersPage(query); } }
然后在 IUserService 中创建 queryUsersPage 方法并在 UserServiceImpl 中实现:
PageDTO<UserVO> queryUsersPage(PageQuery query);
@Override public PageDTO<UserVO> queryUsersPage(PageQuery query) { // 1.构建条件 // 1.1.分页条件 Page<User> page = Page.of(query.getPageNo(), query.getPageSize()); // 1.2.排序条件 if (query.getSortBy() != null) { page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc())); }else{ // 默认按照更新时间排序 page.addOrder(new OrderItem("update_time", false)); } // 2.查询并回填到page对象 page(page); // 3.数据非空校验 List<User> records = page.getRecords(); if (records == null || records.size() <= 0) { // 无数据,返回空结果 return new PageDTO<>(page.getTotal(), page.getPages(), Collections.emptyList()); } // 4.有数据,转换 List<UserVO> list = BeanUtil.copyToList(records, UserVO.class); // 5.封装返回 return new PageDTO<UserVO>(page.getTotal(), page.getPages(), list); }
在上面的代码中,从 PageQueryMybatisPlusPage 之间转换的过程还是比较麻烦的。
我们完全可以在 PageQuery 这个实体中定义一个工具方法,简化开发。
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); } }
在查询出分页结果后,数据的非空校验、数据的vo转换都是重复代码,编写起来很麻烦。 我们完全可以将其封装到 PageDTO 的工具方法中,简化整个过程:
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); } }
最终,业务层的代码可以简化为:
@Override public PageDTO<UserVO> queryUserByPage(PageQuery query) { // 1.构建条件 Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc(); // 2.查询 page(page); // 3.封装返回 return PageDTO.of(page, UserVO.class); }
如果是希望自定义PO到VO的转换过程,可以这样做:
@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; }); }