MyBatisPlus update field is the correct posture of null and the source code analysis of conditional field parsing in lambda mode

Article directory

    • @[toc]
  • 1. Problem
  • 2. Reason
  • 3. Solution
    • 3.1 The wrong way
      • Method 1: Configure a global field policy
      • Method 2: Add field policy annotations to entities
    • 3.2 Correct posture
      • Method 1: Use LambdaUpdateWrapper (recommended)
      • Method 2: Use UpdateWrapper
      • way three
  • Summarize

1. Question

Because the updateById(Entity) interface api of MyBatisPlus is used in the project to switch according to different operations clicked by the user, it is necessary to update the field of the table to null according to the primary key id in the table. When using this interface api, set the entity field according to the primary key as It is also a strange problem that the null update does not take effect.

2. Reason

The reason is that MyBatisPlus’s field update strategy is to blame. MyBatisPlus has the following strategies:

The most commonly used and main ones are the first three

public enum FieldStrategy {<!-- -->
    IGNORED, //ignore
    NOT_NULL, //Non-NULL, default policy, do not ignore ""
    NOT_EMPTY, //Not empty, "" will be ignored, NULL will be ignored
    DEFAULT, // default
    NEVER; //none

    private FieldStrategy() {<!-- -->
    }
}

Because the default strategy is NOT_NULL, the following two APIs will cause the following two APIs to update the entity fields to be null and invalidate them in the table. This strategy will only update the values of non-null fields in the entity, and the null fields will be ignored.

this.updateById(entity);
this. update(entity, updateWrapper);

3. Solution

3.1 Error method

Method 1: Configure global field strategy

mybatis-plus:
  global-config:
  #Field policy 0:"ignore judgment",1:"non-NULL judgment",2:"non-null judgment"
    field-strategy: 0

The overall situation has not been configured and debugged. It is problematic to only try the following method 2. This method will have a wider impact on the global situation, so use it with caution

Method 2: Add field policy annotations to entities

@TableField(updateStrategy = FieldStrategy.IGNORED)

Method 2 Added annotations to the entity fields, and the debugging found that it will take effect. After running once, the fields in the database table are set to null, and then after running several times, the modified entity fields are set to have values. Update, It is found that the value will not be set and will not be updated, and the fields in the table are still null. This is also a big pit, so if the posture is wrong, it will be a pit.

3.2 Correct posture

Method 1: Use LambdaUpdateWrapper (recommended)

LambdaUpdateWrapper<WhiteListManagementEntity>
    lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
//condition
lambdaUpdateWrapper.eq(WhiteListManagementEntity::getId,
                       updateWhiteListManagementEntity. getId());
// set field value
lambdaUpdateWrapper.set(WhiteListManagementEntity::getIsNight, 0);
lambdaUpdateWrapper.set(WhiteListManagementEntity::getNightStart, null);
lambdaUpdateWrapper.set(WhiteListManagementEntity::getNightEnd, null);
//renew
this. update(lambdaUpdateWrapper);

This method is concise and ingenious. The ingenious point is that the Lambda syntax feature of JDK8 is used, and SerializedLambda is used to parse entity fields and then stitch them into query conditions.

This abstract AbstractWrapper’s AbstractLambdaWrapper subclass generic parameter SFunction, so you can pass the Lambda interface expression

The relationship diagram of the AbstractWrapper class is as follows:

image

The relationship diagram of the AbstractLambdaWrapper class is as follows:

image

SFunction interface source code is as follows:

package com.baomidou.mybatisplus.core.toolkit.support;

import java.io.Serializable;
import java.util.function.Function;

/**
 * Function that supports serialization
 *
 * @author miemie
 * @since 2018-05-12
 */
@FunctionalInterface
public interface SFunction<T, R> extends Function<T, R>, Serializable {<!-- -->
}

