精盡MyBatis原始碼分析 - SQL執行過程(二)之 StatementHandler

月圓吖發表於2020-11-25

該系列文件是本人在學習 Mybatis 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋(Mybatis原始碼分析 GitHub 地址Mybatis-Spring 原始碼分析 GitHub 地址Spring-Boot-Starter 原始碼分析 GitHub 地址)進行閱讀

MyBatis 版本:3.5.2

MyBatis-Spring 版本:2.0.3

MyBatis-Spring-Boot-Starter 版本:2.1.4

MyBatis的SQL執行過程

在前面一系列的文件中,我已經分析了 MyBatis 的基礎支援層以及整個的初始化過程,此時 MyBatis 已經處於就緒狀態了,等待使用者發號施令了

那麼接下來我們來看看它執行SQL的整個過程,該過程比較複雜,涉及到二級快取,將返回結果轉換成 Java 物件以及延遲載入等等處理過程,這裡將一步一步地進行分析:

MyBatis中SQL執行的整體過程如下圖所示:

SQLExecuteProcess

在 SqlSession 中,會將執行 SQL 的過程交由Executor執行器去執行,過程大致如下:

  1. 通過DefaultSqlSessionFactory建立與資料庫互動的 SqlSession “會話”,其內部會建立一個Executor執行器物件
  2. 然後Executor執行器通過StatementHandler建立對應的java.sql.Statement物件,並通過ParameterHandler設定引數,然後執行資料庫相關操作
  3. 如果是資料庫更新操作,則可能需要通過KeyGenerator先設定自增鍵,然後返回受影響的行數
  4. 如果是資料庫查詢操作,則需要將資料庫返回的ResultSet結果集物件包裝成ResultSetWrapper,然後通過DefaultResultSetHandler對結果集進行對映,最後返回 Java 物件

上面還涉及到一級快取二級快取延遲載入等其他處理過程

SQL執行過程(二)之StatementHandler

在上一篇文件中,已經詳細地分析了在MyBatis的SQL執行過程中,SqlSession會話將資料庫操作交由Executor執行器去完成,實際上需要通過StatementHandler建立相應的Statement物件,並做一些準備工作,然後通過Statement執行資料庫操作,查詢結果則需要通過ResultSetHandler對結果集進行對映轉換成Java物件,那麼接下來我們先來看看StatementHandler到底做哪些操作

StatementHandler介面的實現類如下圖所示:

StatementHandler
  • org.apache.ibatis.executor.statement.RoutingStatementHandler:實現StatementHandler介面,裝飾器模式,根據Statement型別建立對應的StatementHandler物件,所有的方法執行交由該物件執行

  • org.apache.ibatis.executor.statement.BaseStatementHandler:實現StatementHandler介面,提供骨架方法,指定的幾個抽象方法交由不同的子類去實現

  • org.apache.ibatis.executor.statement.SimpleStatementHandler:繼承BaseStatementHandler抽象類,建立java.sql.Statement進行資料庫操作

  • org.apache.ibatis.executor.statement.PreparedStatementHandler:繼承BaseStatementHandler抽象類,建立java.sql.PreparedStatement進行資料庫操作(預設)

  • org.apache.ibatis.executor.statement.CallableStatementHandler:繼承BaseStatementHandler抽象類,建立java.sql.CallableStatement進行資料庫操作,用於儲存過程

我們先回顧一下StatementHandler是在哪裡被建立的,可以在《SQL執行過程(一)之Executor》SimpleExecutor小節中有講到,建立的是RoutingStatementHandler物件

StatementHandler

org.apache.ibatis.executor.statement.StatementHandler:Statement處理器介面,程式碼如下:

public interface StatementHandler {
	/**
	 * 準備操作,可以理解成建立 Statement 物件
	 *
	 * @param connection         Connection 物件
	 * @param transactionTimeout 事務超時時間
	 * @return Statement 物件
	 */
	Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;

	/**
	 * 設定 Statement 物件的引數
	 *
	 * @param statement Statement 物件
	 */
	void parameterize(Statement statement) throws SQLException;

	/**
	 * 新增 Statement 物件的批量操作
	 *
	 * @param statement Statement 物件
	 */
	void batch(Statement statement) throws SQLException;

	/**
	 * 執行寫操作
	 *
	 * @param statement Statement 物件
	 * @return 影響的條數
	 */
	int update(Statement statement) throws SQLException;

	/**
	 * 執行讀操作
	 *
	 * @param statement     Statement 物件
	 * @param resultHandler ResultHandler 物件,處理結果
	 * @param <E>           泛型
	 * @return 讀取的結果
	 */
	<E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;

