拓展Mybatis-plus的insertBatchSomeColumn方法

背景

在使用Mybatis-plus进行项目开发的过程中由于有很多地方需要批量插入,而且普通的saveBatch是循环单条插入的,是对大数据量插入极不友好的一种插入方式,看到其官方文档中有提到一个insertBatchSomeColumn的方法,试用之后发现不太符合自己的预期效果,遂对其进行改造🔧

以下文档基于该版本进行改造,有其他问题可以查阅官方文档

xml 复制代码
<mybatis-plus.version>3.5.3.2</mybatis-plus.version>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>${mybatis-plus.version}</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-core</artifactId>
    <version>${mybatis-plus.version}</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-annotation</artifactId>
    <version>${mybatis-plus.version}</version>
</dependency>

基础配置

java 复制代码
@Configuration
public class MybatisPlusConfiguration {
    // 按需注入自己需要的配置信息,我这里需要对租户配置做一些自定义的处理
    @Bean
    public BatchInsertSqlInjector easySqlInjector(@Autowired TenantFilterProperties tenantFilterProperties) {
        return new BatchInsertSqlInjector(tenantFilterProperties);
    }
}

@RequiredArgsConstructor
public class BatchInsertSqlInjector extends DefaultSqlInjector {
    private final TenantFilterProperties tenantFilterProperties;

    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
        List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);
        // 批量插入
        methodList.add(new CustomInsertBatchSomeColumnMethod(i -> i.getFieldFill() != FieldFill.UPDATE, tenantFilterProperties));
        return methodList;
    }
}

重写自定义插入方法逻辑

java 复制代码
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlInjectionUtils;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;


public class CustomInsertBatchSomeColumnMethod extends AbstractMethod {

    private static final String INSERT_SQL = "INSERT INTO %s %s VALUES %s";

    private static final String CREATE_TIME_FIELD = "createTime";
    private static final String CREATE_BY_FIELD = "createBy";
    private static final String INSERT_BY_FIELD = "insertBy";

    /**
     * 字段筛选条件
     */
    @Setter
    @Accessors(chain = true)
    private Predicate<TableFieldInfo> predicate;

    private final TenantFilterProperties tenantFilterProperties;

    /**
     * 默认方法名
     *
     * @param predicate 字段筛选条件
     */
    public CustomInsertBatchSomeColumnMethod(Predicate<TableFieldInfo> predicate, TenantFilterProperties tenantFilterProperties) {
        // 此处定义的就是一个方法名,后续会在mapper上使用该方法
        super("customInsertBatchSomeColumn");
        this.predicate = predicate;
        this.tenantFilterProperties = tenantFilterProperties;
    }


    @Override
    // 这里的方法相当于写XML,这里只有启动的时候会扫描,并返回MappedStatement对象
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
        // 自定义业务逻辑需求
        boolean noTenant = (CollectionUtils.isEmpty(tenantFilterProperties.getTables()) || !tenantFilterProperties.getTables().contains(tableInfo.getTableName()));
        String keyProperty = null;
        String keyColumn = null;
        // 表包含主键处理逻辑,如果不包含主键当普通字段处理
        if (tableInfo.havePK()) {
            if (tableInfo.getIdType() == IdType.AUTO) {
                /* 自增主键生成方法,原先使用的Jdbc3KeyGenerator有点坑 */
                // keyGenerator = Jdbc3KeyGenerator.INSTANCE;
                keyGenerator = new CustomerKeyGenerator(tableInfo);
                keyProperty = tableInfo.getKeyProperty();
                // 去除转义符
                keyColumn = SqlInjectionUtils.removeEscapeCharacter(tableInfo.getKeyColumn());
            } else if (null != tableInfo.getKeySequence()) {
                keyGenerator = TableInfoHelper.genKeyGenerator(this.methodName, tableInfo, builderAssistant);
                keyProperty = tableInfo.getKeyProperty();
                keyColumn = tableInfo.getKeyColumn();
            }
        }
        // 生成默认的全字段的SQL
        String defaultSql = getDefaultSql(tableInfo);
        // 生成去除掉租户信息的SQL
        String noTenantSql = getNoTenantSql(tableInfo);
        // 这里比较关键,写好SQL模板,test里面的isolated是在外面通过方法参数传入的,noTenant的变量是在启动初始化就判断好了
        String finalSql = "<script>\n"
            + "         <choose>\n"
            + "            <when test=\"isolated and " + noTenant + "\">\n"
            +                   noTenantSql
            + "            </when>\n"
            + "            <otherwise>\n"
            +                   defaultSql
            + "            </otherwise>\n"
            + "        </choose>\n"
            + "     </script>";

