1. 程式人生 > 其它 >Mybatis原始碼解讀:executor包(主鍵自增功能)

Mybatis原始碼解讀:executor包(主鍵自增功能)

技術標籤:mybaits原始碼mybatis原始碼

歡迎關注本人公眾號:

​executor執行器包作為mybatis的核心將其他各個包凝聚在一起,會呼叫配置解析包解析出配置資訊,會依賴基礎包提供的基礎功能,最終executor包將所有的操作串連在一起,通過session包向外暴露出一套完整的服務。

1.主鍵自增功能

在進行資料插入操作時,經常需要一個自增生成的主鍵編號,這既能保證主鍵的唯一性, 又能保證主鍵的連續性。mybatis的executor包中的keygen子包提供主鍵自增功能。

1.主鍵自增的配置與生效

mybatis通過KeyGenerator介面提供主鍵自增功能,而KeyGenerator的實現類有Jdbc3KeyGenenrator、SelectKeyGenerator、NoKeyGenerator這3種。在實際使用時,這3種實現類中只能有一種實現類生效,而如果生效的是NoKeyGenerator則表明不具有任何主鍵自增功能。

啟用Jdbc3KeyGenerator,可以在配置檔案中增加設定

<settingname="useGeneratedKey" value="true"/>

或者直接在語句中配置

<insertid="insert"parameterType="User"useGeneratedKey="true"keyProperty="id"></insert>

啟用SelectKeyGenerator則需要在sql語句前配置

<insert id="insert" parameterType="User" >
<selectKeyresultType="java.lang.Integer" keyProperty="id" order="AFTER">selectLAST_INSERT_ID()</selectKey></insert>

注意:如果一條語句中同時設定了useGeneratedKey和selectKey,則selectKey生效。

在XMLStatementBuilder類中有用到主鍵功能被解析。

