【撸源码】【mybatis-plus】乐观锁和逻辑删除是如何工作的——下篇

1. 背景

上篇撸了逻辑删除的源码,这次把乐观锁的源码再撸一遍,看看乐观锁是如何实现的,同时根据细节,看能否和逻辑删除相呼应上。

2. 乐观锁工作原理

首先搞清楚乐观锁是什么。

mybatis-plus的乐观锁是这样的,比如在一张表中,增加一个version字段,在执行更新语句时,会set version = #{version} + 1, 同时会增加条件: where version = #{version} 以此来实现乐观锁的一个基本逻辑。如果当前修改的版本号和数据库中的版本号不一致,则不修改这条数据。

乐观锁和悲观锁的理论这里就不做过多赘述了。

既然了解了mp的基本工作原理,那么就来撸源码

2.1. 基本使用

mybatis-plus的用法很简单,因为乐观锁是一个插件,所以将这个插件注入到mp的全局配置中,就可以开启乐观锁插件了。

@Bean
public MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 乐观锁
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    return interceptor;
}

所以,要了解mybatis-plus的乐观锁原理,只需要去撸这个插件的源码即可。

2.2. 乐观锁源码解析

2.2.1. 构造器解析

乐观锁的构造器有两个

    public OptimisticLockerInnerInterceptor() {
        this(false);
    }

    public OptimisticLockerInnerInterceptor(boolean wrapperMode) {
        this.wrapperMode = wrapperMode;
    }

通过构造器,可以看到有一个参数,是构造时需要初始化的。wrapperMode

  • 无参构造时,默认为false。
  • 有参数构造时,则为传入的值。

这个参数,字面意思为是否为包装类型。具体的含义,后面撸代码看一下。

2.2.2. 乐观锁入口

com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor#beforeUpdate

乐观锁的入口是一个更新前处理器。

留一个点,这里为什么是入口呢?向上追溯一下,现在先继续往下看。【已处理】2.3.2mp插件装载原理

@Override
public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
    if (SqlCommandType.UPDATE != ms.getSqlCommandType()) {
        return;
    }
    if (parameter instanceof Map) {
        Map<String, Object> map = (Map<String, Object>) parameter;
        doOptimisticLocker(map, ms.getId());
    }
}

通过代码,这里是只会执行UPDATE类型的。

ms.getSqlCommandType() 这个的含义,在

【撸源码】【mybatis-plus】乐观锁和逻辑删除是如何工作的——上篇icon-default.png?t=N7T8https://blog.csdn.net/smile_795/article/details/138602029

这篇文章中,有说明,有兴趣的可以去看下。

这个类型,只有执行更新语句和逻辑删除时,会使用到这个类型。

com.baomidou.mybatisplus.core.injector.AbstractMethod#addUpdateMappedStatement(java.lang.Class<?>, java.lang.Class<?>, java.lang.String, org.apache.ibatis.mapping.SqlSource)

感兴趣的看下这个方法的引用即可明白。这个在上篇也有讲到。

那么就说明,乐观锁的作用范围在更新和逻辑删除时,会用到乐观锁。

这个方法没有什么逻辑,核心就是执行了 com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor#doOptimisticLocker 这个方法。那我们看这个方法,就可以知道乐观锁的核心原理。

2.2.3. 核心原理


