Optimize the autoResultMap generation strategy of MyBatisPlus

preface

Using mybatis plus Field type processor , you can easily map data such as arrays and objects directly to entity classes with only one annotation.

@Data
@Accessors(chain = true)
@TableName(autoResultMap = true)
public class User {
    private Long id;

    ...


    /**
     * be careful!! Mapping annotation must be turned on
     *
     * @TableName(autoResultMap = true)
     *
     * Either of the following two types of processors can exist at the same time
     *
     * be careful!! Select the corresponding JSON processor, and the corresponding JSON parsing dependency package must also exist
     */
    @TableField(typeHandler = JacksonTypeHandler.class)
    // @TableField(typeHandler = FastjsonTypeHandler.class)
    private OtherInfo otherInfo;

}

This annotation corresponds to the XML that is written as

<result column="other_info" jdbcType="VARCHAR" property="otherInfo" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler" />

The implementation principle can refer to the source code of TableInfo initResultMapIfNeed method:

    /**
     * Automatically build the resultMap and inject it (if the conditions are met)
     */
    void initResultMapIfNeed() {
        if (autoInitResultMap && null == resultMap) {
            String id = currentNamespace + DOT + MYBATIS_PLUS + UNDERSCORE + entityType.getSimpleName();
            List<ResultMapping> resultMappings = new ArrayList<>();
            if (havePK()) {
                ResultMapping idMapping = new ResultMapping.Builder(configuration, keyProperty, keyColumn, keyType)
                    .flags(Collections.singletonList(ResultFlag.ID)).build();
                resultMappings.add(idMapping);
            }
            if (CollectionUtils.isNotEmpty(fieldList)) {
                fieldList.forEach(i -> resultMappings.add(i.getResultMapping(configuration)));
            }
            ResultMap resultMap = new ResultMap.Builder(configuration, id, entityType, resultMappings).build();
            configuration.addResultMap(resultMap);
            this.resultMap = id;
        }
    }

Existing problems

When autoResultMap=true is used, MP will do the following:

  • If the Java type of the field is not a basic type, you will be forced to set the typeHandler property. For example, this is not possible:
/**
 * IN query
*/
public static final String IN = "%s IN <foreach item=\"item\" collection=\"%s\" separator=\",\" open=\"(\" close=\")\" index=\"\">#{item}</foreach>";

@TableField(value="code", conditon=IN )
private Strinng[] codes;
  • If the fields are complex, such as including functions, with table alias restrictions, etc., the column attribute corresponding to the automatically generated resultMap will become very strange:
@TableField(value = "array_agg(distinct code)",jdbcType = JdbcType.VARCHAR, typeHandler = ArrayTypeHandler.class)
private String[] codes;

