[ORM] MyBatis-Plus实际使用中碰到的坑

我永远喜欢Spring Data JPA

Posted by Penistrong on June 21, 2023

Mybatis-Plus

自动填充配置

字段自动填充没有生效

S:

实体类基类BaseEntity中的updatedTime字段明明标注了fill=FieldFill.INSERT_UPDATE, 按照字面含义是在插入和更新时自动更新该字段,而实际使用中却并没有自动更新

T:

定位MyBatis-Plus处理字段自动更新的逻辑,尝试解决该问题

A:

查询MyBatis-Plus文档的自动填充功能一节可知自动更新的原理如下:

  • 在实体类上使用注解@TableField(..., fill = FieldFill.xxx)将对应字段标记为自动填充且可指定填充策略FieldFill

    public enum FieldFill {
      DEFAULT,        // 默认不处理
      INSERT,         // 插入时填充字段
      UPDATE,         // 更新时填充字段
      INSERT_UPDATE   // 插入和更新时填充字段
    }
    
  • 实现元对象处理接口: com.baomidou.mybatisplus.core.handlers.MetaObjectHandler

    @Component
    public class CustomMetaObjectHandler implements MetaObjectHandler {
    
        @Override
        public void insertFill(MetaObject metaObject) {
            this.fillStrategy(metaObject, 'createdTime', LocalDateTime.now());
        }
    
        @Override
        public void updateFill(MetaObject metaObject) {
            this.strictUpdateFill(metaObject, "updatedTime", () -> LocalDateTime.now(), LocalDateTime.class);
        }
    }
    

    插入或更新时就会使用上述重写的方法进行实际填充

    1. 粒度较粗的情况下,使用MetaObjectHandler接口中的fillStrategy()方法即可

    2. 如果想要粒度更细(比如以字段类型+字段名区分不同字段的填充策略)则需要使用接口中的strictInsertFillstrictUpdateFill方法

      // in interface: MetaObjectHandler 
      // The following method has 2 other overloading method 
      default <T, E extends T> MetaObjectHandler strictUpdateFill(MetaObject metaObject, String fieldName, Supplier<E> fieldVal, Class<T> fieldType) {
          return strictUpdateFill(findTableInfo(metaObject), metaObject, Collections.singletonList(StrictFill.of(fieldName, fieldVal, fieldType)));
      }
      

(坑来了)填充的原理是在插入或更新时由MetaObjectHandler实现类直接给对应的实体类对象entity设置属性值,如果无值则入库后必为null

MetaObjectHandler提供的默认策略为: 如果属性有值则不覆盖,如果填充值为null则不填充

且在使用IServiceMapper中的update(T t, Wrapper updateWrapper)时,如果实体类tnull,也会导致自动填充失效

从上面的描述中可以看到,自动填充是对实际对象entity设置值,如果不存在实际的实体类对象,那么自动填充就会失败

按照MyBatis-Plus的CRUD接口部分文档所述:

QueryWrapper(LambdaQueryWrapper) 和 UpdateWrapper(LambdaUpdateWrapper) 的父类用于生成 sql 的 where 条件, entity 属性也用于生成 sql 的 where 条件

如果没有entityMetaObjectHandler就无法在对应的entity中设置字段值,也就无法自动填充字段

比如基于LambdaUpdateWrapperlambdaUpdate()方法构造的更新条件,如果调用this.baseMapper.update(T entity, Wrappers.<T>lambdaUpdate().eq().set())时实体类对象entity传参null,由于不存在实体对象entity去构造SQL的WHERE子句,自动填充就会失效

另外,使用LambdaUpdateChainWrapper时通常都是利用链式调用在最后一步调用.update()方法,如果不给entity作为参数也会导致自动填充失效

解决方法

分两种情况讨论:

  1. 没有利用QueryWrapper查出数据表实体entity,直接使用[Lambda]Update[Chain]Wrapper进行操作

    • 调用this.update(T entity, Wrapper<T> updateWrapper): 第一个参数直接new一个entity空对象

      this.update(new CameraInstance(),
                  Wrappers.lambdaUpdate()
                             .eq(CameraInstance::getId, "xxx")
                             .set(CameraInstance::getStatus, heartBeatStatusDto.getStatus()))
      
    • 调用this.update(Wrapper<T> updateWrapper): 构造updateWrapper时new一个entity空对象传参,比如Wrappers.update(entity)或者Wrappers.lambdaUpdate(entity)

      this.update(Wrappers.lambdaUpdate(new CameraInstance())
                             .eq(CameraInstance::getId, "xxx")
                             .set(CameraInstance::getStatus, heartBeatStatusDto.getStatus()))
      
    • 调用this.lambdaUpdate()构造链式条件构造器,在链式调用的最后调用.update(T entity),new一个entity空对象传参即可

      this.lambdaUpdate()
             .eq(CameraInstance::getInstanceId, heartBeatStatusDto.getInstanceId())
             .eq(CameraInstance::getGatewayId, heartBeatStatusDto.getGatewayId())
             .set(CameraInstance::getStatus, heartBeatStatusDto.getStatus())
             .update(new CameraInstance())
      
  2. 已经使用QueryWrapper查出数据表实体entity,直接将entity作为UpdateWrapper的参数

    与上面一种情况类似,直接将查出的数据表实体entity传入UpdateWrapper用于组装SQL时的WHERE子句条件生成(调用其他API构造的条件字段可以不去重)

LambdaQuery/Update[Chain]Wrapper