	/**
	 * 執行讀操作,返回 Cursor 物件
	 *
	 * @param statement Statement 物件
	 * @param <E>       泛型
	 * @return Cursor 物件
	 */
	<E> Cursor<E> queryCursor(Statement statement) throws SQLException;

	/**
	 * @return BoundSql 物件
	 */
	BoundSql getBoundSql();

	/**
	 * @return ParameterHandler 物件
	 */
	ParameterHandler getParameterHandler();

}

每個方法可以根據註釋先理解它的作用,在實現類中的會講到

RoutingStatementHandler

org.apache.ibatis.executor.statement.RoutingStatementHandler:實現StatementHandler介面,採用裝飾器模式,在初始化的時候根據Statement型別,建立對應的StatementHandler物件,程式碼如下:

public class RoutingStatementHandler implements StatementHandler {

	private final StatementHandler delegate;

	public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
			ResultHandler resultHandler, BoundSql boundSql) {
		// 根據不同的型別,建立對應的 StatementHandler 實現類
		switch (ms.getStatementType()) {
		case STATEMENT:
			delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
			break;
		case PREPARED:
			delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
			break;
		case CALLABLE:
			delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
			break;
		default:
			throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
		}
	}
}
  • 在建構函式中初始化delegate委託物件,根據MappedStatement(每個SQL對應的物件)的statementType型別,建立對應的StatementHandler實現類

  • 其餘所有的方法都是直接交由delegate去執行的,這裡就不列出來了,就是實現StatementHandler介面的方法

回顧到《MyBatis初始化(二)之載入Mapper介面與XML對映檔案》中的XMLStatementBuilder小節,在parseStatementNode方法中的第10步如下:

StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));

所以說statementType的預設值為PREPARED,委託物件也就是PreparedStatementHandler型別

BaseStatementHandler

org.apache.ibatis.executor.statement.BaseStatementHandler:實現StatementHandler介面,提供骨架方法,指定的幾個抽象方法交由不同的子類去實現

構造方法

public abstract class BaseStatementHandler implements StatementHandler {

    /**
     * 全域性配置
     */
	protected final Configuration configuration;
    /**
     * 例項工廠
     */
	protected final ObjectFactory objectFactory;
    /**
     * 型別處理器登錄檔
     */
	protected final TypeHandlerRegistry typeHandlerRegistry;
    /**
     * 執行結果處理器
     */
	protected final ResultSetHandler resultSetHandler;
    /**
     * 引數處理器,預設 DefaultParameterHandler
     */
	protected final ParameterHandler parameterHandler;
    /**
     * 執行器
     */
	protected final Executor executor;
    /**
     * SQL 相關資訊
     */
	protected final MappedStatement mappedStatement;
    /**
     * 分頁條件
     */
	protected final RowBounds rowBounds;
    /**
     * SQL 語句
     */
	protected BoundSql boundSql;

	protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject,
			RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
		this.configuration = mappedStatement.getConfiguration();
		this.executor = executor;
		this.mappedStatement = mappedStatement;
		this.rowBounds = rowBounds;
		this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
		this.objectFactory = configuration.getObjectFactory();

		// <1> 如果 boundSql 為空,更新資料庫的操作這裡傳入的物件會為 null
		if (boundSql == null) { // issue #435, get the key before calculating the statement
			// <1.1> 生成 key,定義了 <selectKey /> 且配置了 order="BEFORE",則在 SQL 執行之前執行
			generateKeys(parameterObject);
			// <1.2> 建立 BoundSql 物件
			boundSql = mappedStatement.getBoundSql(parameterObject);
		}

		this.boundSql = boundSql;

		// <2> 建立 ParameterHandler 物件,預設為 DefaultParameterHandler
		// PreparedStatementHandler 實現的 parameterize 方法中需要對引數進行預處理,進行引數化時需要用到
		this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
		// <3> 建立 DefaultResultSetHandler 物件
		this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
	}
}

