淺談Mybatis SqlSession執行流程

Mybatis執行SQL流程

在看源碼之前,我們需要瞭解一些基本知識,如果您沒有閱讀Mybatis SqlSessionFactory 初始化原理,可以先閱讀Mybatis SqlSessionFactory 初始化原理這篇文章,這用更有助於我們理解接下來的文章
在看源碼之前,我們需要瞭解一些基本知識

SqlSession

SqlSession是一個接口,它有兩個實現類:
 – DefaultSqlSession:默認實現類
 – SqlSessionManager:已經棄用的實現類,所以我們不需要關註他
SqlSession是與數據庫交互的頂層類,通常與ThreadLocal綁定,一個會話使用一個SqlSession,SqlSession是線程不安全的,使用完畢需要close()

public class DefaultSqlSession implements SqlSession {
 private final Configuration configuration;
 private final Executor executor;
}

SqlSession中最重要的兩個變量:
 – Configuration:核心配置類,也是初始化時傳過來的
 – Executor:實際執行SQL的執行器

Executor

Executor是一個接口,有三個實現類
 – BatchExecutor 重用語句,並執行批量更新
 – ReuseExecutor 重用預處理語句prepared statements
 – SimpleExecutor 普通的執行器,默認使用

瞭解完基本知識後,我們接著往下看代碼。

當創建完SqlSessionFactory後,就可以創建SqlSession,然後使用SqlSession進行增刪改查:

// 1. 讀取配置文件,讀成字節輸入流,註意:現在還沒解析
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
// 2. 解析配置文件,封裝Configuration對象   創建DefaultSqlSessionFactory對象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

SqlSession sqlSession = sqlSessionFactory.openSession();
List<Object> objects = sqlSession.selectList("namespace.id");

我們先去看openSession()方法,創建瞭SqlSession

//6. 進入openSession方法
@Override
public SqlSession openSession() {
    //getDefaultExecutorType()傳遞的是SimpleExecutor
    // level:數據庫事物級別,null
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

//7. 進入openSessionFromDataSource。
//ExecutorType 為Executor的類型,TransactionIsolationLevel為事務隔離級別,autoCommit是否開啟事務
//openSession的多個重載方法可以指定獲得的SeqSession的Executor類型和事務的處理
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        // 獲得 Environment 對象
        final Environment environment = configuration.getEnvironment();
        // 創建 Transaction 對象
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        // 創建 Executor 對象
        final Executor executor = configuration.newExecutor(tx, execType);
        // 創建 DefaultSqlSession 對象
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        // 如果發生異常,則關閉 Transaction 對象
        closeTransaction(tx); // may have fetched a connection so lets call close()
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

通過源碼可以清晰的看到,會話工廠創建瞭Environment,Transaction,Executor,DefaultSqlSession對象,並且對於會話對象來說,他的autoCommit默認為false,默認不自動提交。

然後我回到原來的代碼,接著就需要使用SqlSession進行增刪改查操作瞭

所以我們進入selectList()查看

//8.進入selectList方法,多個重載方法
@Override
public <E> List<E> selectList(String statement) {
    return this.selectList(statement, null);
}

@Override
public <E> List<E> selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        // 獲得 MappedStatement 對象
        MappedStatement ms = configuration.getMappedStatement(statement);
        // 執行查詢
        return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

selectList有多個重載方法,進入到最終方法後,我們可以看到它做瞭兩件事

  • 通過statementId,從Configuration中取MappedStatement對象,就是存放瞭sql語句,返回值類型,輸入值類型的對象
  • 然後委派Executor執行器去執行具體的增刪改查方法

所以,對於實際JDBC的操作,我們還需要進入Executor中查看

Mybatis之Executor

我們繼續從剛剛selectList源碼中,進入Executor查看

return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

//此方法在SimpleExecutor的父類BaseExecutor中實現
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //根據傳入的參數動態獲得SQL語句,最後返回用BoundSql對象表示
    BoundSql boundSql = ms.getBoundSql(parameter);
    //為本次查詢創建緩存的Key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 查詢
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

拆分成瞭三大步:

(1)先調用MappedStatement的getBoundSql方法,獲取解析後的SQL語句,解析工作是由SqlSourceBuilder完成的

什麼叫解析後的SQL語句呢?因為Mybatis編寫SQL語句時,會用到動態SQL,比如#{}占位符,這種占位符JDBC是不認識的,所以需要將其轉換成?占位符,並且將其內部的字段名存儲起來,後面填充參數的時候好使用反射獲取值。

/**
 * 執行解析原始 SQL ,成為 SqlSource 對象
 *
 * @param originalSql 原始 SQL
 * @param parameterType 參數類型
 * @param additionalParameters 附加參數集合。可能是空集合,也可能是 {@link org.apache.ibatis.scripting.xmltags.DynamicContext#bindings} 集合
 * @return SqlSource 對象
 */
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    // 創建 ParameterMappingTokenHandler 對象
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    // 創建 GenericTokenParser 對象
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    // 執行解析
    String sql = parser.parse(originalSql);
    // 創建 StaticSqlSource 對象
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

上面代碼就可以看到,會將拆分#{和},進行解析

(2)根據查詢條件,創建緩存key,用來接下來去緩存查找是否有已經執行過的結果

(3)調用重載query()方法

接著我們進入重載方法查看:

@Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        // 已經關閉,則拋出 ExecutorException 異常
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        // 清空本地緩存,如果 queryStack 為零,並且要求清空本地緩存。
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            clearLocalCache();
        }
        List<E> list;
        try {
            // queryStack + 1
            queryStack++;
            // 從一級緩存中,獲取查詢結果
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
            // 獲取到,則進行處理
            if (list != null) {
                handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            // 獲得不到,則從數據庫中查詢
            } else {
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            // queryStack - 1
            queryStack--;
        }
        if (queryStack == 0) {
            // 執行延遲加載
            for (DeferredLoad deferredLoad : deferredLoads) {
                deferredLoad.load();
            }
            // issue #601
            // 清空 deferredLoads
            deferredLoads.clear();
            // 如果緩存級別是 LocalCacheScope.STATEMENT ,則進行清理
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                // issue #482
                clearLocalCache();
            }
        }
        return list;
    }

