一文带你掌握MyBatis-Plus的plus高级功能点如何使用

一文带你掌握MyBatis-Plus的plus高级功能点如何使用

一文带你掌握MyBatis-Plus的plus高级功能点如何使用

今天接着之前总结的入门教程分析:MyBatis-Plus最详细的入门教程,首先还是同样地需要准备一张表tb_user:

CREATE TABLE `tb_user` (
 `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
 `user_no` varchar(255) NOT NULL COMMENT '编号',
 `nickname` varchar(255) DEFAULT NULL COMMENT '昵称',
 `email` varchar(255) DEFAULT NULL COMMENT '邮箱',
 `phone` varchar(255) NOT NULL COMMENT '手机号',
 `gender` tinyint(4) NOT NULL DEFAULT '0' COMMENT '性别 0:男生   1:女生',
 `birthday` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '出生日期',
 `is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标志 0:否 1:是',
 `create_time` datetime DEFAULT NULL COMMENT '创建时间',
 `update_time` datetime DEFAULT NULL COMMENT '更新时间',
 `create_by` bigint(20) DEFAULT NULL COMMENT '创建人',
 `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
 `address` varchar(1024) DEFAULT NULL COMMENT '地址',
 PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

在项目服务中对应的实体类User:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "tb_user")
public class User {
   @TableId(type = IdType.AUTO)
   private Long id;
   private String userNo;
   private String nickname;
   private String email;
   private String phone;
   private Integer gender;
   private Date birthday;
   private Integer isDelete;
   private Date createTime;
   private Date updateTime;
}

下面高级功能的示例都是基于上面的表进行展开的。

1.批量插入

我们在日常开发中知道大批量插入数据可能造成性能瓶颈,所以需要格外关注。在之前的入门教程讲过mp(MyBatis-Plus简称,下文都用简称)对数据库的CRUD操作提供了service层和mapper层的接口方法封装,两者的一大区别就是service CRUD接口提供了批量保存的操作,下面就分别来看看批量保存1000条user数据,然后对执行时间进行统计对比和性能评估,这里我连接的数据库是4核8G10M轻量服务器部署的,不同的数据库服务环境配置,执行效率是不一样的,所以下面的执行时间仅供对比参考。

mapper层的CRUD接口

    /**
    * mapper层的crud接口方法批量插入
    */
   @Test
   public void testMapperBatchAdd() {
       List<User> users = new ArrayList<>();
       for(long i = 1; i <= 1000; i++) {
           User user = User.builder()
                  .id(i)
                  .userNo("No-" + i)
                  .nickname("哈哈")
                  .phone("12345678901")
                  .email("shepherd_123@qq.com")
                  .birthday(new Date())
                  .gender(0)
                  .isDelete(0)
                  .build();
           users.add(user);
      }
       long start = System.currentTimeMillis();
       users.forEach(user -> {
           userDAO.insert(user);
      });
       long end = System.currentTimeMillis();
       System.out.println("执行时长:" + (end-start) + "毫秒");
  }

控制台输出如下:

执行时长:42516毫秒

service层CRUD接口

    /**
    * service层的crud接口方法批量插入
    */
   @Test
   public void testServiceBatchAdd() {
       List<User> users = new ArrayList<>();
       for(long i = 1; i <= 1000; i++) {
           User user = User.builder()
                  .id(i)
                  .userNo("No-" + i)
                  .nickname("哈哈")
                  .phone("12345678901")
                  .email("shepherd_123@qq.com")
                  .birthday(new Date())
                  .gender(0)
                  .isDelete(0)
                  .build();
           users.add(user);
      }
       long start = System.currentTimeMillis();
       userService.saveBatch(users);
       long end = System.currentTimeMillis();
       System.out.println("执行时长:" + (end-start) + "毫秒");
  }

执行结果:

执行时长:19385毫秒

可以看出,使用service层提供的批量保存接口userService.saveBatch(users)虽然快了很多,却仍需要19s,耗时比起mapper层的一条一条地插入快了23s,但是对于服务接口响应来说还是不可接受的。那为什么批量插入保存还是比较慢呢?

MySQL 的 JDBC 连接的 url 中要加 rewriteBatchedStatements 参数,并保证 5.1.13 以上版本的驱动,才能实现高性能的批量插入。

spring:
datasource:
  driver-class-name: com.mysql.cj.jdbc.Driver
  username: root
  password: root
  url: jdbc:mysql://ip:3306/db_test?&serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF8&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true

再次执行上面的service层crud接口批量插入方法,执行结果如下:

执行时长:1364毫秒

可以看到 jdbcurl 添加了 rewriteBatchedStatements=true 参数后,批量操作的执行耗时已经只有 1364 毫秒快的飞起,具体缘由分析请看:https://juejin.cn/post/7295688187752562751

2.逻辑删除

现今互联网系统数据安全越发重要,逻辑删表是指在删除表中数据时,并不是直接将数据从表中删除,而是将数据的状态标记为已删除。这种方式被称为逻辑删除,与之相对的是物理删除。逻辑删除可以保留数据的完整性,同时也方便数据恢复。

添加配置如下:

mybatis-plus:
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

测试样例:

    /**
* 逻辑删除
*/
@Test
public void testLogicDelete() {
userDAO.deleteById(1L);
}

控制台输出如下:

2023-11-16 17:01:47.656 DEBUG 10649 --- [           main] c.s.m.demo.dao.UserDAO.deleteById        : ==>  Preparing: UPDATE tb_user SET is_delete=1 WHERE id=? AND is_delete=0
2023-11-16 17:01:47.702 DEBUG 10649 --- [ main] c.s.m.demo.dao.UserDAO.deleteById : ==> Parameters: 1(Long)
2023-11-16 17:01:47.744 DEBUG 10649 --- [ main] c.s.m.demo.dao.UserDAO.deleteById : <== Updates: 1

从日志可以看出做了更新,这就是逻辑删除,接下来我们看看查询:

   @Test
public void testQuery() {
User user = userDAO.selectById(1L);
}
2023-11-16 17:06:19.802 DEBUG 10687 --- [           main] c.s.m.demo.dao.UserDAO.selectById        : ==>  Preparing: SELECT id,user_no,nickname,email,phone,gender,birthday,is_delete,create_time,update_time FROM tb_user WHERE id=? AND is_delete=0
2023-11-16 17:06:19.851 DEBUG 10687 --- [ main] c.s.m.demo.dao.UserDAO.selectById : ==> Parameters: 1(Long)
2023-11-16 17:06:19.896 DEBUG 10687 --- [ main] c.s.m.demo.dao.UserDAO.selectById : <== Total: 0

可以知道加上了未删除标志位这个条件is_delete=0,注意逻辑删除会造成唯一索引冲突

建立唯一索引时,需要额外增加 delete_time 字段,添加到唯一索引字段中,避免唯一索引冲突。例如说,tb_user 使用 user_no 作为唯一索引:

  • 未添加前:先逻辑删除了一条 user_no = 001 的记录,然后又插入了一条 user_no = 001 的记录时,会报索引冲突的异常。

  • 已添加后:先逻辑删除了一条 user_no = 001 的记录并更新 delete_time 为当前时间,然后又插入一条 user_no = 001 并且 delete_time 为 0 的记录,不会导致唯一索引冲突。

3.默认字段填充

按照一般设计规范,数据库表都有这几个字段创建时间(create_time)、更新时间(update_time)、创建人(create_by)、更新人(update_by),所以我抽取一个基础的DO类:BaseDO

/**
* 数据库表字段公共属性抽象类
*/
@Data
public class BaseDO implements Serializable {
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 最后更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private Long createBy;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
/**
* 是否删除
*/
// @TableLogic
// private Integer deleted;
}

定义一个类实现MetaObjectHandler接口完成自动填充逻辑:DefaultDBFieldHandler

/**
* 公共字段属性值自动填充
*/
public class DefaultDBFieldHandler implements MetaObjectHandler {

@Override
public void insertFill(MetaObject metaObject) {
if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();

Date current = new Date();
// 创建时间为空,则以当前时间为插入时间
if (Objects.isNull(baseDO.getCreateTime())) {
baseDO.setCreateTime(current);
}
// 更新时间为空,则以当前时间为更新时间
if (Objects.isNull(baseDO.getUpdateTime())) {
baseDO.setUpdateTime(current);
}

baseDO.setCreateBy(1001L);
baseDO.setUpdateBy(1002L);

// // 根据登录上下文信息设置创建人和更新人
// LoginUser currentUser = RequestUserHolder.getCurrentUser();
// // 当前登录用户不为空,创建人为空,则当前登录用户为创建人
// if (Objects.nonNull(currentUser) && Objects.isNull(baseDO.getCreator())) {
// baseDO.setCreator(currentUser.getId());
// }
// // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
// if (Objects.nonNull(currentUser) && Objects.isNull(baseDO.getUpdater())) {
// baseDO.setUpdater(currentUser.getId());
// }
}
}

@Override
public void updateFill(MetaObject metaObject) {
// 更新时间为空,则以当前时间为更新时间
Object modifyTime = getFieldValByName(“updateTime”, metaObject);
if (Objects.isNull(modifyTime)) {
setFieldValByName(“updateTime”, new Date(), metaObject);
}
Object modifier = getFieldValByName(“updateBy”, metaObject);
if (Objects.isNull(modifier)) {
setFieldValByName(“updateBy”, 1002L, metaObject);
}

// LoginUser currentUser = RequestUserHolder.getCurrentUser();
// // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
// if (Objects.nonNull(currentUser) && Objects.isNull(modifier)) {
// setFieldValByName(“updater”, currentUser.getId(), metaObject);
// }

}
}

注入容器:

@Bean
public MetaObjectHandler defaultMetaObjectHandler(){
return new DefaultDBFieldHandler();
}

测试示例:让上面的User类继承BaseDO

  @Test
public void testAutoFieldFill() {
User user = User.builder()
.id(1001L)
.userNo("No-001")
.nickname("哈哈")
.phone("12345678901")
.email("shepherd_123@qq.com")
.birthday(new Date())
.gender(0)
.isDelete(0)
.build();
userDAO.insert(user);
}

上面我们并没有对创建时间,创建人等字段设置值,执行控制台输出如下:

2023-11-16 18:16:41.225 DEBUG 11359 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert  : ==>  Preparing: INSERT INTO tb_user ( id, user_no, nickname, email, phone, gender, birthday, is_delete, create_time, update_time, create_by, update_by ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
2023-11-16 18:16:41.267 DEBUG 11359 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert : ==> Parameters: 1001(Long), No-001(String), 哈哈(String), shepherd_123@qq.com(String), 12345678901(String), 0(Integer), 2023-11-16 18:16:40.968(Timestamp), 0(Integer), 2023-11-16 18:16:41.018(Timestamp), 2023-11-16 18:16:41.018(Timestamp), 1001(Long), 1002(Long)
2023-11-16 18:16:41.272 DEBUG 11359 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert : <==   Updates: 1

可以看到四个基础字段都进行了值填充。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

交流探讨qun:Shepherd_126

4.字段类型处理

我们平时会碰到有些表字段是一个”复杂”字段类型,比如说是个JSON或者数组,这时候需要手动处理之后保存,查询之后也需要自己处理转换,虽然不难但是繁杂,所以mp针对这一情况也做了对应功能,比如每个人都包含一个户籍地址,如果地址对象类是Address,那个user就包含一个address属性:

首先数据库tb_user表添加添加一个地址字段address,存的是JSON字符串,所以长度大一些varchar(1024),接着定义地址类Address

@Data
public class Address {
   private Long id;
   private String province;
   private String city;
   private String region;
   private String address;
}

实体类中添加地址address属性:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "tb_user", autoResultMap = true)
public class User extends BaseDO {

   @TableId(type = IdType.AUTO)
   private Long id;

   private String userNo;

   private String nickname;

   private String email;

   private String phone;

   private Integer gender;

   @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
   private Date birthday;

   private Integer isDelete;

   @TableField(typeHandler = JacksonTypeHandler.class)
   private Address address;

}

测试示例:

  @Test
 public void testTypeHandler() {
     Address address = new Address();
     address.setProvince("浙江省");
     address.setCity("杭州市");
     address.setRegion("余杭区");
     address.setAddress("城北万象城");
     address.setId(1L);
     User user = User.builder()
            .id(100L)
            .userNo("No-001")
            .nickname("哈哈")
            .phone("12345678901")
            .email("shepherd_123@qq.com")
            .birthday(new Date())
            .gender(0)
            .isDelete(0)
            .address(address)
            .build();
     userDAO.insert(user);
}
2023-11-16 18:37:58.251 DEBUG 11546 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert  : ==>  Preparing: INSERT INTO tb_user ( id, user_no, nickname, email, phone, gender, birthday, is_delete, address, create_time, update_time, create_by, update_by ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
2023-11-16 18:37:58.333 DEBUG 11546 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert : ==> Parameters: 100(Long), No-001(String), 哈哈(String), shepherd_123@qq.com(String), 12345678901(String), 0(Integer), 2023-11-16 18:37:57.997(Timestamp), 0(Integer), {"id":1,"province":"浙江省","city":"杭州市","region":"余杭区","address":"城北万象城"}(String), 2023-11-16 18:37:58.039(Timestamp), 2023-11-16 18:37:58.039(Timestamp), 1001(Long), 1002(Long)
2023-11-16 18:37:58.336 DEBUG 11546 --- [           main] c.s.mybatisplus.demo.dao.UserDAO.insert : <==   Updates: 1

查询示例:

    @Test
   public void testQuery() {
       User user = userDAO.selectById(100L);
       System.out.println(user);
  }
2023-11-16 18:42:47.023 DEBUG 11564 --- [           main] c.s.m.demo.dao.UserDAO.selectById        : ==>  Preparing: SELECT id,user_no,nickname,email,phone,gender,birthday,is_delete,address,create_time,update_time,create_by,update_by FROM tb_user WHERE id=?
2023-11-16 18:42:47.051 DEBUG 11564 --- [           main] c.s.m.demo.dao.UserDAO.selectById       : ==> Parameters: 100(Long)
2023-11-16 18:42:47.164 DEBUG 11564 --- [           main] c.s.m.demo.dao.UserDAO.selectById       : <==     Total: 1
User(id=100, userNo=No-001, nickname=哈哈, email=shepherd_123@qq.com, phone=12345678901, gender=0, birthday=Thu Nov 16 18:37:58 CST 2023, isDelete=0, address=Address(id=1, province=浙江省, city=杭州市, region=余杭区, address=城北万象城))

可以看出复杂类型对象字段都自动转换了,注意在实体类上一定要指定@TableName(autoResultMap = true)

之前我们分析过的字段加密存储,也是通过类型转换器实现,详见:Spring Boot如何优雅实现数据加密存储

5.动态表名

业务系统随着使用时间的推移数据量越来越大,所以我们会对数据量大的表进行归档处理,比如说通过日期进行了水平分表,需要通过日期参数,动态的查询数据,Mybatis-plus可以通过此插件解析替换设定表名为处理器的返回表名

插件配置

    @Bean
public MybatisPlusInterceptor paginationInterceptor()
{
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//动态表名
interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor());
}

@Bean
public DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor()
{
DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor =new DynamicTableNameInnerInterceptor();
//
TableNameHandler tableNameHandler=new MyTableNameHandler();
dynamicTableNameInnerInterceptor.setTableNameHandler(tableNameHandler);
return dynamicTableNameInnerInterceptor;
}

动态表名规则处理类

public class MyTableNameHandler implements TableNameHandler
{
private Logger logger = LoggerFactory.getLogger(MyTableNameHandler.class);
@Override
public String dynamicTableName(String sql, String tableName)
{
logger.info("dynamicTableName sql:{},tableName:{}",sql,tableName);

//如果参数为空,则以不要动态动态查询表名
Map<String, Object> paramMap = RequestDataHelper.getRequestData();
if(paramMap==null || paramMap.isEmpty())
{
logger.info(“dynamicTableName paramMap is null”);
return tableName;
}

// 获取参数方法
paramMap.forEach((k, v) -> logger.info(k + “—-” + v));

int random = (int) paramMap.get(“tableNo”);
String tableNo = “_1”;
if (random % 2 == 1)
{
tableNo = “_2”;
}
String queryTableName=tableName + tableNo;
logger.info(“———> queryTableName:{}”,queryTableName);
return queryTableName;
}
}

参数上下文传递

public class RequestDataHelper
{
   /**
    * 请求参数存取
    */
   private static final ThreadLocal<Map<String, Object>> REQUEST_DATA = new ThreadLocal<>();

   /**
    * 设置请求参数
    *
    * @param requestData 请求参数 MAP 对象
    */
   public static void setRequestData(Map<String, Object> requestData) {
       REQUEST_DATA.set(requestData);
  }


   /**
    * 获取请求参数
    *
    * @return 请求参数 MAP 对象
    */
   public static Map<String, Object> getRequestData()
  {
       return REQUEST_DATA.get();
  }
}

6.多租户插件

关于多租户的使用场景和功能实现,插件使用,之前我们已经总结分析过了,传送门:浅析SaaS多租户系统数据隔离实现方案

7.总结

以上全部就是mp提供的一些高级扩展功能,在日常开发中比较实用,都有相应的场景去使用,提高代码高效性,防止重复编码。

THE END
喜欢就支持一下吧
点赞11 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容