關於它的屬性可以根據註釋進行理解

  1. 如果入參中的boundSqlnull,則需要進行初始化,可以會看到SimpleExecutor中執行資料庫的更新操作時,傳入的boundSqlnull,資料庫的查詢操作才會傳入該物件的值

    1. 呼叫generateKeys(Object parameter)方法,根據配置的KeyGenerator物件,在SQL執行之前執行查詢操作獲取值,設定到入參物件對應屬性中,程式碼如下:

      protected void generateKeys(Object parameter) {
          /*
           * 獲得 KeyGenerator 物件
           * 1. 配置了 <selectKey /> 則會生成 SelectKeyGenerator 物件
           * 2. 配置了 useGeneratedKeys="true" 則會生成 Jdbc3KeyGenerator 物件
           * 否則為 NoKeyGenerator 物件
           */
          KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
          ErrorContext.instance().store();
          // 前置處理,建立自增編號到 parameter 中
          keyGenerator.processBefore(executor, mappedStatement, null, parameter);
          ErrorContext.instance().recall();
      }
      

      只有配置的<selectKey />標籤才有前置處理,這就是為什麼資料庫的更新操作傳入的boundSqlnull的原因,因為入參中有的屬性值可能需要提前生成一個值(執行配置的SQL語句),KeyGenerator會在後續講到?

    2. 通過MappedStatement物件根據入參獲取BoundSql物件,在《MyBatis初始化(四)之SQL初始化(下)》中的SqlSource小節中有講到這個方法,如果是動態SQL則需要進行解析,獲取到最終的SQL,替換成?佔位符

  2. 建立ParameterHandler物件,用於對引數進行預處理,預設為DefaultParameterHandler,這個也在《MyBatis初始化(四)之SQL初始化(下)》中有講過

    可以看到Configuration的newParameterHandler方法:

    public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        // 建立 ParameterHandler 物件
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
        // 應用外掛
        parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
        return parameterHandler;
    }
    
  3. 建立ResultSetHandler,用於返回結果的對映,預設為DefaultResultSetHandler,這個對映過程非常複雜,會有單獨一篇文件進行分析?

    可以看到Configuration的newResultSetHandler方法:

    public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds,
            ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
        // 建立 DefaultResultSetHandler 物件
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, 
                                                                        resultHandler, boundSql, rowBounds);
        // 應用外掛
        resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
        return resultSetHandler;
    }
    

prepare方法

建立Statement物件,做一些初始化工作,程式碼如下:

@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
        // <1> 建立 Statement 物件
        statement = instantiateStatement(connection);
        // <2> 設定執行和事務的超時時間
        setStatementTimeout(statement, transactionTimeout);
        // <3> 設定 fetchSize,為驅動的結果集獲取數量(fetchSize)設定一個建議值
        setFetchSize(statement);
        return statement;
    } catch (SQLException e) {
        // 發生異常,進行關閉
        closeStatement(statement);
        throw e;
    } catch (Exception e) {
        // 發生異常,進行關閉
        closeStatement(statement);
        throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
}
  1. 建立 Statement 物件,呼叫instantiateStatement(Connection connection)抽象方法,交由不同的子類去實現

  2. 設定執行和事務的超時時間,呼叫setStatementTimeout(Statement stmt, Integer transactionTimeout)方法,如下:

    protected void setStatementTimeout(Statement stmt, Integer transactionTimeout) throws SQLException {
        Integer queryTimeout = null;
        // 獲得 queryTimeout
        if (mappedStatement.getTimeout() != null) {
            queryTimeout = mappedStatement.getTimeout();
        } else if (configuration.getDefaultStatementTimeout() != null) {
            queryTimeout = configuration.getDefaultStatementTimeout();
        }
        // 設定執行的超時時間
        if (queryTimeout != null) {
            stmt.setQueryTimeout(queryTimeout);
        }
        // 設定事務超時時間
        StatementUtil.applyTransactionTimeout(stmt, queryTimeout, transactionTimeout);
    }
    
  3. 設定 fetchSize,為驅動的結果集獲取數量(fetchSize)設定一個建議值(無預設值),呼叫setFetchSize(Statement stmt)方法,如下:

    protected void setFetchSize(Statement stmt) throws SQLException {
        // 獲得 fetchSize 配置
        Integer fetchSize = mappedStatement.getFetchSize();
        if (fetchSize != null) {
            stmt.setFetchSize(fetchSize);
            return;
        }
        // 獲得 fetchSize 的預設配置
        Integer defaultFetchSize = configuration.getDefaultFetchSize();
        if (defaultFetchSize != null) {
            stmt.setFetchSize(defaultFetchSize);
        }
    }
    
  4. 發生任何異常都會關閉 Statement 物件

SimpleStatementHandler

org.apache.ibatis.executor.statement.SimpleStatementHandler:繼承BaseStatementHandler抽象類,建立java.sql.Statement進行資料庫操作,部分程式碼如下:

public class SimpleStatementHandler extends BaseStatementHandler {