主要的邏輯:

  • 從一級緩存取數據,如果有直接使用緩存的進行接下來的操作
  • 如果沒有,從數據庫查詢

進入queryFromDatabase()方法:

// 從數據庫中讀取操作
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 在緩存中,添加占位對象。此處的占位符,和延遲加載有關,可見 `DeferredLoad#canLoad()` 方法
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        // 執行讀操作
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        // 從緩存中,移除占位對象
        localCache.removeObject(key);
    }
    // 添加到緩存中
    localCache.putObject(key, list);
    // 暫時忽略,存儲過程相關
    if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    return list;
}

@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
        Configuration configuration = ms.getConfiguration();
        // 傳入參數創建StatementHanlder對象來執行查詢
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        // 創建jdbc中的statement對象
        stmt = prepareStatement(handler, ms.getStatementLog());
        // 執行 StatementHandler  ,進行讀操作
        return handler.query(stmt, resultHandler);
    } finally {
        // 關閉 StatementHandler 對象
        closeStatement(stmt);
    }
}

通過代碼可以看到,對於實際與JDBC交互的代碼,Executor也懶得搞,又像SqlSession一樣,委派給小弟StatementHandler瞭。

Mybatis之StatementHandler

我們從剛剛的Executor的代碼查看

@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
        Configuration configuration = ms.getConfiguration();
        // 傳入參數創建StatementHanlder對象來執行查詢
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        // 創建jdbc中的statement對象
        stmt = prepareStatement(handler, ms.getStatementLog());
        // 執行 StatementHandler  ,進行讀操作
        return handler.query(stmt, resultHandler);
    } finally {
        // 關閉 StatementHandler 對象
        closeStatement(stmt);
    }
}

可以看到,這裡創建完StatementHandler後,回調用prepareStatement()方法,用來創建Statement對象

我們進入prepareStatement方法中查看

// 初始化 StatementHandler 對象
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    // 獲得 Connection 對象
    Connection connection = getConnection(statementLog);
    // 創建 Statement 或 PrepareStatement 對象
    stmt = handler.prepare(connection, transaction.getTimeout());
    // 設置 SQL 上的參數,例如 PrepareStatement 對象上的占位符
    handler.parameterize(stmt);
    return stmt;
}

@Override
public void parameterize(Statement statement) throws SQLException {
    //使用ParameterHandler對象來完成對Statement的設值
    parameterHandler.setParameters((PreparedStatement) statement);
}

這裡可以看到,它實際是使用ParameterHandler來設置Statement的參數

@Override
public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    // 遍歷 ParameterMapping 數組
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
        for (int i = 0; i < parameterMappings.size(); i++) {
            // 獲得 ParameterMapping 對象
            ParameterMapping parameterMapping = parameterMappings.get(i);
            if (parameterMapping.getMode() != ParameterMode.OUT) {
                // 獲得值
                Object value;
                String propertyName = parameterMapping.getProperty();
                if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                    value = boundSql.getAdditionalParameter(propertyName);
                } else if (parameterObject == null) {
                    value = null;
                } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    value = parameterObject;
                } else {
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    value = metaObject.getValue(propertyName);
                }
                // 獲得 typeHandler、jdbcType 屬性
                TypeHandler typeHandler = parameterMapping.getTypeHandler();
                JdbcType jdbcType = parameterMapping.getJdbcType();
                if (value == null && jdbcType == null) {
                    jdbcType = configuration.getJdbcTypeForNull();
                }
                // 設置 ? 占位符的參數
                try {
                    typeHandler.setParameter(ps, i + 1, value, jdbcType);
                } catch (TypeException | SQLException e) {
                    throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
                }
            }
        }
    }
}

