Mybatis 主键回显 KeyGenerator原理

这篇文章研究下 Mybatis 配置主键回显相关功能。

本篇文章将以以下几个问题切入:

  1. Mybatis 如何 配置主键自增回显?
  2. JDBC 主键回显用法?
  3. 对于不支持自增主键数据库,Mybatis 有怎么解决这个问题?
  4. Mybatis 有哪几种主键生成方式?

例子

  1. JDBC 自增主键例子
    对于数据库支持自增主键的库,例如:Mysql:
            Connection conn = DriverManager.getConnection(url, "root", "123456");
            String[] columnNames = {"ids", "name"};
            PreparedStatement stmt = conn.prepareStatement(sql, columnNames);
            stmt.setString(1, "test jdbc3 ");
            stmt.executeUpdate();
            ResultSet rs = stmt.getGeneratedKeys();
            int id = 0;
            if (rs.next()) {
                id = rs.getInt(1);
                System.out.println("----------" + id);
            }

即当使用 prepareStatement 或者 Statement时候,可以通过getGeneratedKeys 获取 当条插入语句的自增而成的主键。
当然还有其他几种方式,原理是 数据库端返回一个LAST_INSERT_ID,这个跟auto_increment_id 强相关。
其他几种方式可以参考oracle文档:https://docs.oracle.com/cd/E17952_01/connector-j-en/connector-j-usagenotes-last-insert-id.html

Mybtis 对于数据库主键回显,主要分为两种,一种是数据库支持自增主键字段,另一种是数据库不支持方式即手动指定方式。

  1. 数据库支持主键回显,例如 Mysql,postgresql 使用 Jdbc3KeyGenerator 进行数据库回显:
    在 insert 中配置 useGeneratedKeys="true" keyColumn="id" keyProperty="id" ,即当插入或者批量插入后,成功后即可返回配置id:
    
        insert into department (name) values (#{department.name})
    
  1. 数据库不支持主键回显,例如 oracle,DB2。则使用 selectKey 先查出最大id,而后进行插入操作:
    
        
            SELECT if(max(id) is null,1,max(id)+10) as newId FROM department
        
        insert into department (id, name) values (#{id}, #{department.name})
    

分析

下面以 上面三个用法为基础,分析Mybatis 主键自增回显原理。
当 Mybatis 解析 xml节点是,读到 insert 有配置时,会判断是否 有配置 useGeneratedKeys,如果有则会使用 Jdbc3KeyGenerator 作为sql回显,否则会以 NoKeyGenerator 作为主键回显。
当执行插入数据操作时,执行以下逻辑:

  1. 首先会进入 MapperMethodexecute 方法,判断执行类型
  2. 当上一步判断是 INSERT 时,则会 进入 DefaultSqlSessioninsert 方法,随后进入 其 update 方法。
  3. 进入 Executorupdate 方法,配置完 ErrorContext 后,进入 doUpdate 方法:
  public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    clearLocalCache();
    return doUpdate(ms, parameter);
  }
  1. MybatisKeyGenerator 使用是在 PreparedStatementHandlerupdate 中:
  @Override
  public int update(Statement statement) throws SQLException {
    // 获取 preparedStatement
    PreparedStatement ps = (PreparedStatement) statement;
   // 执行 语句
    ps.execute();
    // 获取影响行
    int rows = ps.getUpdateCount();
    // 获取参数
    Object parameterObject = boundSql.getParameterObject();
    // 获取 KeyGenerator,自增则通过 jdbc获取,否则就通过selectKey 查询获取
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    // 回填 配置星到 参数中
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    return rows;
  }

Mybatis 对于 KeyGenerator 主要集中在上面的update 代码中,下面看看 三种 KeyGenerator 含义:

KeyGenerator

KeyGenerator 接口 中 只有 两个方法:

  void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);

  void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);

