拓展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);
      }
    }
  }
}

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

相关推荐
evans在进步10 分钟前
Spring Boot 工程化核心详解:Parent、Starter、热部署、事务与多数据源
spring boot·后端·python
宫水三叶的刷题日记44 分钟前
馋猫外卖:AI 让字节式试错没有天花板
后端
SomeB1oody1 小时前
【RustyML入门】7.4. 按需裁剪与模块化集成
开发语言·后端·机器学习·rust·教程
罗超驿1 小时前
SpringBoot 快速入门
java·spring boot·后端
卷无止境2 小时前
除了开发api,FastAPI其实也可以配合jinja2模板写页面
后端·python·fastapi
newerp2 小时前
Redis 操作与缓存策略
后端·程序员·go
newerp2 小时前
GORM ORM 基础
后端·程序员·go
小岛前端2 小时前
AI Skills 已经封神,但新的问题却越来越严重!
前端·后端·github
拖孩2 小时前
一个人 + AI 做的小程序,上线 15 天赚了 10 块 5
前端·后端·微信小程序
newerp2 小时前
CRUD 操作与预处理语句
后端·程序员·go