In the above code, the column attribute of the generated resultMap is rray_agg(distinct code, (one bit is intercepted before and after the value attribute). For specific logic, you can see the MP source code:

TableFieldInfo.java

    /**
  * Get ResultMapping
  *
  * @param configuration MybatisConfiguration
  * @return ResultMapping
  */
 ResultMapping getResultMapping(final Configuration configuration) {
     ResultMapping.Builder builder = new ResultMapping.Builder(configuration, property,
         StringUtils.getTargetColumn(column), propertyType);
     TypeHandlerRegistry registry = configuration.getTypeHandlerRegistry();
     if (jdbcType != null && jdbcType != JdbcType.UNDEFINED) {
         builder.jdbcType(jdbcType);
     }
     if (typeHandler != null && typeHandler != UnknownTypeHandler.class) {
         TypeHandler<?> typeHandler = registry.getMappingTypeHandler(this.typeHandler);
         if (typeHandler == null) {
             typeHandler = registry.getInstance(propertyType, this.typeHandler);
             // todo, this will affect registry register(typeHandler);
         }
         builder.typeHandler(typeHandler);
     }
     return builder.build();
 }

StringUtils.java

   /**
    * Verify that the string is a database field
    */
   private static final Pattern P_IS_COLUMN = Pattern.compile("^\\w\\S*[\\w\\d]*$");
  
    /**
    * Judge whether the string conforms to the naming of database fields
    *
    * @param str character string
    * @return Judgment result
    */
   public static boolean isNotColumnName(String str) {
       return !P_IS_COLUMN.matcher(str).matches();
   }

   /**
    * Get the real field name
    *
    * @param column Field name
    * @return Field name
    */
   public static String getTargetColumn(String column) {
       if (isNotColumnName(column)) {
           return column.substring(1, column.length() - 1);
       }
       return column;
   }

Yes, rather baffling and make complaints about the open source software in China, which is puzzled by logic and git's questions about developers' questions.

Optimization ideas

In fact, if a field has a typehandler attribute, a ResultMap must be created to handle the type mapping. There is no need to specify autoResultMap=true. However, with this attribute, MP will automatically generate a ResultMap, so that we can generate our own ResultMap without specifying this attribute.
It's not my style to directly modify the official source code. Fortunately, MP provides a custom injection mechanism. Before injecting methods into Mapper, we can generate ResultMap.

Optimization steps

  1. Create a subclass of the abstract injection method: better autoresultmap java
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import lombok.SneakyThrows;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.apache.ibatis.type.UnknownTypeHandler;

import java.lang.reflect.Field;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Optimized AutoResultMap generation strategy
 * As long as TypeHandler is specified in the field, ResultMap is automatically generated without specifying autoResultMap=true
 * And only generate ResultMapping for fields with TypeHandler set, not all fields
 */
public class BetterAutoResultMap extends AbstractMethod {

    /**
     * Force resetting the resultMap property of TableInfo
     */
    static Field ResultMapOfTableInfo;

    /**
     * Force reset of autoInitResultMap property of TableInfo
     */
    static Field AutoInitResultMapOfTableInfo;
    static {
        try {
            ResultMapOfTableInfo = TableInfo.class.getDeclaredField("resultMap");
            ResultMapOfTableInfo.setAccessible(true);
            AutoInitResultMapOfTableInfo = TableInfo.class.getDeclaredField("autoInitResultMap");
            AutoInitResultMapOfTableInfo.setAccessible(true);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    /**
     * Inject custom MappedStatement
     *
     * When the entity class does not specify autoResultMap and resultmap, you can use this method to automatically inject resultmap
     *
     * @param mapperClass mapper Interface
     * @param modelClass  mapper generic paradigm
     * @param tableInfo   Database table reflection information
     * @return MappedStatement
     */
    @SneakyThrows
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        if (!tableInfo.isAutoInitResultMap() && tableInfo.getResultMap() == null) {

            // The ResultMap is automatically generated whenever TypeHandler is specified in the field
            if (tableInfo.getFieldList().stream()
                    .filter(f -> f.getTypeHandler() != null && f.getTypeHandler() != UnknownTypeHandler.class)
                    .findAny().isPresent()) {

                // Generate ResultMap
                ResultMap resultMap = generatorResultMap(tableInfo);
                configuration.addResultMap(resultMap);

                // Set the ResultMap property to TableInfo
                ResultMapOfTableInfo.set(tableInfo, resultMap.getId());
                AutoInitResultMapOfTableInfo.set(tableInfo, true);
            }
        }
        return null;
    }

    /**
     * Build resultMap
     */
    ResultMap generatorResultMap(TableInfo tableInfo) {
        String resultMapId = tableInfo.getCurrentNamespace() + DOT + MYBATIS_PLUS + UNDERSCORE + tableInfo.getEntityType().getSimpleName();
        List<ResultMapping> resultMappings = tableInfo.getFieldList().stream().filter(f -> f.getTypeHandler() != null &&
                f.getTypeHandler() != UnknownTypeHandler.class).map(this::getResultMapping).collect(Collectors.toList());
        return new ResultMap.Builder(configuration, resultMapId, tableInfo.getEntityType(), resultMappings).build();
    }

    /**
     * Build resultMapping (only for the fields of typeHandler)
     *
     * @param tableFieldInfo
     * @return
     */
    ResultMapping getResultMapping(TableFieldInfo tableFieldInfo) {
        ResultMapping.Builder builder = new ResultMapping.Builder(configuration, tableFieldInfo.getProperty(),
                tableFieldInfo.getProperty(), tableFieldInfo.getPropertyType());
        TypeHandlerRegistry registry = configuration.getTypeHandlerRegistry();
        JdbcType jdbcType = tableFieldInfo.getJdbcType();
        if (jdbcType != null && jdbcType != JdbcType.UNDEFINED) {
            builder.jdbcType(jdbcType);
        }
        TypeHandler<?> typeHandlerMapped = registry.getMappingTypeHandler(tableFieldInfo.getTypeHandler());
        if (typeHandlerMapped == null) {
            typeHandlerMapped = registry.getInstance(tableFieldInfo.getPropertyType(), tableFieldInfo.getTypeHandler());
        }
        builder.typeHandler(typeHandlerMapped);
        return builder.build();
    }
}
  1. Create a custom class for SQL injector: sqlinjectorx java
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.caspe.base.support.mybatisplus.injector.methods.BetterAutoResultMap;
import com.caspe.base.support.mybatisplus.injector.methods.DeleteByMultiId;
import com.caspe.base.support.mybatisplus.injector.methods.SelectByMultiId;
import com.caspe.base.support.mybatisplus.injector.methods.UpdateByMultiId;

import java.util.ArrayList;
import java.util.List;

public class SqlInjectorX extends DefaultSqlInjector {

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methodList = new ArrayList<>();
        // Priority injection AutoResultMap generation method
        methodList.add(new BetterAutoResultMap());
        methodList.addAll(super.getMethodList(mapperClass));
        return methodList;
    }
}
  1. Add a custom Sql injector to the container
@Configuration
@Import({SqlInjectorX.class})
public class MybatisPlusAutoConfiguration {
}

verification

First, according to the optimization idea, the attribute autoResultMap=true must be deleted first.
Question 1 will no longer exist. Question 2 uses our customized ResultMap. The column attribute and property attribute will be consistent, and everything will become simple and natural.

Keywords: Java Mybatis

Added by BinaryStar on Sat, 22 Jan 2022 05:22:58 +0200