Mybatis3.3.x技術內幕(十四):Mybatis之KeyGenerator

在Mybatis中,執行insert操做時,若是咱們但願返回數據庫生成的自增主鍵值,那麼就須要使用到KeyGenerator對象。java

須要注意的是,KeyGenerator的做用,是返回數據庫生成的自增主鍵值,而不是生成數據庫的自增主鍵值。返回的主鍵值放到哪兒呢?放到parameter object的主鍵屬性上。mysql

下面看看其接口定義。sql

public interface KeyGenerator {
  void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
  void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}

接口定義仍是比較簡單的,就是在insert前、insert後,策略處理主鍵值。數據庫

 

 

 

(Made In IntelliJ IDEA IDE)apache

Jdbc3KeyGenerator:用於處理數據庫支持自增主鍵的狀況,如MySQL的auto_increment。網絡

NoKeyGenerator:空實現,不須要處理主鍵。mybatis

SelectKeyGenerator:用於處理數據庫不支持自增主鍵的狀況,好比Oracle的sequence序列。app

上面都比較泛泛而談,咱們來點實際的,看看它們都是如何工做的。框架

1. JDBC實現insert後,返回自增主鍵值的原理ide

Class.forName("com.mysql.jdbc.Driver");

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root", "123");
conn.setAutoCommit(false);
PreparedStatement pstm = conn.prepareStatement("insert into students(name, email) values(?, ?)",
Statement.RETURN_GENERATED_KEYS);

pstm.setString(1, "name1");
pstm.setString(2, "email1");
pstm.addBatch();
pstm.setString(1, "name2");
pstm.setString(2, "email2");
pstm.addBatch();
pstm.executeBatch();
// 返回自增主鍵值
ResultSet rs = pstm.getGeneratedKeys();
while (rs.next()) {
		Object value = rs.getObject(1);
		System.out.println(value);
	}
conn.commit();
rs.close();
pstm.close();
conn.close();

output:
246
247

以上代碼,僅做爲演示使用。Mybatis是對JDBC的封裝,其Jdbc3KeyGenerator類,就是使用上面的原理,來返回數據庫生成的主鍵值的。

2. Jdbc3KeyGenerator源碼解讀

public class Jdbc3KeyGenerator implements KeyGenerator {

  @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, getParameters(parameter));
  }

  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();
      final String[] keyProperties = ms.getKeyProperties();
      final ResultSetMetaData rsmd = rs.getMetaData();
      TypeHandler<?>[] typeHandlers = null;
      if (keyProperties != null && rsmd.getColumnCount() >= keyProperties.length) {
        // 給參數object對象的屬性賦主鍵值(批量插入,多是多個)
        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) {
            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
        }
      }
    }
  }
private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {
    // 主鍵字段,多是多個(通常狀況下,是一個)
    for (int i = 0; i < keyProperties.length; i++) {
      TypeHandler<?> th = typeHandlers[i];
      if (th != null) {
        Object value = th.getResult(rs, i + 1);
       // 反射賦值
        metaParam.setValue(keyProperties[i], value);
      }
    }
  }
//...

Mapper.Xml配置方式。

<insert id="insertStudents" useGeneratedKeys="true" keyProperty="studId" parameterType="Student">

3. NoKeyGenerator源碼解讀

徹底是空實現,沒啥可說的。

4. SelectKeyGenerator的原理

<insert id="insertStudent" parameterType="Student" >
		<selectKey keyProperty="studId" resultType="int" order="BEFORE"> 
			SELECT ELEARNING.STUD_ID_SEQ.NEXTVAL FROM DUAL 
		</selectKey>
		INSERT INTO
		STUDENTS(STUD_ID, NAME, EMAIL, DOB, PHONE)
		VALUES(#{studId}, #{name},
		#{email}, #{dob}, #{phone})
	</insert>

在執行insert以前,先發起一個sql查詢,將返回的序列值賦值給Student的stuId屬性,而後再執行insert操做,這樣表中的stud_id字段就有值了。order="BEFORE"表示insert前執行,好比取sequence序列值;order="AFTER"表示insert以後執行,好比使用觸發器給主鍵stud_id賦值。比較簡單,我就再也不貼源碼了。

注意:因爲selectKey自己返回單個序列主鍵值,也就沒法支持批量insert操做並返回主鍵id列表了。若是要執行批量insert,請選擇使用for循環執行屢次插入操做。

5. KeyGenerator的建立過程

每個MappedStatement,都有一個非空的KeyGenerator引用。

org.apache.ibatis.mapping.MappedStatement.Builder.Builder()構造方法賦初始值源碼。

mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? new Jdbc3KeyGenerator() : new NoKeyGenerator();

org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode()覆蓋KeyGenerator初始值的源碼。

String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
if (configuration.hasKeyGenerator(keyStatementId)) {
      // 表示存在selectKey獲取主鍵值方式
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
    }

org.apache.ibatis.builder.xml.XMLStatementBuilder.parseSelectKeyNode()解析<selectKey>元素,構建SelectKeyGenerator的源碼。

MappedStatement keyStatement = configuration.getMappedStatement(id, false);
configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));