/** *       // 單條語句的解析器,解析類似: *       //   <select id="selectUser" resultType="User">
* // select * from `user` where id = #{id}*//</select> */public class XMLStatementBuilder extends BaseBuilder {/*解析select、insert、update、delete這四類節點 */ public void parseStatementNode() { // 讀取當前節點的id與databaseId String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); // 驗證id與databaseId是否匹配。MyBatis允許多資料庫配置,因此有些語句只對特定資料庫生效 if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } // 讀取節點名 String nodeName = context.getNode().getNodeName(); // 讀取和判斷語句型別 SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); boolean useCache = context.getBooleanAttribute("useCache", isSelect); boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); // 處理語句中的Include節點 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); // 引數型別 String parameterType = context.getStringAttribute("parameterType"); Class<?> parameterTypeClass = resolveClass(parameterType); // 語句型別 String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); // 處理SelectKey節點,在這裡會將KeyGenerator加入到Configuration.keyGenerators中 processSelectKeyNodes(id, parameterTypeClass, langDriver); // 此時,<selectKey> 和 <include> 節點均已被解析完畢並被刪除,開始進行SQL解析 KeyGenerator keyGenerator; String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); // 判斷是否已經有解析好的KeyGenerator if (configuration.hasKeyGenerator(keyStatementId)) { keyGenerator = configuration.getKeyGenerator(keyStatementId); } else { // 全域性或者本語句只要啟用自動key生成,則使用key生成 keyGenerator = context.getBooleanAttribute("useGeneratedKeys", configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } // 讀取各個配置屬性 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); Integer fetchSize = context.getIntAttribute("fetchSize"); Integer timeout = context.getIntAttribute("timeout"); String parameterMap = context.getStringAttribute("parameterMap"); String resultType = context.getStringAttribute("resultType"); Class<?> resultTypeClass = resolveClass(resultType); String resultMap = context.getStringAttribute("resultMap"); String resultSetType = context.getStringAttribute("resultSetType"); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); if (resultSetTypeEnum == null) { resultSetTypeEnum = configuration.getDefaultResultSetType(); } String keyProperty = context.getStringAttribute("keyProperty"); String keyColumn = context.getStringAttribute("keyColumn"); String resultSets = context.getStringAttribute("resultSets"); // 在MapperBuilderAssistant的幫助下建立MappedStatement物件,並寫入到Configuration中 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); } }

processSelectKeyNodes方法最終解析了selectKey節點從xml中刪除了,而解析出來的資訊則放入了configuration的keyGenerators中。之後如果沒有解析好的keyGenerator,則會更根據useGeneratedKeys判斷是否使用Jdbc3KeyGenerator.

最終KeyGenerator資訊會被儲存在整個Statement中。在Statement執行時直接呼叫KeyGenerator中的processBefore方法和processAfter方法即可,必然會有Jdbc3KeyGenerator、SelectKeyGenerator、NoKeyGenerator三者中的一個來實際執行者兩個方法。

2. Jdbc3KeyGenerator類

2.1. Jdbc3KeyGenerator類的功能

資料庫已經支援主鍵自增了,那麼Jdbc3KeyGenerator類存在的意義是什麼呢?

它存在的意義是提供自增主鍵的回寫功能,能夠將資料庫中產生的id值回寫給java物件本身。

2.2.Jdbc3KeyGenerator類的原理

Jdbc3KeyGenerator類的工作是在資料庫主鍵自增結束後,將自增處理的主鍵讀取處理並賦給java物件。這些工作都是在資料插入後進行的,即在processAfter方法中進行,而processBefore方法中不需要進行任何操作。

processAfter方法直接呼叫了processBatch方法,而processBatch方法主要工作就是呼叫Statement物件的getGeneratedKeys方法獲取資料庫自增生成的主鍵,然後將主鍵賦給實參以達到回寫的目的。

public class Jdbc3KeyGenerator implements KeyGenerator { @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;    }    // 呼叫Statement物件的getGeneratedKeys方法獲取自動生成的主鍵值    try (ResultSet rs = stmt.getGeneratedKeys()) {      // 獲取輸出結果的描述資訊      final ResultSetMetaData rsmd = rs.getMetaData();      final Configuration configuration = ms.getConfiguration();      if (rsmd.getColumnCount() < keyProperties.length) {        // 主鍵數目比結果的總欄位數目還多,則發生了錯誤。        // 但因為此處是獲取主鍵這樣的附屬操作,因此忽略錯誤,不影響主要工作      } 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);    }  }}

3.SelectKeyGenerator類

Jdbc3KeyGenerator類其實並沒有真正地生成自增主鍵,而只是把資料庫自增出的主鍵值回寫到了java物件中。所以面對不支援主鍵自增功能的資料庫時,Jdbc3KeyGenerator類將無能為力,而SelectKeyGenerator類可以真正地生成自增的主鍵。

public class SelectKeyGenerator implements KeyGenerator {  // 使用者生成主鍵的SQL語句的特有標誌,該標誌會追加在用於生成主鍵的SQL語句的id的後方  public static final String SELECT_KEY_SUFFIX = "!selectKey";  // 插入前執行還是插入後執行  private final boolean executeBefore;  // 使用者生成主鍵的SQL語句  private final MappedStatement keyStatement;  public SelectKeyGenerator(MappedStatement keyStatement, boolean executeBefore) {    this.executeBefore = executeBefore;    this.keyStatement = keyStatement;  }  /**   * 資料插入前進行的操作   * @param executor 執行器   * @param ms 對映語句物件   * @param stmt Statement物件   * @param parameter SQL語句實參物件   */  @Override  public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {    if (executeBefore) {      processGeneratedKeys(executor, ms, parameter);    }  }  /**   * 資料插入後進行的操作   * @param executor 執行器   * @param ms 對映語句物件   * @param stmt Statement物件   * @param parameter SQL語句實參物件   */  @Override  public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {    if (!executeBefore) {      processGeneratedKeys(executor, ms, parameter);    }  }  /**   * 執行一段SQL語句後獲取一個值,然後將該值賦給Java物件的自增屬性   *   * @param executor 執行器   * @param ms 插入操作的SQL語句(不是生成主鍵的SQL語句)   * @param parameter 插入操作的物件   */  private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {    try {      // keyStatement為生成主鍵的SQL語句;keyStatement.getKeyProperties()拿到的是要自增的屬性      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) {          // 為生成主鍵的SQL語句建立執行器keyExecutor。          // 原註釋:不要關閉keyExecutor,因為它會被父級的執行器關閉          Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);          // 執行SQL語句,得到主鍵值          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])) {                // 從metaResult中用getter得到主鍵值                setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));              } else {                // 可能返回的直接就是主鍵值本身                setValue(metaParam, keyProperties[0], values.get(0));              }            } else {              // 要把執行SQL得到的值賦給多個屬性              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類的功能就是先執行一段特定的sql語句獲取一個值,然後將該值賦給java物件的自增屬性。然而這個功能執行時機分為以下2種,這2種執行時機通過配置二選一。

1.在資料庫插入之前執行,執行完特定的sql語句並將值賦給java物件的自增屬性後,再將這個物件插入資料庫中。而這種操作又分2種情況:

1.1 如果資料庫沒有設定或者不支援主鍵自增,則完整的物件會被完整地插入資料庫中。

1.2 如果資料庫設定了主鍵自增,則剛才特定sql語句生成的自增屬性值會被資料庫自身的自增值覆蓋。這種情況下java物件的自增屬性值可能會和資料庫中的自增屬性值不一致,因此是錯誤的。這種情況下,建議使用Jdbc3KeyGenerator類的回寫功能。

2.在資料庫插入後執行,物件插入資料庫結束後,java物件的自增屬性被設定成特定sql語句的執行結果。這種操作也分2種情況:

2.1 如果資料庫不支援主鍵自增,則之前被插入資料庫中的物件的自增屬性是沒有被賦值的,而java物件的自增屬性卻被賦值了,這會導致不一致。這種操作是錯誤的。

2.2 如果資料庫設定了主鍵自增,則資料庫自增生成的值和sql語句執行產生的值可能不一樣。不過可以通過設定特定的sql語句來保證兩者一致,這其實和Jdbc3KeyGenerator類回寫功能類似。

可見SelectKeyGenerator類功能簡單又靈活,但因為執行時機、資料庫狀況等不同可能產生多種情況,需要注意。