	public SimpleStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter,
			RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
		super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
	}

	@Override
	public void batch(Statement statement) throws SQLException {
		String sql = boundSql.getSql();
		// 新增到批處理
		statement.addBatch(sql);
	}

	@Override
	public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
		String sql = boundSql.getSql();
		// <1> 執行查詢
		statement.execute(sql);
		// <2> 處理返回結果
		return resultSetHandler.handleResultSets(statement);
	}

	@Override
	public <E> Cursor<E> queryCursor(Statement statement) throws SQLException {
		String sql = boundSql.getSql();
		statement.execute(sql);
		return resultSetHandler.handleCursorResultSets(statement);
	}

	@Override
	protected Statement instantiateStatement(Connection connection) throws SQLException {
		if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
			// 建立Statement物件
			return connection.createStatement();
		} else {
			return connection.createStatement(mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
		}
	}

	@Override
	public void parameterize(Statement statement) {
		// N/A
	}
}
  • 上面的方法都很簡單,都是直接通過Statement物件執行資料庫操作

  • 在查詢方法中,需要通過resultSetHandler對結果集進行對映,返回對應的Java物件

update方法

執行資料庫更新操作,方法如下:

@Override
public int update(Statement statement) throws SQLException {
    String sql = boundSql.getSql();
    Object parameterObject = boundSql.getParameterObject();
    /*
     * 獲得 KeyGenerator 物件
     * 1. 配置了 <selectKey /> 則會生成 SelectKeyGenerator 物件
     * 2. 配置了 useGeneratedKeys="true" 則會生成 Jdbc3KeyGenerator 物件
     * 否則為 NoKeyGenerator 物件
     */
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    int rows;
    if (keyGenerator instanceof Jdbc3KeyGenerator) { // 如果是 Jdbc3KeyGenerator 型別
        // <1.1> 執行寫操作,設定返回自增鍵,可通過 getGeneratedKeys() 方法獲取
        statement.execute(sql, Statement.RETURN_GENERATED_KEYS);
        // <1.2> 獲得更新數量
        rows = statement.getUpdateCount();
        // <1.3> 執行 keyGenerator 的後置處理邏輯,也就是對我們配置的自增鍵進行賦值
        keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
    } else if (keyGenerator instanceof SelectKeyGenerator) { // 如果是 SelectKeyGenerator 型別
        // <2.1> 執行寫操作
        statement.execute(sql);
        // <2.2> 獲得更新數量
        rows = statement.getUpdateCount();
        // <2.3>執行 keyGenerator 的後置處理邏輯,也就是對我們配置的自增鍵進行賦值
        keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
    } else {
        // <3.1> 執行寫操作
        statement.execute(sql);
        // <3.2> 獲得更新數量
        rows = statement.getUpdateCount();
    }
    return rows;
}

為什麼資料庫更新操作的方法需要做這麼多處理,其實目的就一個,支援使用者配置的自增鍵,設定到入參中

BaseStatementHandler的構造方法已經有過KeyGenerator前置處理了,那裡是在SQL執行之前,執行查詢操作獲取值,設定到入參物件對應屬性中

而這裡需要做的就是在SQL執行的後置處理了,在SQL執行之後,執行查詢操作獲取值或者設定需要返回哪些自增鍵,設定到入參物件對應屬性中

  1. 如果KeyGeneratorJdbc3KeyGenerator型別,也就是配置useGeneratedKeys="true"

    1. 執行寫操作,設定需要返回自增鍵,可通過getGeneratedKeys()方法獲取
    2. 獲得受影響的行數
    3. 執行後置處理,呼叫其processAfter方法,也就是將我們配置的自增鍵設定到入參物件中
  2. 如果KeyGeneratorSelectKeyGenerator型別,也就是新增了<selectKey />標籤

    1. 執行寫操作
    2. 獲得受影響的行數
    3. 執行後置處理,呼叫其processAfter方法,也就是執行查詢操作獲取值,設定到入參物件對應屬性中
  3. 如果沒有配置KeyGenerator

    1. 執行寫操作
    2. 獲得受影響的行數

PreparedStatementHandler

org.apache.ibatis.executor.statement.PreparedStatementHandler:繼承BaseStatementHandler抽象類,建立java.sql.PreparedStatement進行資料庫操作(預設),部分程式碼如下:

public class PreparedStatementHandler extends BaseStatementHandler {