        SqlSource sqlSource = super.createSqlSource(configuration, finalSql, modelClass);
        return this.addInsertMappedStatement(mapperClass, modelClass, methodName, sqlSource, keyGenerator, keyProperty, keyColumn);
    }

    /**
     * 获取默认的SQL
     * @param tableInfo
     * @return
     */
    private String getDefaultSql(TableInfo tableInfo) {
        List<TableFieldInfo> fieldList = tableInfo.getFieldList();
        return generateSql(tableInfo,fieldList);
    }

    /**
     * 获取没有租户信息的SQL
     * @param tableInfo
     * @return
     */
    private String getNoTenantSql(TableInfo tableInfo) {
        List<TableFieldInfo> fieldList = tableInfo.getFieldList().stream().filter(filed -> {
            return !filed.getProperty().equals(COMPANY_ID_FIELD) && !filed.getProperty().equals(CHAIN_ID_FIELD) && !filed.getProperty()
                .equals(ORG_ID_FIELD);
        }).collect(Collectors.toList());
        return generateSql(tableInfo,fieldList);
    }

    /**
     * 生成SQL
     * @param tableInfo
     * @param fieldList
     * @return
     */
    private String generateSql(TableInfo tableInfo,List<TableFieldInfo> fieldList) {
        String insertSqlColumn = tableInfo.getKeyInsertSqlColumn(true, null, false) +
            this.filterTableFieldInfo(fieldList, predicate, TableFieldInfo::getInsertSqlColumn, EMPTY);
        String columnScript = LEFT_BRACKET + insertSqlColumn.substring(0, insertSqlColumn.length() - 1) + RIGHT_BRACKET;
        String insertSqlProperty = tableInfo.getKeyInsertSqlProperty(true, ENTITY_DOT, false) +
            this.filterTableFieldInfo(fieldList, predicate, i -> i.getInsertSqlProperty(ENTITY_DOT), EMPTY);
        insertSqlProperty = LEFT_BRACKET + insertSqlProperty.substring(0, insertSqlProperty.length() - 1) + RIGHT_BRACKET;
        String valuesScript = SqlScriptUtils.convertForeach(insertSqlProperty, "entityList", null, ENTITY, COMMA);
        String finalValuesScript = "\n" + fillInsertField(fieldList, valuesScript) + "\n";
        return String.format(INSERT_SQL, tableInfo.getTableName(), columnScript, finalValuesScript);
    }


    /**
     * 填充创建所需要的字段信息,如创建时间、创建人等
     * @param fieldList
     * @param valuesScript
     * @return
     */
    private String fillInsertField(List<TableFieldInfo> fieldList,String valuesScript) {
        for (TableFieldInfo tableFieldInfo : fieldList) {
            // 字段名
            String property = tableFieldInfo.getProperty();
            // #{et.xxx} 对应foreach里的变量信息
            String valueExp = String.format("#{et.%s}",property);
            if (CREATE_TIME_FIELD.equals(property)) {
                valuesScript = valuesScript.replace(valueExp, "\n<choose>\n"
                    + "    <when test=\"createTime != null and et.createTime == null\">\n"
                    + "        #{createTime}\n"
                    + "    </when>\n"
                    + "    <otherwise>\n"
                    + "        #{et.createTime}\n"
                    + "    </otherwise>\n"
                    + "</choose>");
            }
            if (CREATE_BY_FIELD.equals(property)) {
                valuesScript = valuesScript.replace(valueExp, "\n<choose>\n"
                    + "    <when test=\"createBy != null and et.createBy == null\">\n"
                    + "        #{createBy}\n"
                    + "    </when>\n"
                    + "    <otherwise>\n"
                    + "        #{et.createBy}\n"
                    + "    </otherwise>\n"
                    + "</choose>");
            }
            if (INSERT_BY_FIELD.equals(property)) {
                valuesScript = valuesScript.replace(valueExp, "\n<choose>\n"
                    + "    <when test=\"insertBy != null and et.insertBy == null\">\n"
                    + "        #{insertBy}\n"
                    + "    </when>\n"
                    + "    <otherwise>\n"
                    + "        #{et.insertBy}\n"
                    + "    </otherwise>\n"
                    + "</choose>");
            }
        }
        return valuesScript;
    }

}