protected void doOptimisticLocker(Map<String, Object> map, String msId) {
    // updateById(et), update(et, wrapper);
    Object et = map.getOrDefault(Constants.ENTITY, null);
    if (Objects.nonNull(et)) {

        // version field
        TableFieldInfo fieldInfo = this.getVersionFieldInfo(et.getClass());
        if (null == fieldInfo) {
            return;
        }

        try {
            Field versionField = fieldInfo.getField();
            // 旧的 version 值
            Object originalVersionVal = versionField.get(et);
            if (originalVersionVal == null) {
                if (null != exception) {
                    /**
                     * 自定义异常处理
                     */
                    throw exception;
                }
                return;
            }
            String versionColumn = fieldInfo.getColumn();
            // 新的 version 值
            Object updatedVersionVal = this.getUpdatedVersionVal(fieldInfo.getPropertyType(), originalVersionVal);
            String methodName = msId.substring(msId.lastIndexOf(StringPool.DOT) + 1);
            if ("update".equals(methodName)) {
                AbstractWrapper<?, ?, ?> aw = (AbstractWrapper<?, ?, ?>) map.getOrDefault(Constants.WRAPPER, null);
                if (aw == null) {
                    UpdateWrapper<?> uw = new UpdateWrapper<>();
                    uw.eq(versionColumn, originalVersionVal);
                    map.put(Constants.WRAPPER, uw);
                } else {
                    aw.apply(versionColumn + " = {0}", originalVersionVal);
                }
            } else {
                map.put(Constants.MP_OPTLOCK_VERSION_ORIGINAL, originalVersionVal);
            }
            versionField.set(et, updatedVersionVal);
        } catch (IllegalAccessException e) {
            throw ExceptionUtils.mpe(e);
        }
    }

    // update(LambdaUpdateWrapper) or update(UpdateWrapper)
    else if (wrapperMode && map.entrySet().stream().anyMatch(t -> Objects.equals(t.getKey(), Constants.WRAPPER))) {
        setVersionByWrapper(map, msId);
    }
}
2.2.3.1. 最外层解读

通过代码,最外层有两块逻辑

  • 如果当前的更新入参是Entity,那么就执行第一段逻辑。
  • 如果wrapperMode 为true,并且当前的更新入参是包装类,那么就执行第二段逻辑。

这里就看到,构造器初始化的参数是在这里使用的。

那么就可以理解为,如果开启了这个参数,在更新数据时,使用entity类型的,就会自动携带上乐观锁字段。否则就不会更新乐观锁字段。

这里为一个问题,后需可以通过测试来验证。【已处理】2.4.3关闭包装类型参数,且不使用entity来更新,是否会触发乐观锁。

继续撸代码,先来看第一段逻辑。

2.2.3.2. 第一段逻辑解读

逻辑概要

  • 获取当前实体类的类型
  • 从表信息中,获取乐观锁字段,判断当前类型中是否包含乐观锁字段。
  • 如果不包含,则直接退出,不再处理乐观锁信息。
  • 通过乐观锁字段,获取当前对象中乐观锁字段的值。
  • 如果值为空
    • 判断当前是否设置了异常类信息,如果设置了,就抛出异常。
    • 如果没有配置异常,则退出,不再处理乐观锁字段。
    • 这里设置为一个问题,如果更新时,使用entity来更新,但是version字段为null,是不是就不再更新乐观锁了?【已处理】​​​​​​​2.4.2version为null,是否会更新乐观锁字段
    • 如果装载时,增加了异常选项,version为空时,是否就会抛出异常?【已处理】​​​​​​​2.4.1装载乐观锁插件时,放入异常对象,那么在使用乐观锁时没有值是否会抛出异常。
  • 通过当前的version值和version类型,从工厂中获取一个新的version值。
    • 乐观锁工厂可以详细解读下。【已处理】​​​​​​​2.3.1乐观锁获取的工厂逻辑
  • 再次判断当前执行方法,是否为更新语句。
    • 这里需要细节性分析一下。 【已处理】​​​​​​​2.2.3.2.1是否为update细节分析
  • 如果是update
    • 获取当前上下文中的包装类条件参数,如果当前上下文中,已经存在了条件,则会增加一个乐观锁条件。
    • 如果当前上下文中,没有条件,则创建一个包装类条件参数,并增加乐观锁条件。
  • 如果不是update
    • 则在全局参数中,添加一个乐观锁的条件。
  • 将当前更新对象中的值中,乐观锁字段的值设置为刚才获取的新值。

2.2.3.2.1. 是否为update细节分析

为什么上层逻辑,已经判断了 ms.getSqlCommandType() == UPDATE,为什么这里要再判断一次?