	public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameter,
			RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
		super(executor, mappedStatement, parameter, rowBounds, resultHandler, boundSql);
	}

	@Override
	public void batch(Statement statement) throws SQLException {
		PreparedStatement ps = (PreparedStatement) statement;
		// 新增到批處理
		ps.addBatch();
	}

	@Override
	public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
		PreparedStatement ps = (PreparedStatement) statement;
		// 執行
		ps.execute();
		// 結果處理器並返回結果
		return resultSetHandler.handleResultSets(ps);
	}

	@Override
	public <E> Cursor<E> queryCursor(Statement statement) throws SQLException {
		PreparedStatement ps = (PreparedStatement) statement;
    	// 執行
		ps.execute();
    	// 結果處理器並返回 Cursor 結果
		return resultSetHandler.handleCursorResultSets(ps);
	}

}
  • 上面的方法都很簡單,都是直接通過PreparedStatement物件執行資料庫操作

  • 在查詢方法中,需要通過resultSetHandler對結果集進行對映,返回對應的Java物件

instantiateStatement方法

建立一個PreparedStatement物件,方法如下:

@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    /*
     * 獲得 KeyGenerator 物件
     * 1. 配置了 <selectKey /> 則會生成 SelectKeyGenerator 物件
     * 2. 配置了 useGeneratedKeys="true" 則會生成 Jdbc3KeyGenerator 物件
     * 否則為 NoKeyGenerator 物件
     */
    // <1> 處理 Jdbc3KeyGenerator 的情況
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
        // <1.1> 獲得 keyColumn 配置
        String[] keyColumnNames = mappedStatement.getKeyColumns();
        if (keyColumnNames == null) {
            // <1.2 >建立 PreparedStatement 物件,並返回自增鍵,並可通過 getGeneratedKeys() 方法獲取
            return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
        } else {
            // <1.3> 建立 PreparedStatement 物件,並返回我們配置的 column 列名自增鍵,並可通過 getGeneratedKeys() 方法獲取
            return connection.prepareStatement(sql, keyColumnNames);
        }
    } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
        // <2> 建立 PrepareStatement 物件
        return connection.prepareStatement(sql);
    } else {
        // <3> 建立 PrepareStatement 物件,指定 ResultSetType
        return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    }
}
  1. 處理Jdbc3KeyGenerator的情況,也就是配置了useGeneratedKeys="true"

    1. 獲得keyColumn配置,哪些自增鍵需要返回
    2. 如果keyColumn為null,返回PreparedStatement物件,設定RETURN_GENERATED_KEYS,表示所有自增列都返回,可通過getGeneratedKeys()方法獲取
    3. 如果keyColumn不為null,返回PreparedStatement物件,設定需要返回的自增列為keyColumn,可通過getGeneratedKeys()方法獲取
  2. 沒有配置Jdbc3KeyGenerator物件,建立PreparedStatement物件返回,預設情況

  3. 沒有配置Jdbc3KeyGenerator物件,但是指定了ResultSetType,則返回PreparedStatement物件,指定ResultSetType

parameterize方法

設定PreparedStatement的佔位符引數,方法如下:

@Override
public void parameterize(Statement statement) throws SQLException {
    // 通過 DefaultParameterHandler 設定 PreparedStatement 的佔位符引數
    parameterHandler.setParameters((PreparedStatement) statement);
}

update方法

執行資料庫更新操作,方法如下:

@Override
public int update(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    // 執行
    ps.execute();
    // 獲得更新數量
    int rows = ps.getUpdateCount();
    // 入參物件
    Object parameterObject = boundSql.getParameterObject();
    /*
     * 獲得 KeyGenerator 物件
     * 1. 配置了 <selectKey /> 則會生成 SelectKeyGenerator 物件
     * 2. 配置了 useGeneratedKeys="true" 則會生成 Jdbc3KeyGenerator 物件
     * 否則為 NoKeyGenerator 物件
     */
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
	// 執行 keyGenerator 的後置處理邏輯,也就是對我們配置的自增鍵進行賦值
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    return rows;
}
  1. 執行資料庫更新操作
  2. 獲得受影響行數
  3. 根據配置的KeyGenerator物件,執行後置處理,執行查詢操作獲取值,或者獲取返回的自增鍵,設定到入參物件對應屬性中

CallableStatementHandler

org.apache.ibatis.executor.statement.CallableStatementHandler:繼承BaseStatementHandler抽象類,建立java.sql.CallableStatement進行資料庫操作,用於儲存過程

在執行完資料庫的操作後需要呼叫DefaultResultSetHandlerhandleOutputParameters方法,處理需要作為出參的引數,這裡就不做過得的講述了?

KeyGenerator