自定义主键生成方法

上面有段代码没有使用Mybatis-Plus默认的主键生成填充逻辑,后面改造使用自定义的主键生成回填逻辑,文末也会解读一下这个主键生成逻辑的源码的坑

java 复制代码
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.util.Collection;
import java.util.Iterator;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.ExecutorException;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.Reflector;
import org.apache.ibatis.session.Configuration;


/**
 * 自定义主键生成器
 * 1、继承Jdbc3KeyGenerator,才能使用主键生成策略
 * 2、仅用于insertBatchSomeColumn,其他方法需要特殊定制
 * 3、适用于能返回自增主键的数据库,不支持的数据库也无法使用这个方法
 */
public class CustomerKeyGenerator extends Jdbc3KeyGenerator {

    private TableInfo tableInfo;

    public CustomerKeyGenerator(TableInfo tableInfo) {
        this.tableInfo = tableInfo;
    }


    @Override
    public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
        // do nothing
    }

    @Override
    public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
        final String[] keyProperties = ms.getKeyProperties();
        if (keyProperties == null || keyProperties.length == 0) {
            return;
        }
        // 支持主键返回的数据库才能使用这个方法
        try (ResultSet rs = stmt.getGeneratedKeys()) {
            final ResultSetMetaData rsmd = rs.getMetaData();
            final Configuration configuration = ms.getConfiguration();
            if (rsmd.getColumnCount() < keyProperties.length) {
                // Error?
            } else {
                String keyProperty = tableInfo.getKeyProperty();
                if (StringUtils.isBlank(keyProperty)) {
                    return;
                }
                Class<?> keyPropertiesClass = tableInfo.getKeyType();
                // 通过反射获取到当前的数据对象
                MetaObject metaParam = configuration.newMetaObject(parameter);
                Object entityListObject = metaParam.getValue("entityList");
                Collection<?> entityList = (Collection<?>) entityListObject;
                Iterator<?> iterator = entityList.iterator();
                // 主键列表
                while (rs.next()) {
                    Object entity = iterator.next();
                    String columnName = rs.getMetaData().getColumnName(1);
                    if (!"GENERATED_KEY".equals(columnName)) {
                        continue;
                    }
                    // 根据主键列类型获取对应的主键值
                    Object value = rs.getObject(1, keyPropertiesClass);
                    Reflector reflector = tableInfo.getReflector();
                    // 使用mybatis自带的Reflector设置主键值
                    reflector.getSetInvoker(keyProperty).invoke(entity, new Object[]{value});
                }
            }
        } catch (Exception e) {
            throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
        }
    }
}

定义 CommonBaseMapper

java 复制代码
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.google.common.base.Strings;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Map;
import org.apache.ibatis.annotations.Param;

public interface CommonBaseMapper<T> extends BaseMapper<T> {

    /**
     * 批量插入 仅适用于mysql
     * 插入返回的ID会回显到list的对象中,创建信息(创建时间等)不会回显
     * @param entityList 实体列表
     * @return 影响行数
     */
    default Integer customInsertBatchSomeColumn(@Param("entityList") Collection<T> entityList) {
        // 自定义租户隔离逻辑
        boolean isolated = ServiceContextHolder.getServiceContext().isIsolated();
        // 填充创建时间-createTime
        LocalDateTime createTime = LocalDateTime.now();
        // 填充用户ID-createBy
        Integer userId = ServiceContextHolder.getServiceContext().getUserId();
        final Integer createBy = userId == null ? Tenant.FICTIONAL_USER_ID : userId;
        // 填充用户账号-insertBy
        String account = ServiceContextHolder.getServiceContext().getAccount();
        final String insertBy = !Strings.isNullOrEmpty(account) ? account : Tenant.SYSTEM_ACCOUNT;
        return customInsertBatchSomeColumn(isolated, createTime, createBy, insertBy, entityList);
    }