String methodName = msId.substring(msId.lastIndexOf(StringPool.DOT) + 1);
if ("update".equals(methodName)) {
    AbstractWrapper<?, ?, ?> aw = (AbstractWrapper<?, ?, ?>) map.getOrDefault(Constants.WRAPPER, null);
    if (aw == null) {
        UpdateWrapper<?> uw = new UpdateWrapper<>();
        uw.eq(versionColumn, originalVersionVal);
        map.put(Constants.WRAPPER, uw);
    } else {
        aw.apply(versionColumn + " = {0}", originalVersionVal);
    }
} else {
    map.put(Constants.MP_OPTLOCK_VERSION_ORIGINAL, originalVersionVal);
}

仔细观察,这里的update,匹配的是methodName

再来看methodName是从哪获取到的

String methodName = msId.substring(msId.lastIndexOf(StringPool.DOT) + 1);

这里是根据msId来判断的

从上篇文章可以知道,statement的构造在这里 com.baomidou.mybatisplus.core.injector.methods.Delete#injectMappedStatement

public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
    String sql;
    SqlMethod sqlMethod = SqlMethod.LOGIC_DELETE;
    if (tableInfo.isWithLogicDelete()) {
        sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlLogicSet(tableInfo),
            sqlWhereEntityWrapper(true, tableInfo),
            sqlComment());
        SqlSource sqlSource = super.createSqlSource(configuration, sql, modelClass);
        return addUpdateMappedStatement(mapperClass, modelClass, methodName, sqlSource);
    } else {
        sqlMethod = SqlMethod.DELETE;
        sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(),
            sqlWhereEntityWrapper(true, tableInfo),
            sqlComment());
        SqlSource sqlSource = super.createSqlSource(configuration, sql, modelClass);
        return this.addDeleteMappedStatement(mapperClass, methodName, sqlSource);
    }
}

通过继续往下追溯,发现这里的id = namespace + methodName。中间以 "." 连接。

所以,这里的method,就是statement构建时的method。

而构建时的method,是com.baomidou.mybatisplus.core.enums.SqlMethod 这个枚举的method字段。

所以,这里的匹配,就是在看方法名字是不是update,而不是判断是否为更新语句。

com.baomidou.mybatisplus.core.mapper.BaseMapper#update(T, com.baomidou.mybatisplus.core.conditions.Wrapper<T>)

通过代码,查看所有的update方法,全是包装类,那么这里判断upadte的意思,就是看是否为包装类。

另外拓展看了一下,非update方法,全都不是包装类,这就很好区分了,只要是update,就处理包装类的参数。不是update,就处理map即可。

2.2.3.3. 第二段逻辑解读
private void setVersionByWrapper(Map<String, Object> map, String msId) {
  Object ew = map.get(Constants.WRAPPER);
  if (ew instanceof AbstractWrapper && ew instanceof Update) {
      Class<?> entityClass = ENTITY_CLASS_CACHE.get(msId);
      if (null == entityClass) {
          try {
              final String className = msId.substring(0, msId.lastIndexOf('.'));
              entityClass = ReflectionKit.getSuperClassGenericType(Class.forName(className), Mapper.class, 0);
              ENTITY_CLASS_CACHE.put(msId, entityClass);
          } catch (ClassNotFoundException e) {
              throw ExceptionUtils.mpe(e);
          }
      }

      final TableFieldInfo versionField = getVersionFieldInfo(entityClass);
      if (null == versionField) {
          return;
      }

      final String versionColumn = versionField.getColumn();
      final FieldEqFinder fieldEqFinder = new FieldEqFinder(versionColumn, (Wrapper<?>) ew);
      if (!fieldEqFinder.isPresent()) {
          return;
      }
      final Map<String, Object> paramNameValuePairs = ((AbstractWrapper<?, ?, ?>) ew).getParamNameValuePairs();
      final Object originalVersionValue = paramNameValuePairs.get(fieldEqFinder.valueKey);
      if (originalVersionValue == null) {
          return;
      }
      final Object updatedVersionVal = getUpdatedVersionVal(originalVersionValue.getClass(), originalVersionValue);
      if (originalVersionValue == updatedVersionVal) {
          return;
      }
      // 拼接新的version值
      paramNameValuePairs.put(UPDATED_VERSION_VAL_KEY, updatedVersionVal);
      ((Update<?, ?>) ew).setSql(String.format("%s = #{%s.%s}", versionColumn, "ew.paramNameValuePairs", UPDATED_VERSION_VAL_KEY));
  }
}