在上面已經講到在執行資料庫更新操作時,需要通過KeyGenerator來進行前置處理或者後置處理,我一般用於自增主鍵

先來看看它的實現類,如下圖所示:

KeyGenerator
  • org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator:實現KeyGenerator介面,只有後置處理的實現,獲取返回的自增鍵,設定到入參的屬性中,配置了useGeneratedKeys="true"則會建立該物件

  • org.apache.ibatis.executor.keygen.SelectKeyGenerator:實現KeyGenerator介面,執行資料庫查詢操作,獲取到對應的返回結果設定到入參的屬性中,新增了<selectKey />標籤則會建立該物件,前後置處理可以配置

  • org.apache.ibatis.executor.keygen.NoKeyGenerator:實現KeyGenerator介面,空實現,一個單例,沒有配置上面的兩種方式,則預設為該物件

KeyGenerator介面,程式碼如下:

public interface KeyGenerator {
  /**
   * 在 SQL 執行後設定自增鍵到入參中
   *
   * @param executor  執行器
   * @param ms        MappedStatement 物件
   * @param stmt      Statement物件
   * @param parameter 入參物件
   */
  void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);

  /**
   * 在 SQL 執行前設定自增鍵到入參中
   *
   * @param executor  執行器
   * @param ms        MappedStatement 物件
   * @param stmt      Statement物件
   * @param parameter 入參物件
   */
  void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}

Jdbc3KeyGenerator

org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator:實現KeyGenerator介面,只有後置處理的實現,獲取返回的自增鍵,設定到入參的屬性中,配置了useGeneratedKeys="true"則會建立該物件,在《MyBatis初始化(二)之載入Mapper介面與XML對映檔案》XMLStatementBuilder小節的parseStatementNode方法中的第8步看到

實現方法:

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

可以看到前置處理的實現方法為空,後置處理的實現方法呼叫processBatch方法

processBatch方法

processBatch(MappedStatement ms, Statement stmt, Object parameter)方法,從結果集中獲自增鍵設定到入參物件的屬性中,程式碼如下:

public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
    // 獲取 keyProperty 配置
    final String[] keyProperties = ms.getKeyProperties();
    if (keyProperties == null || keyProperties.length == 0) {
      return;
    }
    // 獲取 Statement 執行後自增鍵對應的 ResultSet 物件
    try (ResultSet rs = stmt.getGeneratedKeys()) {
      // 獲取 ResultSet 的 ResultSetMetaData 後設資料物件
      final ResultSetMetaData rsmd = rs.getMetaData();
      final Configuration configuration = ms.getConfiguration();
      if (rsmd.getColumnCount() < keyProperties.length) { // 自增鍵與 keyProperty 數量不一致則跳過
        // 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);
    }
}
  1. 獲取keyProperty配置,也就是需要將自增鍵設定到入參物件的屬性名稱
  2. 通過StatementgetGeneratedKeys()方法獲取到自增鍵
  3. 如果自增鍵與屬性個數不相同則跳過,不進行處理了
  4. 否則呼叫assignKeys方法,分配自增鍵給對應的屬性

assignKeys方法

/**
* 關於 ParamMap,可以看到 {@link org.apache.ibatis.reflection.ParamNameResolver#getNamedParams} 這個方法
* 根據入參獲取獲取引數名稱與引數值的對映關係
* 如果為多個入參或者一個入參並且新增了 @Param 註解,則會返回 ParamMap 物件,key為引數名稱,value為引數值
*
* 需要使用到獲取自增鍵時,我們一般入參都是一個實體類,則進入的是下面第3種情況
*/
@SuppressWarnings("unchecked")
private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties,
  Object parameter) throws SQLException {
    if (parameter instanceof ParamMap || parameter instanceof StrictMap) { // <1>
  		// 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) { // <2>
  		// Multi-param or single param with @Param in batch operation
  		assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, ((ArrayList<ParamMap<?>>) parameter));
	} else { // <3>
  		// Single param without @Param
  		assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
	}
}
  • 因為我的入參通常是一個實體類的時候,配置自增鍵的生成,這裡我們直接看第三種情況,其他兩種可參考我的註釋進行閱讀

assignKeysToParam方法

private void assignKeysToParam(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd,
  String[] keyProperties, Object parameter) throws SQLException {
    // 將入參物件 parameter 轉換成集合,因為批處理時可能傳入多個入參物件
    Collection<?> params = collectionize(parameter);
    if (params.isEmpty()) {
      return;
    }
    List<KeyAssigner> assignerList = new ArrayList<>();
    for (int i = 0; i < keyProperties.length; i++) {
      // 每一個 keyProperty 都建立一個 KeyAssigner 物件,設定 column 的位置
      assignerList.add(new KeyAssigner(configuration, rsmd, i + 1, null, keyProperties[i]));
    }
    Iterator<?> iterator = params.iterator();
    while (rs.next()) {
      if (!iterator.hasNext()) {
        throw new ExecutorException(String.format(MSG_TOO_MANY_KEYS, params.size()));
      }
      Object param = iterator.next();
      // 往每個入參物件中設定配置的 keyProperty 為對應的自增鍵
      assignerList.forEach(x -> x.assign(rs, param));
    }
}
  1. 將入參物件parameter轉換成集合,因為批處理時可能傳入多個入參物件
  2. 每一個keyProperty都建立一個KeyAssigner物件,設定自增鍵的位置,通過該物件往入參中設定該屬性值
  3. 遍歷入參params集合,每個入參物件都遍歷assignerList集合,通過KeyAssigner往入參中設定屬性值

KeyAssigner

定義在Jdbc3KeyGenerator的一個內部類,單個自增鍵的分配者,將該自增鍵設定到入參物件的屬性中,構造方法如下:

private class KeyAssigner {
    /**
     * 全域性配置物件
     */
    private final Configuration configuration;
    /**
     * Statement 執行後返回的 後設資料物件
     */
    private final ResultSetMetaData rsmd;
    /**
     * 型別處理器註冊中心
     */
    private final TypeHandlerRegistry typeHandlerRegistry;
    /**
     * 屬性對應列所在的位置
     */
    private final int columnPosition;
    /**
     * 引數名稱,新增了 @Param 註解時才有
     */
    private final String paramName;
    /**
     * Java 屬性名稱
     */
    private final String propertyName;
    /**
     * 型別處理器
     */
    private TypeHandler<?> typeHandler;

    protected KeyAssigner(Configuration configuration, ResultSetMetaData rsmd, int columnPosition, String paramName,
        String propertyName) {
      super();
      this.configuration = configuration;
      this.rsmd = rsmd;
      this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
      this.columnPosition = columnPosition;
      this.paramName = paramName;
      this.propertyName = propertyName;
    }
}
  • 定義了結果後設資料ResultSetMetaData物件、自增鍵所在位置、屬性名稱、型別處理器
assign方法

assign(ResultSet rs, Object param)方法,將當前自增鍵設定到param入參的屬性中,程式碼如下:

protected void assign(ResultSet rs, Object param) {
  if (paramName != null) {
    // If paramName is set, param is ParamMap
    param = ((ParamMap<?>) param).get(paramName);
  }
  MetaObject metaParam = configuration.newMetaObject(param);
  try {
    if (typeHandler == null) {
      if (metaParam.hasSetter(propertyName)) {
        Class<?> propertyType = metaParam.getSetterType(propertyName);
        // 根據 Java Type 和 Jdbc Type 獲取對應的型別處理器
        typeHandler = typeHandlerRegistry.getTypeHandler(propertyType, JdbcType.forCode(rsmd.getColumnType(columnPosition)));
      } else {
        throw new ExecutorException("No setter found for the keyProperty '" + propertyName + "' in '"
            + metaParam.getOriginalObject().getClass().getName() + "'.");
      }
    }
    if (typeHandler == null) {
      // Error?
    } else {
      // 將 Jdbc Type 轉換成 Java Type
      Object value = typeHandler.getResult(rs, columnPosition);
      // 將該屬性值設定到 入參物件中
      metaParam.setValue(propertyName, value);
    }
  } catch (SQLException e) {
    throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e,
        e);
  }
}
  • 從結果集中根據位置獲取到該自增鍵,然後設定到入參物件的屬性中

SelectKeyGenerator

org.apache.ibatis.executor.keygen.SelectKeyGenerator:實現KeyGenerator介面,執行資料庫查詢操作,獲取到對應的返回結果設定到入參的屬性中,新增了<selectKey />標籤則會建立該物件,前後置處理可以配置,在《MyBatis初始化(二)之載入Mapper介面與XML對映檔案》XMLStatementBuilder小節的parseStatementNode方法中的第7步看到

實現方法:

public class SelectKeyGenerator implements KeyGenerator {

  /**
   * <selectKey /> 解析成 MappedStatement 物件的 id 的字尾
   * 例如 <selectKey /> 所在的 MappedStatement 的 id 為 namespace.test
   * 那麼它的 id 就是 namespace.test!selectKey
   */
  public static final String SELECT_KEY_SUFFIX = "!selectKey";
  /**
   * 是否在 SQL 執行後執行
   */
  private final boolean executeBefore;
  /**
   * <selectKey /> 解析成 MappedStatement 物件
   */
  private final MappedStatement keyStatement;