    /**
     * 批量插入 仅适用于mysql
     *
     * @param isolated   是否隔离租户
     * @param entityList 实体列表
     * @return 影响行数
     */
     // 无默认实现的 对应的就是 CustomInsertBatchSomeColumnMethod 方法实现的SQL模板配置
    Integer customInsertBatchSomeColumn(
        @Param("isolated") boolean isolated, 
        @Param("createTime") LocalDateTime createTime, 
        @Param("createBy") Integer createBy,
        @Param("insertBy") String insertBy, 
        @Param("entityList") Collection<T> entityList
    );

    

}

使用方法

java 复制代码
@Repository
// 继承自定义的mapper,就能使用默认的方法了
public interface UserMapper extends CommonBaseMapper<User> {


}

@Component
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public int batchInsert(List<OrgAutodialerServer> list) {
        return userMapper.customInsertBatchSomeColumn(list);
    }
}

遇到的问题

1. 参数信息无法传递

通过接口的default默认实现来实现参数的传递

bash 复制代码
像这里的SQL片段createTime这种字段,批量插入是不走mybatis的MetaObjectHandler 的默认填充逻辑的,
createTime就是从外面的方法带入的数据信息,et.createTime就是循环内的创建时间,如果有值则默认不填充数据
    <when test="createTime != null and et.createTime == null">\n"
        #{createTime}\n"
    </when>\n"
    <otherwise>\n"
        #{et.createTime}\n"
    </otherwise>

最终出来的SQL应该是这样

bash 复制代码
INSERT INTO user (name,age,sex,create_time,insert_by,create_by) VALUES 
<foreach collection="entityList" item="et" separator=",">
    (
        #{et.name},#{et.age},#{et.sex}
        <choose>
            <when test="createTime != null and et.createTime == null">
                #{createTime}
            </when>
            <otherwise>
                #{et.createTime}
            </otherwise>
        </choose>,
        <choose>
            <when test="insertBy != null and et.insertBy == null">
                #{insertBy}
            </when>
            <otherwise>
                #{et.insertBy}
            </otherwise>
        </choose>,
        <choose>
            <when test="createBy != null and et.createBy == null">
                #{createBy}
            </when>
            <otherwise>
                #{et.createBy}
            </otherwise>
        </choose>
    )
</foreach>     

2. 主键信息无法自动回填

上面有段代码没有使用Mybatis-Plus默认的主键生成填充逻辑,也顺便解读一下这个主键生成逻辑的源码的坑

// keyGenerator = Jdbc3KeyGenerator.INSTANCE; keyGenerator = new CustomerKeyGenerator(tableInfo); 以下源码解析可以重点关注带中文标记的地方

java 复制代码
public class Jdbc3KeyGenerator implements KeyGenerator {

  // 这个常量很重要,后续可以重点关注,param2 
  private static final String SECOND_GENERIC_PARAM_NAME = ParamNameResolver.GENERIC_NAME_PREFIX + "2";

  /**
   * A shared instance.
   *
   * @since 3.4.3
   */
  public static final Jdbc3KeyGenerator INSTANCE = new Jdbc3KeyGenerator();

  private static final String MSG_TOO_MANY_KEYS = "Too many keys are generated. There are only %d target objects. "
      + "You either specified a wrong 'keyProperty' or encountered a driver bug like #1523.";