逻辑解读

  • 从参数中,获取条件
  • 条件如果不是UPDATE包装类,则不做任何处理
  • 通过statementId来获取实体类对象类型
  • 如果获取类型失败,则抛出异常。
  • 从表结构信息中,获取当前表的乐观锁字段
  • 如果不存在这个字段,就退出,不再处理乐观锁
    • 如何寻找乐观锁字段的? 【已处理】​​​​​​​2.2.3.3.1如何寻找乐观锁字段
  • 从当前条件中,获取乐观锁值
  • 如果当前条件中没有乐观锁值,就直接退出,不再处理乐观锁。
  • 通过乐观锁处理工厂来获取新的乐观锁值
  • 将新的乐观锁值放入当前要更新的字段中
  • 将当前更新条件,增加乐观锁的判断条件

这里的逻辑就比较简单,就很正常的一段处理逻辑,从包装类中获取字段,并插入值,插入条件。

和第一段逻辑中的处理有大体的类似。

这里比较有趣的代码,应该就是寻找乐观锁字段的设计了。

2.2.3.3.1. 如何寻找乐观锁字段
final FieldEqFinder fieldEqFinder = new FieldEqFinder(versionColumn, (Wrapper<?>) ew);	

写法很简单,只是new出来了一个对象,就能知道这个字段是否存在。

看下他的构造器,原来别有洞天

public FieldEqFinder(String fieldName, Wrapper<?> wrapper) {
    this.fieldName = fieldName;
    state = State.INIT;
    find(wrapper);
}

首先,将要搜索的字段名,设置到当前的属性中。

将状态初始化

接着就开始搜索这个字段了。

private boolean find(Wrapper<?> wrapper) {
    Matcher matcher;
    final NormalSegmentList segments = wrapper.getExpression().getNormal();
    for (ISqlSegment segment : segments) {
        // 如果字段已找到并且当前segment为EQ
        if (state == State.FIELD_FOUND && segment == SqlKeyword.EQ) {
            this.state = State.EQ_FOUND;
            // 如果EQ找到并且value已找到
        } else if (state == State.EQ_FOUND
            && (matcher = PARAM_PAIRS_RE.matcher(segment.getSqlSegment())).matches()) {
            this.valueKey = matcher.group(1);
            this.state = State.VERSION_VALUE_PRESENT;
            return true;
            // 处理嵌套
        } else if (segment instanceof Wrapper) {
            if (find((Wrapper<?>) segment)) {
                return true;
            }
            // 判断字段是否是要查找字段
        } else if (segment.getSqlSegment().equals(this.fieldName)) {
            this.state = State.FIELD_FOUND;
        }
    }
    return false;
}

这里的设计非常的精妙,首先他把条件列表,定义为了一个个的Segment,小的条件片段。

每一个条件,由于实现了ISqlSegment 接口,所以它还是一个枚举接口。

枚举接口,就可以实现和枚举的==比较。

protected Children addCondition(boolean condition, R column, SqlKeyword sqlKeyword, Object val) {
    return maybeDo(condition, () -> appendSqlSegments(columnToSqlSegment(column), sqlKeyword,
        () -> formatParam(null, val)));
}

看下NormalSegmentList的构造过程,可以发现,每一个条件,都是三个segment

  • 第一个:条件名
  • 第二个:表达式
  • 第三个:值