這段代碼的主要目的,就是獲取入參,然後根據值,來設置?占位符的參數

TypeHandler是具體進行參數設置的對象

所以handler.prepare(connection, transaction.getTimeout());方法,就是使用ParameterHandler來對占位符位置的參數進行值設置

然後我們回到Executor,查看handler.query()方法

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

代碼很簡單,這裡直接使用JDBC的PreparedStatement來進行SQL執行,然後使用ResultSetHandler進行結果數據封裝處理。

進入ResultSetHandler

@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

    // 多 ResultSet 的結果集合,每個 ResultSet 對應一個 Object 對象。而實際上,每個 Object 是 List<Object> 對象。
    // 在不考慮存儲過程的多 ResultSet 的情況,普通的查詢,實際就一個 ResultSet ,也就是說,multipleResults 最多就一個元素。
    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    // 獲得首個 ResultSet 對象,並封裝成 ResultSetWrapper 對象
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    // 獲得 ResultMap 數組
    // 在不考慮存儲過程的多 ResultSet 的情況,普通的查詢,實際就一個 ResultSet ,也就是說,resultMaps 就一個元素。
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount); // 校驗
    while (rsw != null && resultMapCount > resultSetCount) {
        // 獲得 ResultMap 對象
        ResultMap resultMap = resultMaps.get(resultSetCount);
        // 處理 ResultSet ,將結果添加到 multipleResults 中
        handleResultSet(rsw, resultMap, multipleResults, null);
        // 獲得下一個 ResultSet 對象,並封裝成 ResultSetWrapper 對象
        rsw = getNextResultSet(stmt);
        // 清理
        cleanUpAfterHandlingResultSet();
        // resultSetCount ++
        resultSetCount++;
    }

    // 因為 `mappedStatement.resultSets` 隻在存儲過程中使用,本系列暫時不考慮,忽略即可
    // ···

    // 如果是 multipleResults 單元素,則取首元素返回
    return collapseSingleResultList(multipleResults);
}

 // 處理 ResultSet ,將結果添加到 multipleResults 中
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
        // 暫時忽略,因為隻有存儲過程的情況,調用該方法,parentMapping 為非空
        if (parentMapping != null) {
            handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
        } else {
            // 如果沒有自定義的 resultHandler ,則創建默認的 DefaultResultHandler 對象
            if (resultHandler == null) {
                // 創建 DefaultResultHandler 對象
                DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
                // 處理 ResultSet 返回的每一行 Row
                handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
                // 添加 defaultResultHandler 的處理的結果,到 multipleResults 中
                multipleResults.add(defaultResultHandler.getResultList());
            } else {
                // 處理 ResultSet 返回的每一行 Row
                handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
            }
        }
    } finally {
        // issue #228 (close resultsets)
        // 關閉 ResultSet 對象
        closeResultSet(rsw.getResultSet());
    }
}

代碼比較多,實際最重要的代碼就是

// 添加 defaultResultHandler 的處理的結果,到 multipleResults 中
multipleResults.add(defaultResultHandler.getResultList());

將處理後的結果封裝到集合中返回,這樣基本Mybatis邏輯就走完瞭.

我們來回顧一下,都用到瞭哪些類

簡單總結
SqlSessionFactoryBuilder:

  • 解析核心配置文件,創建Configuration
    • XMLConfigBuilder.parse():解析核心配置文件
    • XMLMapperBuilder.parse():解析映射配置文件MappedStatement
  • 創建SqlSessionFactory,默認創建DefaultSqlSessionFactory

SqlSessionFactory:

  • openSession():構建Executor,SqlSession等

SqlSession:

  • 根據statementId獲取MappedStatement
  • 委派給Executor執行器執行

Executor:

  • 使用SqlSourceBuilder,將SQL解析成JDBC認可的
  • 查詢緩存,是否存在結果
  • 結果不存在,委派給StatementHandler處理器

StatementHandler:

  • PreparedStatement:處理參數,將參數賦值到占位符上
    • TypeHandler:具體設置值的類
  • ResultSetHandler:封裝結果集,封裝成設置的返回值類型
    • TypeHandler:根據結果集,取出對應列

到此這篇關於淺談Mybatis SqlSession執行流程的文章就介紹到這瞭,更多相關Mybatis SqlSession執行流程內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: