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 id="insertWithGenertoorKey" useGeneratedKeys="true"  keyColumn="id" keyProperty="id">
        insert into department (name) values (#{department.name})
    </insert>
  1. 數據庫不支持主鍵回顯,例如 oracle,DB2。則使用 selectKey 先查出最大id,而後進行插入操作:
    <insert id="insertWithSelectKeyGenertoorKey">
        <selectKey keyProperty="id" resultType="long" order="BEFORE">
            SELECT if(max(id) is null,1,max(id)+10) as newId FROM department
        </selectKey>
        insert into department (id, name) values (#{id}, #{department.name})
    </insert>

分析

下面以 上面三個用法爲基礎,分析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<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 {
      // 沒有 用 @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<Object> 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:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章