所以说,这段逻辑,正确的匹配逻辑是

  • 先判断第一个,是否为自己需要的字段。如果确定是了,当前状态为字段已找到。第二次循环时,就会到第二个segment
  • 第二个判断是否已经找到这个字段了,如果找到了,就判断当时是否为EQ。如果不是EQ,就不是自己需要的。如果是EQ,当前状态就位EQ_FOUND
  • 如果当前条件也对,就会来到第三个segment中,此时就判断第三个值是否符合正则。如果符合正则,就会从第三个中获取到拿乐观锁值的字段key,并返回找到。
  • 兼容逻辑,如果三个条件都不对,还会判断当前的segment是否会包装类,因为这个类是多个接口嘛,如果这个类是包装类,则通过递归的方式,继续循环处理。
  • 如果全部循环完,还没找到,就会标记没找到字段。

这就是整个的寻找过程,通过这个,就可以精准匹配当前的条件中是否包含字段,是否为EQ匹配,并且顺便找到了获取乐观锁字段值的key,因为是包装类嘛,所以乐观锁的值并不是在实体类中,需要通过key来为乐观锁值进行处理。

2.2.4. 总结

至此,乐观锁的核心原理已经全部完成,到这,乐观锁的整体原理逻辑均已清楚。

但是在乐观锁原理解读的过程中,有一些深入的点需要分析,接着我们就来拓展的再深度挖掘下非核心原理,实现的也很精妙,学习意义很大。

2.3. 待处理的逻辑解读

2.3.1. 乐观锁获取的工厂逻辑

// 新的 version 值
Object updatedVersionVal = this.getUpdatedVersionVal(fieldInfo.getPropertyType(), originalVersionVal);

在获取新的version值的时候,调用的是getUpdatedVersionVal 方法,就能获取到一个新的version值

来看下这块代码的逻辑

/**
 * This method provides the control for version value.<BR>
 * Returned value type must be the same as original one.
 *
 * @param originalVersionVal ignore
 * @return updated version val
 */
protected Object getUpdatedVersionVal(Class<?> clazz, Object originalVersionVal) {
    return VersionFactory.getUpdatedVersionVal(clazz, originalVersionVal);
}

这个方法是调用工厂中的version值。

public static Object getUpdatedVersionVal(Class<?> clazz, Object originalVersionVal) {
    Function<Object, Object> versionFunction = VERSION_FUNCTION_MAP.get(clazz);
    if (versionFunction == null) {
        // not supported type, return original val.
        return originalVersionVal;
    }
    return versionFunction.apply(originalVersionVal);
}

逻辑解析

  • 根据version的类型,从预置的方法中,获取一个方法,该方法是传入一个object类型的参数,获取一个object类型的值,也就是弱类型的。
  • 如果没有获取到,就将原值返回。
  • 如果获取到,就执行这个方法,来获取新的version值。

逻辑很简单,就是通过一个类型的映射,获取一个内置函数,通过函数来计算新的值。核心就是这个内置的函数是什么。

/**
 * 存放版本号类型与获取更新后版本号的map
 */
private static final Map<Class<?>, Function<Object, Object>> VERSION_FUNCTION_MAP = new HashMap<>();

函数的定义是一个HashMap,key是一个类型,value是一个函数。

static {
    VERSION_FUNCTION_MAP.put(long.class, version -> (long) version + 1);
    VERSION_FUNCTION_MAP.put(Long.class, version -> (long) version + 1);
    VERSION_FUNCTION_MAP.put(int.class, version -> (int) version + 1);
    VERSION_FUNCTION_MAP.put(Integer.class, version -> (int) version + 1);
    VERSION_FUNCTION_MAP.put(Date.class, version -> new Date());
    VERSION_FUNCTION_MAP.put(Timestamp.class, version -> new Timestamp(System.currentTimeMillis()));
    VERSION_FUNCTION_MAP.put(LocalDateTime.class, version -> LocalDateTime.now());
    VERSION_FUNCTION_MAP.put(Instant.class, version -> Instant.now());
}

这个工厂中有一块静态方法区,在类加载的时候,就会初始化进去一批内置函数。

这也是乐观锁所支持的类型。

  • long
  • Long
  • int
  • Integer
  • Date
  • Timestamp
  • LocalDateTime
  • Instant

支持这么多种类型,每一种类型后的参数,都是构造出一个新的值。

如果是数字,则自增。如果是时间, 则取当前时间。这就是乐观锁核心的自增逻辑了。

