Mybatis源碼學習(24)-KeyGenerator、SelectKeyGenerator、Jdbc3KeyGenerator

一、簡介

  在Mybatis中,insert語句執行時,可以返回自動產生的主鍵,這裏便是使用KeyGenerator來完成的。本篇內容就來分析Mybatis中的主鍵生成策略是怎麼樣起作用的。
  首先,不同的數據庫產品對應的主鍵生成策略不一樣,主要分爲兩類:一類是在執行insert 語句之前必須明確指定主鍵的,比如: Oracle 、DB2 等數據庫;一類是可以不指定主鍵,而在插入過程中由數據庫自動生成自增主鍵,比如:MYSQL, Postgresql等數據庫。
  在KeyGenerator中,針對不同的數據庫產品,提供了不同的方法進行處理。其中,processBefore()方法,在執行insert之前執行,一般用於Oracle 、DB2 等數據庫;processAfter()方法,在執行insert之後執行,一般用於MYSQL, Postgresql等數據庫。(注:後續 分析源碼發現,其實兩個方法只是明確是在insert執行前或執行後執行,和數據庫沒有關係)

//KeyGenerator.java
/**
 * 主鍵生成器接口,有三個實現類:
 * 1、 {@link Jdbc3KeyGenerator}<br>
 * 2、{@link NoKeyGenerator}<br>
 * 3、{@link SelectKeyGenerator}<br>
 * @author Clinton Begin
 */
public interface KeyGenerator {
  /**
   * 針對Sequence主鍵而言,在執行insert sql前必須指定一個主鍵值給要插入的記錄,
   * 如Oracle、DB2,KeyGenerator提供了processBefore()方法。
   * @param executor
   * @param ms
   * @param stmt
   * @param parameter
   */
  void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
  /**
   * 針對自增主鍵的表,在插入時不需要主鍵,而是在插入過程自動獲取一個自增的主鍵,
   * 比如MySQL,Postgresql,KeyGenerator提供了processAfter()方法
   * @param executor
   * @param ms
   * @param stmt
   * @param parameter
   */
  void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);

}
二、KeyGenerator實現類

  Mybatis提供了三個KeyGenerator接口的實現類,分別是NoKeyGenerator、Jdbc3KeyGenerator、SelectKeyGenerator。其中,

  • NoKeyGenerator:默認空實現,不對主鍵單獨處理;
  • Jdbc3KeyGenerator:主要用於數據庫的自增主鍵,比如 MySQL、PostgreSQL;
  • SelectKeyGenerator:主要用於數據庫不支持自增主鍵的情況,比如 Oracle、DB2;

類圖:
在這裏插入圖片描述

三、實現類Jdbc3KeyGenerator

  Jdbc3KeyGenerator主要用於支持主鍵自增的數據庫,比如MySQL、PostgreSQL、SQL Server等。

1、useGeneratedKeys屬性的用法

  首先,如果你的數據庫支持自動生成主鍵的字段(比如 MySQL 和 SQL Server),那麼你可以設置 useGeneratedKeys=”true”,然後再把 keyProperty 設置到目標屬性上就 OK 了。例如:

<insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
  insert into Author (username,password,email,bio)
  values (#{username},#{password},#{email},#{bio})
</insert>

注:需要把Author表中主鍵對應的字段設置成自動生成的列類型

2、Jdbc3KeyGenerator類詳解

  Jdbc3KeyGenerator實現類就是處理那些支持主鍵自增的數據庫的,主要用來支持上述提到的通過useGeneratedKeys屬性配置主鍵生成策略的實現。

  1. 變量
    Jdbc3KeyGenerator實現類中定義了一個靜態的Jdbc3KeyGenerator變量INSTANCE,供全局使用,即全局共享這一個靜態變量。
//Jdbc3KeyGenerator.java
public static final Jdbc3KeyGenerator INSTANCE = new Jdbc3KeyGenerator();
  1. processBefore()方法
    因爲Jdbc3KeyGenerator實現類是處理那些支持主鍵自增的數據庫的,所以在insert語句執行前,不做任何處理,即該方法爲空實現。
  2. processAfter()方法
    processAfter()方法是通過調用processBatch()方法實現想要的業務邏輯。調用processBatch()方法前,首先通過getParameters()方法處理參數,然後再調用processBatch()方法。
//Jdbc3KeyGenerator.java
  @Override
  public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    processBatch(ms, stmt, getParameters(parameter));
  }
  1. getParameters()方法
    將傳入的parameter實參轉換成Collection類型對象。