所以,只有SelectKeyGenerator會保存至Configuration對象的Map<String, KeyGenerator> keyGenerators屬性當中。<selectKey>元素,會被Mybatis解析爲一個MappedStatement對象,並做爲構造參數傳遞至SelectKeyGenerator內保存起來。

public class SelectKeyGenerator implements KeyGenerator {
  
  public static final String SELECT_KEY_SUFFIX = "!selectKey";
  private boolean executeBefore;
  private MappedStatement keyStatement;
//...

Map<String, KeyGenerator> keyGenerators的存儲結構以下。

{insertStudent!selectKey=org.apache.ibatis.executor.keygen.SelectKeyGenerator@59d016c9, 
com.mybatis3.mappers.StudentMapper.insertStudent!selectKey=org.apache.ibatis.executor.keygen.SelectKeyGenerator@59d016c9}

至此,每個MappedStatement對象,都恰當的綁定了一個KeyGenerator對象,就能夠開始工做了。

6. KeyGenerator的使用過程

keyGenerator.processBefore()方法調用時機。

org.apache.ibatis.executor.statement.BaseStatementHandler.BaseStatementHandler()構造方法源碼。

if (boundSql == null) {
      // 調用keyGenerator.processBefore()方法
      generateKeys(parameterObject);
      boundSql = mappedStatement.getBoundSql(parameterObject);
    }
// ...

 protected void generateKeys(Object parameter) {
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processBefore(executor, mappedStatement, null, parameter);
  }

即,建立StatementHandler對象時,就會執行keyGenerator.processBefore()方法。keyGenerator.processAfter()方法,天然就是Statement執行後執行了。

org.apache.ibatis.executor.statement.SimpleStatementHandler.update(Statement)方法源碼。其餘的StatementHandler都是相似的。

@Override
  public int update(Statement statement) throws SQLException {
    String sql = boundSql.getSql();
    Object parameterObject = boundSql.getParameterObject();
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    int rows;
    if (keyGenerator instanceof Jdbc3KeyGenerator) {
      statement.execute(sql, Statement.RETURN_GENERATED_KEYS);
      rows = statement.getUpdateCount();
      keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
    } else if (keyGenerator instanceof SelectKeyGenerator) {
      statement.execute(sql);
      rows = statement.getUpdateCount();
      keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
    } else {
      statement.execute(sql);
      rows = statement.getUpdateCount();
    }
    return rows;
  }

7. 批量插入,返回主鍵id列表

for (Student student : students) {
	studentMapper.insertStudent(student);
}

對的,你沒看錯,就是像上面這樣for循環逐一insert操做的,此時,若是你考慮性能的話,可使用BatchExecutor來完成,固然了,其餘的Executor也是能夠的。

若是文章就像上面這樣寫,那麼就徹底失去了寫文章的價值,上面的for循環,誰都懂這麼操做能夠實現,可是,不少人想要的並非這個例子,而是另一種批量插入操做,返回主鍵id列表。那麼,看第8條。

8. Mybatis批量插入,返回主鍵id列表爲null

<insert id="insertStudents" useGeneratedKeys="true" keyProperty="studId" parameterType="java.util.ArrayList">
		INSERT INTO
		STUDENTS(STUD_ID, NAME, EMAIL, DOB, PHONE)
		VALUES
	<foreach collection="list" item="item" index="index" separator=","> 
        	(#{item.studId},#{item.name},#{item.email},#{item.dob}, #{item.phone}) 
    	</foreach> 
</insert>

不少同窗,包括開源中國社區,都遇到使用上面的批量insert操做,返回的主鍵id列表是null的問題,不少人得出結論:Mybatis不支持這種形式的批量插入並返回主鍵id列表。真是這樣嗎?

我必須明確的跟你們說,Mybatis是支持上述形式的批量插入,且能夠正確返回主鍵id列表的。之因此返回null值,是Mybatis框架的一個bug,下一篇將具體講述產生這個bug的緣由,以及如何修復它。

版權提示:文章出自開源中國社區,若對文章感興趣,可關注個人開源中國社區博客(http://my.oschina.net/zudajun)。(通過網絡爬蟲或轉載的文章,常常丟失流程圖、時序圖,格式錯亂等,仍是看原版的比較好)

相關文章
相關標籤/搜索