  @Override
  public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    // do nothing
  }

  @Override
  public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    // 实际处理动作
    processBatch(ms, stmt, parameter);
  }

  public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
    final String[] keyProperties = ms.getKeyProperties();
    if (keyProperties == null || keyProperties.length == 0) {
      return;
    }
    try (ResultSet rs = stmt.getGeneratedKeys()) {
      final ResultSetMetaData rsmd = rs.getMetaData();
      final Configuration configuration = ms.getConfiguration();
      if (rsmd.getColumnCount() < keyProperties.length) {
        // Error?
      } else {
        // 分配回填主键的方法
        assignKeys(configuration, rs, rsmd, keyProperties, parameter);
      }
    } catch (Exception e) {
      throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
    }
  }

  @SuppressWarnings("unchecked")
  private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties,
      Object parameter) throws SQLException {
    if (parameter instanceof ParamMap || parameter instanceof StrictMap) {
      // Multi-param or single param with @Param
      assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map<String, ?>) parameter);
    } else if (parameter instanceof ArrayList && !((ArrayList<?>) parameter).isEmpty()
        && ((ArrayList<?>) parameter).get(0) instanceof ParamMap) {
      // Multi-param or single param with @Param in batch operation
      assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, (ArrayList<ParamMap<?>>) parameter);
    } else {
      // Single param without @Param
      assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
    }
  }

  private void assignKeysToParam(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
      String[] keyProperties, Object parameter) throws SQLException {
    Collection<?> params = collectionize(parameter);
    if (params.isEmpty()) {
      return;
    }
    List<KeyAssigner> assignerList = new ArrayList<>();
    for (int i = 0; i < keyProperties.length; i++) {
      assignerList.add(new KeyAssigner(configuration, rsmd, i + 1, null, keyProperties[i]));
    }
    Iterator<?> iterator = params.iterator();
    while (rs.next()) {
      if (!iterator.hasNext()) {
        throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, params.size()));
      }
      Object param = iterator.next();
      assignerList.forEach(x -> x.assign(rs, param));
    }
  }

  private void assignKeysToParamMapList(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
      String[] keyProperties, ArrayList<ParamMap<?>> paramMapList) throws SQLException {
    Iterator<ParamMap<?>> iterator = paramMapList.iterator();
    List<KeyAssigner> assignerList = new ArrayList<>();
    long counter = 0;
    while (rs.next()) {
      if (!iterator.hasNext()) {
        throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, counter));
      }
      ParamMap<?> paramMap = iterator.next();
      if (assignerList.isEmpty()) {
        for (int i = 0; i < keyProperties.length; i++) {
          assignerList
              .add(getAssignerForParamMap(configuration, rsmd, i + 1, paramMap, keyProperties[i], keyProperties, false)
                  .getValue());
        }
      }
      assignerList.forEach(x -> x.assign(rs, paramMap));
      counter++;
    }
  }

  private void assignKeysToParamMap(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
      String[] keyProperties, Map<String, ?> paramMap) throws SQLException {
    if (paramMap.isEmpty()) {
      return;
    }
    Map<String, Entry<Iterator<?>, List<KeyAssigner>>> assignerMap = new HashMap<>();
    for (int i = 0; i < keyProperties.length; i++) {
      Entry<String, KeyAssigner> entry = getAssignerForParamMap(configuration, rsmd, i + 1, paramMap, keyProperties[i],
          keyProperties, true);
      Entry<Iterator<?>, List<KeyAssigner>> iteratorPair = MapUtil.computeIfAbsent(assignerMap, entry.getKey(),
          k -> MapUtil.entry(collectionize(paramMap.get(k)).iterator(), new ArrayList<>()));
      iteratorPair.getValue().add(entry.getValue());
    }
    long counter = 0;
    while (rs.next()) {
      for (Entry<Iterator<?>, List<KeyAssigner>> pair : assignerMap.values()) {
        if (!pair.getKey().hasNext()) {
          throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, counter));
        }
        Object param = pair.getKey().next();
        pair.getValue().forEach(x -> x.assign(rs, param));
      }
      counter++;
    }
  }