processBefore :主要用于 插入前操作,即获取和设置主键(不支持主键自增)
processAfter:用于插入后对主键进行回显到参数中

  1. NoKeyGenerator
    这个类 是对 KeyGenerator 的空实现,主要是不配置 generatorKey,所以 processAfterprocessBefore 事实上不需要进行任何操作
  2. Jdbc3KeyGenerator
    使用 JDBC 方式获取自增主键,其 processBefore 是空实现,只实现了 processAfter
  @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);
    }
  }

上面代码主要就是获取自增主键并且设值:

  private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties,
      Object parameter) throws SQLException {
    if (parameter instanceof ParamMap || parameter instanceof StrictMap) {
      // 多参数或者单参数使用 @Param  Multi-param or single param with @Param
      assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map) 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>) parameter));
    } else {
      // 没有 用 @Param注解的单参数  Single param without @Param
      assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
    }
  }

从上面看到,Mybatis 通过判断三种情况对 数据进行回填,从而使用不同回填,具体不在分析。

Jdbc mysql 连接驱动 中 getGeneratedKeys 获取有以下几个分析点:

  1. 为什么 支持 id 回显? 通过 返回的 LAST_INSERT_ID 返回。
  2. 为什么 批量插入可以 给所有对象返还id? 通过获取最后一个id,并且知道影响行数遍历设值
  3. 为什么批量 insert on duplicate key update 不能回填所有id? 因为insert on duplicate key update这种 方式 有多种情况,分别会返回0(存在但数值一致),1(插入),2(更新), 不是固定的1,所以不能通过遍历设值。

具体可以看看这篇文章,关于 Jdbc Mysql 驱动的 getGeneratedKeys : 深入分析Mybatis 使用useGeneratedKeys获取自增主键

  1. 如果数据库不支持 自增主键,Mybatis 提供一种 SelectKey 方式,即先查出,再将id填充进参数进行插入操作:
  @Override
  public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    if (executeBefore) {
    // 执行前
      processGeneratedKeys(executor, ms, parameter);
    }
  }

  @Override
  public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    if (!executeBefore) {
      // 执行后
      processGeneratedKeys(executor, ms, parameter);
    }
  }

// 查询 初结果并设定
  private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
    try {
      if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {
        String[] keyProperties = keyStatement.getKeyProperties();
        final Configuration configuration = ms.getConfiguration();
        // 获取一个 反射操作类
        final MetaObject metaParam = configuration.newMetaObject(parameter);
        if (keyProperties != null) {
          // Do not close keyExecutor.
          // The transaction will be closed by parent executor.
          // 获取一个Executor
          Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
          List values = keyExecutor.query(keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
          if (values.size() == 0) {
            throw new ExecutorException("SelectKey returned no data.");
          } else if (values.size() > 1) {
            throw new ExecutorException("SelectKey returned more than one value.");
          } else {
            MetaObject metaResult = configuration.newMetaObject(values.get(0));
            if (keyProperties.length == 1) {
            // 设置值
              if (metaResult.hasGetter(keyProperties[0])) {
                setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));
              } else {
                // no getter for the property - maybe just a single value object
                // so try that
                setValue(metaParam, keyProperties[0], values.get(0));
              }
            } else {
            // 设置多个值
              handleMultipleProperties(keyProperties, metaParam, metaResult);
            }
          }
        }
      }
    } catch (ExecutorException e) {
      throw e;
    } catch (Exception e) {
      throw new ExecutorException("Error selecting key or setting result to parameter object. Cause: " + e, e);
    }
  }

对于 SelectKeyGenerator 方式进行插入,则需要在 配置 SelectKey 时候指定 order, 如果没有指定,则默认为 BEFORE
order="BEFORE"BEFORE 则为在 插入前设值,即 上述代码中 executeBeforetrue

processGeneratedKeys 方法主要意思为 获取一个 Executor ,而后对 当前 MappedStatement 进行查询操作,最终返回到 List 的value中,并设值到 parameter 中。

以上,就是 MybatisKeyGenerator 主键回显原理。

觉得博主写的有用,不妨关注博主公众号: 六点A君。
哈哈哈,一起研究Mybatis:
在这里插入图片描述

你可能感兴趣的:(mybatis学习)