//Jdbc3KeyGenerator.java
  private Collection<Object> getParameters(Object parameter) {
    Collection<Object> parameters = null;
    if (parameter instanceof Collection) {
      parameters = (Collection) parameter;
    } else if (parameter instanceof Map) {
      Map parameterMap = (Map) parameter;
      if (parameterMap.containsKey("collection")) {
        parameters = (Collection) parameterMap.get("collection");
      } else if (parameterMap.containsKey("list")) {
        parameters = (List) parameterMap.get("list");
      } else if (parameterMap.containsKey("array")) {
        parameters = Arrays.asList((Object[]) parameterMap.get("array"));
      }
    }
    if (parameters == null) {
      parameters = new ArrayList<Object>();
      parameters.add(parameter);
    }
    return parameters;
  }
  1. processBatch()方法
    processBatch()方法主要實現了一下邏輯:
    1、獲取數據庫自動生成的主鍵
    2、獲取主鍵對應的屬性名稱
    3、檢測數據庫生成的主鍵的列數與keyProperties屬性指定的列數是否匹配
    4、for循環,處理參數。因爲在insert語句中,可能存在同時添加多條數據的情況,每次循環即處理一條添加的記錄。
    5、在for循環過程中,首先根據參數,獲取對應的typeHandler,然後根據typeHandler獲取主鍵字段對應的值,最後通過MetaObject的setValue()方法,把參數及其參數值寫入到對應對象中。
//Jdbc3KeyGenerator.java
public void processBatch(MappedStatement ms, Statement stmt, Collection<Object> parameters) {
    ResultSet rs = null;
    try {
    	//獲取數據庫自動生成的主鍵,如果沒有生成主鍵,則返回結采集爲空
      rs = stmt.getGeneratedKeys();
      final Configuration configuration = ms.getConfiguration();
      final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
      //獲得keyProperties屬性指定的屬性名稱,它表示主鍵對應的屬性名稱
      final String[] keyProperties = ms.getKeyProperties();
      //獲取ResultSet的元數據信息
      final ResultSetMetaData rsmd = rs.getMetaData();
      TypeHandler<?>[] typeHandlers = null;
      //檢測數據庫生成的主鍵的列數與keyProperties屬性指定的列數是否匹配
      if (keyProperties != null && rsmd.getColumnCount() >= keyProperties.length) {
        for (Object parameter : parameters) {
          // there should be one row for each statement (also one for each parameter)
          if (!rs.next()) {
            break;
          }
          final MetaObject metaParam = configuration.newMetaObject(parameter);
          if (typeHandlers == null) {//獲取主鍵字段分別對應的TypeHandler
            typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd);
          }
          //將生成的主鍵設直到用戶傳入的參數的對應位置
          populateKeys(rs, metaParam, keyProperties, typeHandlers);
        }
      }
    } catch (Exception e) {
      throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
    } finally {
      if (rs != null) {
        try {
          rs.close();
        } catch (Exception e) {
          // ignore
        }
      }
    }
  }
  1. getTypeHandlers()方法
    獲取對應的TypeHandler。
//Jdbc3KeyGenerator.java
private TypeHandler<?>[] getTypeHandlers(TypeHandlerRegistry typeHandlerRegistry, MetaObject metaParam, String[] keyProperties, ResultSetMetaData rsmd) throws SQLException {
    TypeHandler<?>[] typeHandlers = new TypeHandler<?>[keyProperties.length];
    for (int i = 0; i < keyProperties.length; i++) {
      if (metaParam.hasSetter(keyProperties[i])) {
        TypeHandler<?> th;
        try {
          Class<?> keyPropertyType = metaParam.getSetterType(keyProperties[i]);
          th = typeHandlerRegistry.getTypeHandler(keyPropertyType, JdbcType.forCode(rsmd.getColumnType(i + 1)));
        } catch (BindingException e) {
          th = null;
        }
        typeHandlers[i] = th;
      }
    }
    return typeHandlers;
  }
  1. populateKeys()方法
    將生成的主鍵設置到用戶傳入的參數的對應位置。
//Jdbc3KeyGenerator.java
private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {
    for (int i = 0; i < keyProperties.length; i++) {
      String property = keyProperties[i];
      TypeHandler<?> th = typeHandlers[i];
      if (th != null) {
        Object value = th.getResult(rs, i + 1);
        metaParam.setValue(property, value);
      }
    }
  }
四、實現類SelectKeyGenerator

  SelectKeyGenerator實現類主要用於數據庫不支持自增主鍵的情況,比如 Oracle、DB2等。