// 重点关注这个方法
  private Entry<String, KeyAssigner> getAssignerForParamMap(Configuration config, ResultSetMetaData rsmd,
      int columnPosition, Map<String, ?> paramMap, String keyProperty, String[] keyProperties, boolean omitParamName) {
    Set<String> keySet = paramMap.keySet();
    // A caveat : if the only parameter has {@code @Param("param2")} on it,
    // it must be referenced with param name e.g. 'param2.x'.
    // 这里就是最顶上的常量所代表的含义,如果传入的参数不止有entityList的话(大于等于1个参数),这里的返回就会为false,不会回填ID,关键就是这里
    boolean singleParam = !keySet.contains(SECOND_GENERIC_PARAM_NAME);
    int firstDot = keyProperty.indexOf('.');
    if (firstDot == -1) {
      if (singleParam) {
        return getAssignerForSingleParam(config, rsmd, columnPosition, paramMap, keyProperty, omitParamName);
      }
      throw new ExecutorException("Could not determine which parameter to assign generated keys to. "
          + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
          + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
          + keySet);
    }
    String paramName = keyProperty.substring(0, firstDot);
    if (keySet.contains(paramName)) {
      String argParamName = omitParamName ? null : paramName;
      String argKeyProperty = keyProperty.substring(firstDot + 1);
      return MapUtil.entry(paramName, new KeyAssigner(config, rsmd, columnPosition, argParamName, argKeyProperty));
    }
    if (singleParam) {
      return getAssignerForSingleParam(config, rsmd, columnPosition, paramMap, keyProperty, omitParamName);
    } else {
      throw new ExecutorException("Could not find parameter '" + paramName + "'. "
          + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
          + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
          + keySet);
    }
  }

  private Entry<String, KeyAssigner> getAssignerForSingleParam(Configuration config, ResultSetMetaData rsmd,
      int columnPosition, Map<String, ?> paramMap, String keyProperty, boolean omitParamName) {
    // Assume 'keyProperty' to be a property of the single param.
    String singleParamName = nameOfSingleParam(paramMap);
    String argParamName = omitParamName ? null : singleParamName;
    return MapUtil.entry(singleParamName, new KeyAssigner(config, rsmd, columnPosition, argParamName, keyProperty));
  }

  private static String nameOfSingleParam(Map<String, ?> paramMap) {
    // There is virtually one parameter, so any key works.
    return paramMap.keySet().iterator().next();
  }

  private static Collection<?> collectionize(Object param) {
    if (param instanceof Collection) {
      return (Collection<?>) param;
    }
    if (param instanceof Object[]) {
      return Arrays.asList((Object[]) param);
    } else {
      return Arrays.asList(param);
    }
  }

  private static class KeyAssigner {
    private final Configuration configuration;
    private final ResultSetMetaData rsmd;
    private final TypeHandlerRegistry typeHandlerRegistry;
    private final int columnPosition;
    private final String paramName;
    private final String propertyName;
    private TypeHandler<?> typeHandler;

    protected KeyAssigner(Configuration configuration, ResultSetMetaData rsmd, int columnPosition, String paramName,
        String propertyName) {
      this.configuration = configuration;
      this.rsmd = rsmd;
      this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
      this.columnPosition = columnPosition;
      this.paramName = paramName;
      this.propertyName = propertyName;
    }
    
    // 实际填充数据的方法逻辑
    protected void assign(ResultSet rs, Object param) {
      if (paramName != null) {
        // If paramName is set, param is ParamMap
        param = ((ParamMap<?>) param).get(paramName);
      }
      MetaObject metaParam = configuration.newMetaObject(param);
      try {
        if (typeHandler == null) {
          if (!metaParam.hasSetter(propertyName)) {
            throw new ExecutorException("No setter found for the keyProperty '" + propertyName + "' in '"
                + metaParam.getOriginalObject().getClass().getName() + "'.");
          }
          Class<?> propertyType = metaParam.getSetterType(propertyName);
          typeHandler = typeHandlerRegistry.getTypeHandler(propertyType,
              JdbcType.forCode(rsmd.getColumnType(columnPosition)));
        }
        if (typeHandler == null) {
          // Error?
        } else {
          Object value = typeHandler.getResult(rs, columnPosition);
          metaParam.setValue(propertyName, value);
        }
      } catch (SQLException e) {
        throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e,
            e);
      }
    }
  }
}

欢迎大家-点赞+评论+关注,如有不对还望指教

相关推荐
程序员大金6 分钟前
基于SpringBoot+Vue+MySQL的在线学习交流平台
java·vue.js·spring boot·后端·学习·mysql·intellij-idea
qq_25183645713 分钟前
基于SpringBoot vue 医院病房信息管理系统设计与实现
vue.js·spring boot·后端
qq_2518364571 小时前
基于springboot vue3 在线考试系统设计与实现 源码数据库 文档
数据库·spring boot·后端
2401_858120531 小时前
古典舞在线交流平台:SpringBoot设计与实现详解
java·spring boot·后端
赐你岁月如歌1 小时前
如何使用ssm实现基于web的网站的设计与实现+vue
java·后端·ssm
闲宇非鱼2 小时前
微服务到底是技术问题还是管理问题?
java·后端·微服务
潘多编程3 小时前
Spring Boot微服务架构设计与实战
spring boot·后端·微服务
2402_857589363 小时前
新闻推荐系统:Spring Boot框架详解
java·spring boot·后端
2401_857622663 小时前
新闻推荐系统:Spring Boot的可扩展性
java·spring boot·后端
江湖十年5 小时前
在 Go 中如何优雅的处理错误
后端·go