The source code of the set method of LambdaUpdateWrapper is as follows:

 @Override
  public LambdaUpdateWrapper<T> set(boolean condition, SFunction<T, ?> column, Object val) {<!-- -->
      if (condition) {<!-- -->
         sqlSet.add(String.format("%s=%s", columnToString(column), formatSql("{0}", val)));
      }
      return typedThis;
  }
 @Override
    protected String columnToString(SFunction<T, ?> column) {<!-- -->
        return columnToString(column, true);
    }

    protected String columnToString(SFunction<T, ?> column, boolean onlyColumn) {<!-- -->
        return getColumn(LambdaUtils. resolve(column), onlyColumn);
    }

    /**
     * Obtain the column information corresponding to SerializedLambda, and infer the entity class from the lambda expression
     * <p>
     * If the column information cannot be obtained, the conditional assembly will fail this time
     *
     * @param lambda lambda expression
     * @param onlyColumn if yes, result: "name", if no: "name" as "name"
     * @return column
     * @throws com.baomidou.mybatisplus.core.exceptions.MybatisPlusException Throws an exception when column information cannot be obtained
     * @see SerializedLambda#getImplClass()
     * @see SerializedLambda#getImplMethodName()
     */
    private String getColumn(SerializedLambda lambda, boolean onlyColumn) throws MybatisPlusException {<!-- -->
        //Use reflection to parse the method starting with is/get of the SerializedLambda object to get the name of the attribute field
        String fieldName = PropertyNamer. methodToProperty(lambda. getImplMethodName());
        Class<?> aClass = lambda. getInstantiatedType();
        if (!initColumnMap) {<!-- -->
            columnMap = LambdaUtils. getColumnMap(aClass);
            initColumnMap = true;
        }
        Assert.notNull(columnMap, "can not find lambda cache for this entity [%s]", aClass.getName());
        ColumnCache columnCache = columnMap.get(LambdaUtils.formatKey(fieldName));
        Assert.notNull(columnCache, "can not find lambda cache for this property [%s] of entity [%s]",
            fieldName, aClass. getName());
        return onlyColumn ? columnCache.getColumn() : columnCache.getColumnSelect();
    }

PropertyNamer’s methodToProperty method source code is as follows:

 public static String methodToProperty(String name) {<!-- -->
        if (name. startsWith("is")) {<!-- -->
            name = name. substring(2);
        } else {<!-- -->
            if (!name.startsWith("get") & amp; & amp; !name.startsWith("set")) {<!-- -->
                throw new ReflectionException("Error parsing property name '" + name + "'. Didn't start with 'is', 'get' or 'set'.");
            }

            name = name. substring(3);
        }

        if (name.length() == 1 || name.length() > 1 & amp; & amp; !Character.isUpperCase(name.charAt(1))) {<!-- -->
            name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1);
        }

        return name;
    }

The parsing of Lambda expressions is realized through the LambdaUtils.resolve(column) method of the LambdaUtils tool class. The source code is as follows:

package com.baomidou.mybatisplus.core.toolkit;

import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.support.ColumnCache;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

import static java.util.Locale.ENGLISH;

/**
 * Lambda parsing tool class
 *
 * @author HCL, MieMie
 * @since 2018-05-10
 */
public final class LambdaUtils {<!-- -->

    /**
     * field mapping
     */
    private static final Map<String, Map<String, ColumnCache>> COLUMN_CACHE_MAP = new ConcurrentHashMap<>();

    /**
     * SerializedLambda deserialization cache
     */
    private static final Map<String, WeakReference<SerializedLambda>> FUNC_CACHE = new ConcurrentHashMap<>();

    /**
     * Parse the lambda expression, this method just calls the method in {@link SerializedLambda#resolve(SFunction)}, and adds a cache on this basis.
     * This cache may be cleared at any arbitrary time
     *
     * @param func the lambda object that needs to be parsed
     * @param <T> type, the target type of the called Function object
     * @return returns the parsed result
     * @see SerializedLambda#resolve(SFunction)
     */
    public static <T> SerializedLambda resolve(SFunction<T, ?> func) {<!-- -->
        Class<?> clazz = func. getClass();
        String canonicalName = clazz. getCanonicalName();
        return Optional.ofNullable(FUNC_CACHE.get(canonicalName))
            .map(WeakReference::get)
            .orElseGet(() -> {<!-- -->
                SerializedLambda lambda = SerializedLambda. resolve(func);
                FUNC_CACHE.put(canonicalName, new WeakReference<>(lambda));
                return lambda;
            });
    }

    /**
     * Format key Change the incoming key to uppercase format
     *
     * <pre>
     * Assert.assertEquals("USERID", formatKey("userId"))
     * 

*
* @param key key
* @return capitalized key
*/
public static String formatKey(String key) {
return key.toUpperCase(ENGLISH);
}

/**
* Add the incoming table information to the cache
*
* @param tableInfo table information
*/
public static void installCache(TableInfo tableInfo) {
COLUMN_CACHE_MAP.put(tableInfo.getEntityType().getName(), createColumnCacheMap(tableInfo));
}

/**
* Cache entity field MAP information
*
* @param info table information
* @return cache map
*/
private static Map createColumnCacheMap(TableInfo info) {
Map map = new HashMap<>();

String kp = info. getKeyProperty();
if (StringUtils.isNotBlank(kp)) {
map.put(formatKey(kp), new ColumnCache(info.getKeyColumn(), info.getKeySqlSelect()));
}

info.getFieldList().forEach(i ->
map.put(formatKey(i.getProperty()), new ColumnCache(i.getColumn(), i.getSqlSelect()))
);
return map;
}

/**
* Get the corresponding field MAP of the entity
*
* @param clazz entity class
* @return cache map
*/
public static Map getColumnMap(Class clazz) {
return COLUMN_CACHE_MAP.computeIfAbsent(clazz.getName(), key -> {
TableInfo info = TableInfoHelper. getTableInfo(clazz);
return info == null ? null : createColumnCacheMap(info);
});
}

}