1、 <selectKey>節點用法

  對於不支持自動生成類型的數據庫或可能不支持自動生成主鍵的 JDBC 驅動,MyBatis 有另外一種方法來生成主鍵。這裏有一個簡單(甚至很傻)的示例,它可以生成一個隨機 ID(你最好不要這麼做,但這裏展示了 MyBatis 處理問題的靈活性及其所關心的廣度)。在下面的示例中,selectKey 元素中的語句將會首先運行,Author 的 id 會被設置,然後插入語句會被調用。這可以提供給你一個與數據庫中自動生成主鍵類似的行爲,同時保持了 Java 代碼的簡潔。

<insert id="insertAuthor">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
  </selectKey>
  insert into Author
    (id, username, password, email,bio, favourite_section)
  values
    (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>
2、 SelectKeyGenerator類詳解

  對於不支持自動生成自增主鍵的數據庫,例如Oracle 數據庫,用戶可以利用Mybatis提供的SelectkeyGenerator 來生成主鍵, SelectkeyGenerator 也可以實現類似於Jdbc3KeyGenerator 提供的、獲取數據庫自動生成的主鍵的功能。

  1. 變量
//SelectKeyGenerator.java
  /**
	 * <selectKey>節點,解析過程中,生成id時,使用的默認後綴
	 */
  public static final String SELECT_KEY_SUFFIX = "!selectKey";
  /**
   * 標識<selectKey>節點中定義的SQL語句是在insert語句之前執行還是之後執行
   */
  private final boolean executeBefore;
  /**
   * <selectKey>節點中定義的SQL語句所對應的MappedStatement對象。
   * 該MappedStatement對象是在解析<selectKey>節點時創建的。
   * 該SQL語句用於獲取insert語句中使用的主鍵。
   */
  private final MappedStatement keyStatement;
  1. 構造函數
//SelectKeyGenerator.java
public SelectKeyGenerator(MappedStatement keyStatement, boolean executeBefore) {
    this.executeBefore = executeBefore;
    this.keyStatement = keyStatement;
  }
  1. processBefore()方法、processAfter()方法
    在processBefore()方法和processAfter()方法的實現都是調用processGeneratedKeys()方法。通過標識<selectKey>節點中定義的SQL語句是在insert語句之前執行還是之後執行的變量executeBefore來確定,執行processBefore()或processAfter()方法。(根據方法的邏輯,支持自增主鍵的數據庫,理論上應該也可以使用該方法,進行主鍵生成,未驗證)
//SelectKeyGenerator.java
@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);
    }
  }
  1. processGeneratedKeys()方法
    執行<selectKey>節點中配置的SQL語句,獲取insert語句中用到的主鍵並映射成對象,然後按照配置,將主鍵對象中對應的屬性設置到用戶參數中。
//SelectKeyGenerator.java
private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
    try {
      if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {
    	//獲取<selectKey>節點的keyProperties配置的屬性名稱,它表示主鍵對應的屬性
        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對象,並執行keyStatement字段中記錄的SQL語句,並得到主鍵對象
          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);
    }
  }
  1. setValue()方法
    通過MetaObject實例對象,實現爲指定對象的字段賦值。
//SelectKeyGenerator.java
private void setValue(MetaObject metaParam, String property, Object value) {
    if (metaParam.hasSetter(property)) {
      metaParam.setValue(property, value);
    } else {
      throw new ExecutorException("No setter found for the keyProperty '" + property + "' in " + metaParam.getOriginalObject().getClass().getName() + ".");
    }
  }
  1. handleMultipleProperties()方法
    處理主鍵有多列的情況,其實現是從主鍵對象中取出指定屬性,並設直到用戶參數的對應屬性中。
//SelectKeyGenerator.java
private void handleMultipleProperties(String[] keyProperties,
      MetaObject metaParam, MetaObject metaResult) {
    String[] keyColumns = keyStatement.getKeyColumns();
      
    if (keyColumns == null || keyColumns.length == 0) {
      // no key columns specified, just use the property names
      for (String keyProperty : keyProperties) {
        setValue(metaParam, keyProperty, metaResult.getValue(keyProperty));
      }
    } else {
      if (keyColumns.length != keyProperties.length) {
        throw new ExecutorException("If SelectKey has key columns, the number must match the number of key properties.");
      }
      for (int i = 0; i < keyProperties.length; i++) {
        setValue(metaParam, keyProperties[i], metaResult.getValue(keyColumns[i]));
      }
    }
  }
發佈了52 篇原創文章 · 獲贊 3 · 訪問量 3881
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章