2.3.2. mp插件装载原理

乐观锁的核心原理基本已经全部讲完了,向下已经下钻到底了,这时我们回过头来,再深度解读下,为什么插件能被执行,原理是什么。

首先就是入口 com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor#beforeUpdate

这个方法为什么会被执行。

org.apache.ibatis.plugin.Plugin#invoke
↓
com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor#intercept
↓
com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor#beforeUpdate

到这里,可以看到实际执行的点是因为Plugin来执行的,是Mybatis的执行逻辑。

再往上追溯,就过于散乱了,无法深入追溯,所以就需要用到debug了。

通过debug,可以得到这样一个逻辑

通过上篇文章,我们知道,所有的sql执行时,都会通过org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor#invoke这个方法。

这个方法中,首先会获取一个sqlSession。

SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);

这里继续深入追踪,不讲细节了,最终可以看到在创建session时,会通过配置new出来一个执行器。最终是执行的这个方法

org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,
      boolean autoCommit) {
  Transaction tx = null;
  try {
    final Environment environment = configuration.getEnvironment();
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    final Executor executor = configuration.newExecutor(tx, execType);
    return new DefaultSqlSession(configuration, executor, autoCommit);
  } catch (Exception e) {
    closeTransaction(tx); // may have fetched a connection so lets call close()
    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

核心在于 final Executor executor = configuration.newExecutor(tx, execType);

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  Executor executor;
  if (ExecutorType.BATCH == executorType) {
    executor = new BatchExecutor(this, transaction);
  } else if (ExecutorType.REUSE == executorType) {
    executor = new ReuseExecutor(this, transaction);
  } else {
    executor = new SimpleExecutor(this, transaction);
  }
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  return (Executor) interceptorChain.pluginAll(executor);
}

在创建过执行器后,还会再包装一下。

继续下钻,看包装逻辑

public static Object wrap(Object target, Interceptor interceptor) {
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  Class<?> type = target.getClass();
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  if (interfaces.length > 0) {
    return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));
  }
  return target;
}

可以看到,执行器全部都包装成了一个Plugin类。

而这个plugin类,就是本小节最开始看到的调用链顶端。

那么到此,就明白了基本的运行原理

  • 执行sql,获取sqlSession
  • 构建sqlSession时,构建执行器
  • 将执行器包装成plugin类
  • 通过sqlSession执行
  • 调用执行器进行执行
  • 执行器被包装到plugin中,先执行plugin的invoke方法
  • plugin执行拦截器
  • 拦截器执行完后,调用执行器。

plugin执行器执行时,调用的就是Mybatis-plus的拦截器

而我们所有注册的mp插件,全部都注册到了Mybatis-plus的拦截器中,再由mp的拦截器进行顺序执行。

Iterator var8 = this.interceptors.iterator();

while(var8.hasNext()) {
    InnerInterceptor update = (InnerInterceptor)var8.next();
    if (!update.willDoUpdate(executor, ms, parameter)) {
        return -1;
    }

    update.beforeUpdate(executor, ms, parameter);
}

所有的逻辑全部畅通!

2.4. 待验证的逻辑点

2.4.1. 装载乐观锁插件时,放入异常对象,那么在使用乐观锁时没有值是否会抛出异常。

乐观锁插件注入时,如果配置了异常信息,在更新时,没有乐观锁字段,就会抛出异常。

如果要强制使用乐观锁,可以考虑在注入时加上这个参数。

2.4.2. version为null,是否会更新乐观锁字段

version如果为null,乐观锁字段就不会更新。

2.4.3. 关闭包装类型参数,且不使用entity来更新,是否会触发乐观锁。

如果使用默认的构造器来构造乐观锁插件,使用包装类来更新,则会导致乐观锁失效。

因为此时et为null,第二段逻辑也无法走。

3. 结束

OK,至此Mybatis-plus中,乐观锁和逻辑删除的全部源码逻辑均已撸完。

如果有想看的源码,欢迎评论,我会出文带你一起撸!

  • 26
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

写代码的喵o

请作者吃包辣条可好

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值