Through the analysis of the LambdaUtils.resolve(column) method, you can get the analytical object of SerializedLambda, but not many SerializedLambda classes are copied from the JDK8 source code to the project by the author of MyBatisPlus. The location of SerializedLambda is as follows:

Image

Analytic Lambda expression is to call the resolve method of this class:

SerializedLambda lambda = SerializedLambda.*resolve*(func);

installCache in LambdaUtils initializes TableInfo and joins the cache logic entry. It is a bridge connected by @TableName entity parsing and condition matching through SerializedLambda. As for the parsing and initialization process of TableInfo, use idea to reverse the vines to find the upper layer of this method, layer by layer You can know where the real entrance is by looking for the layer. The current entrance is to analyze the mapping of MyBatisPlus when mybatis builds the Session. There are mapping analysis of XML type and Mapper interface annotation type. MybatisSqlSessionFactoryBuilder.build() method:

package com.baomidou.mybatisplus.core;

import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.injector.SqlRunnerInjector;
import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import org.apache.ibatis.exceptions.ExceptionFactory;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.Properties;

/**
 * Rewrite SqlSessionFactoryBuilder
 *
 * @author nieqiurong 2019/2/23.
 */
public class MybatisSqlSessionFactoryBuilder extends SqlSessionFactoryBuilder {<!-- -->

    @SuppressWarnings("Duplicates")
    @Override
    public SqlSessionFactory build(Reader reader, String environment, Properties properties) {<!-- -->
        try {<!-- -->
            //TODO replace it with MybatisXMLConfigBuilder instead of XMLConfigBuilder
            MybatisXMLConfigBuilder parser = new MybatisXMLConfigBuilder(reader, environment, properties);
            return build(parser. parse());
        } catch (Exception e) {<!-- -->
            throw ExceptionFactory. wrapException("Error building SqlSession.", e);
        } finally {<!-- -->
            ErrorContext.instance().reset();
            try {<!-- -->
                reader. close();
            } catch (IOException e) {<!-- -->
                // Intentionally ignore. Prefer previous error.
            }
        }
    }

    @SuppressWarnings("Duplicates")
    @Override
    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {<!-- -->
        try {<!-- -->
            //TODO replace it with MybatisXMLConfigBuilder instead of XMLConfigBuilder
            MybatisXMLConfigBuilder parser = new MybatisXMLConfigBuilder(inputStream, environment, properties);
            return build(parser. parse());
        } catch (Exception e) {<!-- -->
            throw ExceptionFactory. wrapException("Error building SqlSession.", e);
        } finally {<!-- -->
            ErrorContext.instance().reset();
            try {<!-- -->
                inputStream. close();
            } catch (IOException e) {<!-- -->
                // Intentionally ignore. Prefer previous error.
            }
        }
    }

    // TODO use your own logic, inject required components
    @Override
    public SqlSessionFactory build(Configuration config) {<!-- -->
        MybatisConfiguration configuration = (MybatisConfiguration) config;
        GlobalConfig globalConfig = GlobalConfigUtils.getGlobalConfig(configuration);
        final IdentifierGenerator identifierGenerator;
        if (globalConfig. getIdentifierGenerator() == null) {<!-- -->
            if (null != globalConfig.getWorkerId() & amp; & amp; null != globalConfig.getDatacenterId()) {<!-- -->
                identifierGenerator = new DefaultIdentifierGenerator(globalConfig.getWorkerId(), globalConfig.getDatacenterId());
            } else {<!-- -->
                identifierGenerator = new DefaultIdentifierGenerator();
            }
            globalConfig.setIdentifierGenerator(identifierGenerator);
        } else {<!-- -->
            identifierGenerator = globalConfig. getIdentifierGenerator();
        }
        //TODO here is just for compatibility, it doesn't matter much, the method mark is outdated.
        IdWorker.setIdentifierGenerator(identifierGenerator);

        if (globalConfig.isEnableSqlRunner()) {<!-- -->
            new SqlRunnerInjector().inject(configuration);
        }

        SqlSessionFactory sqlSessionFactory = super.build(configuration);

        // cache sqlSessionFactory
        globalConfig.setSqlSessionFactory(sqlSessionFactory);

        return sqlSessionFactory;
    }
}