  @Override
  public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    if (executeBefore) { // 如果是在 SQL 執行之前進行生成 key
      processGeneratedKeys(executor, ms, parameter);
    }
  }

  @Override
  public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
    if (!executeBefore) { // 如果是在 SQL 執行之後進行生成 key
      processGeneratedKeys(executor, ms, parameter);
    }
  }
}
  • 可以看到<selectKey />標籤還會生成一個MappedStatement物件,用於執行查詢語句

  • executeBefore屬性表示,是否為前置處理,所以<selectKey />要麼就是前置處理,要麼就是後置處理,都是呼叫processGeneratedKeys方法

processGeneratedKeys方法

執行資料庫的查詢操作,生成“主鍵”,設定到入參物件的屬性中,程式碼如下:

private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {
    try {
      if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {
        // <1> 獲取 keyProperty 配置
        String[] keyProperties = keyStatement.getKeyProperties();
        final Configuration configuration = ms.getConfiguration();
        // <2> 建立入參 MetaObject 物件
        final MetaObject metaParam = configuration.newMetaObject(parameter);
        if (keyProperties != null) {
          // Do not close keyExecutor.
          // The transaction will be closed by parent executor.
          // <3> 建立一個 SimpleExecutor 執行器
          Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
          // <4> 執行資料庫查詢
          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 {
            // <5> 為資料庫查詢的到資料建立 MetaObject 物件
            MetaObject metaResult = configuration.newMetaObject(values.get(0));
            if (keyProperties.length == 1) { // <6.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 { //<6.2> 多個屬性
              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);
    }
}

private void setValue(MetaObject metaParam, String property, Object value) {
	if (metaParam.hasSetter(property)) {
 	 	metaParam.setValue(property, value);
	} else {
  		throw new ExecutorException("省略...");
	}
}
  1. 獲取keyProperty配置,也就是需要設定值到入參中的屬性名稱
  2. 建立入參的MetaObject物件metaParam,便於操作
  3. 建立一個SimpleExecutor執行器
  4. 使用該執行器執行資料庫查詢操作
  5. 為資料庫查詢獲取到的結果建立MetaObject物件metaResult
  6. 將查詢結果設定到入參物件中
    1. 如果keyProperty配置的僅僅是一個屬性,則從metaResult中獲取查詢結果設定到metaParam入參物件的該屬性中
    2. 如果多個屬性需要賦值,則呼叫handleMultipleProperties方法將查詢結果設定到入參物件的多個屬性中

handleMultipleProperties方法

private void handleMultipleProperties(String[] keyProperties, MetaObject metaParam, MetaObject metaResult) {
    // 獲取 keyColumn 配置
    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]));
      }
    }
}
  1. 沒有配置列名,則根據keyProperty屬性名稱從metaResult查詢結果中獲取結果,一個一個設定到metaParam入參物件的該屬性中
  2. 沒有配置列名,則根據keyColumn列名從metaResult查詢結果中獲取結果,一個一個設定到metaParam入參物件的該屬性中

NoKeyGenerator

org.apache.ibatis.executor.keygen.NoKeyGenerator:實現KeyGenerator介面,空實現,一個單例,沒有配置上面的兩種方式,則預設為該物件,程式碼如下:

public class NoKeyGenerator implements KeyGenerator {
  /**
   * A shared instance.
   * @since 3.4.3
   */
  public static final NoKeyGenerator INSTANCE = new NoKeyGenerator();

  @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) {
    // Do Nothing
  }
}

總結

本文分析了MyBatis在執行SQL的過程中,SimpleExecutor(預設型別)執行器需要通過PrepareStatementHandler(預設)來執行資料庫的操作,建立PrepareStatement(預設)物件來完成資料操作

如果你配置了useGeneratedKeys="true",則需要在執行完資料庫更新操作後,通過Jdbc3KeyGenerator設定自增鍵到入參物件中(後置處理)

如果你新增了<selectKey />標籤,則需要通過SelectKeyGenerator執行資料庫查詢操作獲取到結果,設定到入參物件中(前置處理、後置處理)

如果是查詢操作則需要通過ResultSetHandler對結果集進行對映轉換成Java物件,這就是我們下一篇文件需要分析的內容?

參考文章:芋道原始碼《精盡 MyBatis 原始碼分析》

相關文章