According to the analysis parameter type, according to the call of mybatis, in short, a method will be called, but mybaisPlus rewrites and expands the construction logic of this SqlSession, which is generally in the process of mybatis and The implementation has been enhanced and simplified to simplify operations and improve efficiency. This is the most valuable place loved and trusted by developers

Finally, there will be a piece of code logic in the Mapper interface registration parsing parse() interface in the MybatisMapperAnnotationBuilder class:

@Override
    public void parse() {<!-- -->
        String resource = type. toString();
        if (!configuration.isResourceLoaded(resource)) {<!-- -->
             ,,,,,,,,,,,
            // TODO injects CURD dynamic SQL, put it at the end, because someone may rewrite sql with annotations
            if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {<!-- -->
                GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
            }
        }
        parsePendingMethods();
    }

Then go to the inspectInject() method of the abstract sql injector AbstractSqlInjector:

package com.baomidou.mybatisplus.core.injector;

import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.ArrayUtils;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.List;
import java.util.Set;

/**
 * SQL auto injector
 *
 * @author hubin
 * @since 2018-04-07
 */
public abstract class AbstractSqlInjector implements ISqlInjector {<!-- -->

    private static final Log logger = LogFactory. getLog(AbstractSqlInjector. class);

    @Override
    public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {<!-- -->
        Class<?> modelClass = extractModelClass(mapperClass);
        if (modelClass != null) {<!-- -->
            String className = mapperClass.toString();
            Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
            if (!mapperRegistryCache. contains(className)) {<!-- -->
                List<AbstractMethod> methodList = this. getMethodList(mapperClass);
                if (CollectionUtils. isNotEmpty(methodList)) {<!-- -->
                    //Here is the logic of adding the initial tableInfo information to the cache Map
                    TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
                    // loop injection custom method
                    methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
                } else {<!-- -->
                    logger.debug(mapperClass.toString() + ", No effective injection method was found.");
                }
                mapperRegistryCache.add(className);
            }
        }
    }

    /**
     * <p>
     * Get the injected method
     *</p>
     *
     * @param mapperClass current mapper
     * @return injected method set
     * @since 3.1.2 add mapperClass
     */
    public abstract List<AbstractMethod> getMethodList(Class<?> mapperClass);

    /**
     * Extract the generic model, please put the generic T first when there are multiple generic types
     *
     * @param mapperClass mapper interface
     * @return mapper generic
     */
    protected Class<?> extractModelClass(Class<?> mapperClass) {<!-- -->
        Type[] types = mapperClass. getGenericInterfaces();
        ParameterizedType target = null;
        for (Type type : types) {<!-- -->
            if (type instanceof ParameterizedType) {<!-- -->
                Type[] typeArray = ((ParameterizedType) type). getActualTypeArguments();
                if (ArrayUtils. isNotEmpty(typeArray)) {<!-- -->
                    for (Type t : typeArray) {<!-- -->
                        if (t instanceof TypeVariable || t instanceof WildcardType) {<!-- -->
                            break;
                        } else {<!-- -->
                            target = (ParameterizedType) type;
                            break;
                        }
                    }
                }
                break;
            }
        }
        return target == null ? null : (Class<?>) target. getActualTypeArguments()[0];
    }
}

Method 2: Use UpdateWrapper

The difference between UpdateWrapper and LambdaUpdateWrapper is that the writing method is different. The first parameter needs to write the field name of the table (xxx_xxx)

UpdateWrapper<WhiteListManagementEntity>
      updateWrapper = new UpdateWrapper<WhiteListManagementEntity>();
//condition
updateWrapper.eq("id", updateWhiteListManagementEntity.getId());
// set field value
updateWrapper.set("is_night", 0);
updateWrapper.set("night_start", null);
updateWrapper.set("night_end", null);
//renew
this. update(updateWrapper);

Method 3

Add the following annotations to the entity

@TableField(fill = FieldFill.UPDATE)
//Then call the following method
this. updateById(entity);
this. update(entity, updateWrapper);

This method has not been personally tested. Only method 1 and method 2 are effective. I have added this annotation to the entity and it is also valid.

Summary

Many persistence layer ORM frameworks use the characteristics of JDK8’s Lambda expression and this class SerializedLambda to parse the Lambda expression interface object information, and then use Java reflection or bytecode technology to further process and analyze the Clazz file. For example: the EasyEs open source framework uses the idea of ORM to make operating the ES database simple and efficient. The analysis idea of the ORM layer of this framework is also realized by referring to the idea of MyBatisPlus. Interested friends can go to the source code. That’s all for my sharing, I hope it can help you, if you like it, please follow it three times with one click, ok!