sqlSession讀寫資料庫完全解析
1 引言和主要類
建立完sqlSession例項後,我們就可以進行資料庫操作了。比如通過selectOne()方法查詢資料庫,如程式碼
// 讀取XML配置檔案
String resource = "main/resources/SqlMapConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 建立sqlSessionFactory單例,初始化mybatis容器
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 建立sqlSession例項,用它來進行資料庫操作,mybatis執行時的門面
SqlSession session = sessionFactory.openSession();
// 進行資料庫查詢操作
User user = session.selectOne("test.findUserById", 1); // 訪問資料庫,statement為mapper.xml中的id
建立sqlSessionFactory單例和sqlSession例項在前兩節中分析過了,下面我們著重來分析sqlSession操作資料庫的過程。
sqlSession操作資料庫有兩種方法,可以直接使用select update insert delete等方法;也可以通過getMapper()先獲取mapper動態代理例項,然後再進行資料庫操作。相比而言mapper方式更靈活且不易出錯,是mybatis推薦的方式。本節我們分析直接使用select等方法的流程,下一節再分析mapper方式。
本節以selectOne()方法的實現過程為例來分析sqlSession操作資料庫的流程,涉及的主要類如下。
- DefaultSqlSession:SqlSession的預設實現,其方法基本都是利用Executor代理實現。
- Executor:mybatis執行的核心,排程器。排程mybatis其他三大元件的執行。
- StatementHandler:SQL語句執行器,cache的管理等
- ParameterHandler:入參處理器,statementType為PREPARE是需要使用到它,來解析入參到preparedStatement中
- ResultSetHandler:結果集對映處理器,將資料庫操作原始結果(主要是查詢操作),對映為Java POJO。這正是ORM要解決的關鍵問題。
2 流程
2.1 DefaultSqlSession的selectOne()
先從DefaultSqlSession的selectOne()方法看起。
public <T> T selectOne(String statement, Object parameter) {
// selectOne本質上是呼叫selectList實現,如果結果集大於一個,則報TooManyResultsException。
List<T> list = this.<T>selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
// 由sql語句的標示statement和入參parameter,查詢滿足條件的資料列表
// @Param statement: mapper.xml中mapper節點下的select delete update insert等子節點的id屬性
// @Param parameter: 傳入sql語句的入參
// @Param rowBounds: 邏輯分頁,包含offset和limit兩個主要成員變數。mybatis分頁邏輯為捨棄offset之前條目,取剩下的limit條。預設DEFAULT不分頁
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 從mappers節點初始化階段建立好的mappedStatements這個Map中,找到key為當前要找到的sql的id的那條
MappedStatement ms = configuration.getMappedStatement(statement);
// 通過執行器Executor作為總排程來執行查詢語句,後面以BaseExecutor來分析。
// BatchExecutor ReuseExecutor SimpleExecutor均繼承了BaseExecutor
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();
}
}
selectOne()方法其實是通過呼叫selectList()實現的,因為二者本質是完全相同的,只是前者返回一個物件,而後者為列表List而已。selectList()採用代理模式,使用排程器Executor的query()方法實現。上一節著重講過Executor是SqlSession各個方法的具體實現,是mybatis執行的核心,通過排程StatementHandler ParameterHandler ResultSetHandler三個元件來完成sqlSession操作資料庫的整個過程。Executor的實現類有SimpleExecutor ReuseExecutor BatchExecutor等,它們的基類都是BaseExecutor。下面來分析BaseExecutor的query
2.2 BaseExecutor的query() 排程器開始資料庫query
// BaseExecutor的查詢方法
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 從MappedStatement中找到boundSql成員變數,前面SqlSessionFactory建立部分講到過Mapper解析時的三大元件:MappedStatement SqlSource BoundSql
// 其中BoundSql通過sql執行語句和入參,來組裝最終查詢資料庫用到的sql。
BoundSql boundSql = ms.getBoundSql(parameter);
// 建立CacheKey,用作快取的key,不用深入理解。
// sql的id,邏輯分頁rowBounds的offset和limit,boundSql的sql語句均相同時(主要是動態sql的存在),也就是組裝後的SQL語句完全相同時,才認為是同一個cacheKey
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 真正的查詢語句執行處,關鍵程式碼
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
// 查詢方法
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());
// 排程器已經close了則報錯
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// flush cache, 即寫入並清空cache。之後就只能從資料庫中讀取了,這樣可以防止髒cache
// localCache和localOutputParameterCache為BaseExecutor的成員變數,它們構成了mybatis的一級快取,也就是sqlSession級別的快取,預設是開啟的。
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
// 從快取或資料庫中查詢結果list
List<E> list;
try {
// queryStack用來記錄當前有幾條同樣的查詢語句在同時執行,也就是併發
queryStack++;
// 未定義resultHandler時,先嚐試從快取中取。
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--;
}
// 當前所有查詢語句都結束時,開始處理延遲載入。從快取中取出執行結果,因為前面已經有過本查詢語句了。
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
// 延遲載入從快取中獲取結果
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// statement級別的快取,只快取id相同的sql。當所有查詢語句和延遲載入的查詢語句均執行完畢後,可清空cache。這樣可節約記憶體
clearLocalCache();
}
}
return list;
}
排程器的query方法先從MappedStatement中獲取BoundSql,它包含了sql語句和入參物件等變數,再構造快取的key,即cacheKey。然後先嚐試從快取中取,快取未命中則直接從資料庫中查詢。最後處理延遲載入,直接從快取中取出查詢資料即可。下面我們著重分析直接從資料庫中查詢的過程,也即queryFromDatabase()方法。
// 直接從資料庫中查詢
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 先利用佔位符將本次查詢設定到本地cache中,個人理解是防止後面延遲載入時cache為空
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 真正的資料庫查詢,後面詳細分析
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 查到了結果後,將前面的佔位符的cache刪掉
localCache.removeObject(key);
}
// 將查詢結果放到本地cache中快取起來
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
doQuery()進行真正的資料庫查詢,它由SimpleExecutor等具體類來實現。我們以SimpleExecutor為例分析。
// 資料庫查詢
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();
// 建立StatementHandler,用來執行sql語句。SimpleExecutor建立的是RoutingStatementHandler。
// 它的是一個門面類,幾乎所有方法都是通過代理來實現。代理則由配置XML settings節點的statementType區分。故僅僅是一個分發和路由。後面詳細分析
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 構造Statement,後面詳細分析
stmt = prepareStatement(handler, ms.getStatementLog());
// 通過語句執行器的query方法進行查詢, 查詢結果通過resultHandler處理後返回。後面詳細分析
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
doQuery流程為
- 先建立StatementHandler語句處理器。前面講過StatementHandler是mybatis四大元件之一,負責sql語句的執行。根據XML配置檔案的settings節點的statementType子元素,來建立不同的實現類,如SimpleStatementHandler,PreparedStatementHandler,CallableStatementHandler。他們的基類統一為BaseStatementHandler,外觀類為RoutingStatementHandler(後面詳細分析)。
- 建立完StatementHandler後,呼叫prepareStatement進行初始化,
- 然後呼叫實現類的query方法進行查詢。
2.3 StatementHandler的query(), 語句處理器進行查詢
下面我們來看StatementHandler是如何執行的,先看StatementHandler的建立過程。
2.3.1 StatementHandler的建立過程
// 建立RoutingStatementHandler,它是StatementHandler的外觀類,也是StatementHandler的一個實現類
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 直接構造一個RoutingStatementHandler
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
// 將statementHandler,新增為外掛的目標執行器。外掛通過配置XML檔案的plugins節點設定。
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
// RoutingStatementHandler的構造器,根據statementType變數來建立不同的StatementHandler實現,作為它的代理
// RoutingStatementHandler的幾乎所有方法都是通過這些代理實現的,典型的代理模式。
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 根據statementType建立不同的StatementHandler實現類。statementType是在xml配置檔案的settngs節點的statementType子元素中設定的。
switch (ms.getStatementType()) {
case STATEMENT:
// 直接操作sql,不進行預編譯。此時直接進行字串拼接構造sql String
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());
}
}
2.3.2 StatementHandler的初始化
StatementHandler的初始化如下
// 通過事務構造sql執行語句statement,如JdbcTransaction
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// 開啟資料庫連線,建立Connection物件。JdbcTransaction事務直接通過JDBC建立connection
Connection connection = getConnection(statementLog);
// 初始化statement並設定期相關變數,不同的StatementHandler實現不同。後面以RoutingStatementHandler為例分析
stmt = handler.prepare(connection, transaction.getTimeout());
// 設定parameterHandler,對於SimpleStatementHandler來說不用處理
handler.parameterize(stmt);
return stmt;
}
StatementHandler初始化步驟如下:
- 先開啟一個數據庫連線connection,
- 然後初始化statementHandler,
- 最後進行引數預處理。
先開啟資料庫連線connection,直接獲取資料來源dataSource的connection,即通過資料庫本身來開啟連線。
// JdbcTransaction和ManagedTransaction都是直接呼叫dataSource的getConnection
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
再進行初始化statementHandler,呼叫基類BaseStatementHandler的prepare方法完成
// 初始化statementHandler
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
// 典型的代理模式,不同的statementType建立不同的Statement,但這兒他們都呼叫到他們的基類BaseStatementHandler中的prepare方法
return delegate.prepare(connection, transactionTimeout);
}
// BaseStatementHandler初始化statement並設定期相關變數,不同的StatementHandler實現不同。
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
// 初始化statement,由具體的StatementHandler來實現。比如SimpleStatementHandler通過JDBC connection的createStatement來建立
statement = instantiateStatement(connection);
// 設定timeout(超時時間)和fetchSize(獲取資料庫的行數)
setStatementTimeout(statement, transactionTimeout);
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);
}
}
最後進行引數預處理。不同的statementHandler實現類有不同的引數預處理方式。
- SimpleStatementHandler不進行任何引數預處理,它的sql直接通過字串拼接而成。
- PreparedStatementHandler進行預處理,會將#轉換為?,然後設定對應的變數到sql String中。
// RoutingStatementHandler的parameterize方法,通過代理模式實現
public void parameterize(Statement statement) throws SQLException {
// 又是代理模式,由具體的statementHandler實現類來實現
delegate.parameterize(statement);
}
// SimpleStatementHandler不做引數預處理
public void parameterize(Statement statement) throws SQLException {
// N/A
}
// PreparedStatementHandler進行引數預處理,通過parameterHandler實現
public void parameterize(Statement statement) throws SQLException {
// parameterHandler可以由使用者通過外掛方式實現,mybatis預設為DefaultParameterHandler。這個方法我們不進行詳細分析了。
parameterHandler.setParameters((PreparedStatement) statement);
}
2.3.3 statementHandler的query()進行資料庫查詢
建立和初始化statementHandler後,就可以呼叫它的query()方法來執行語句查詢了。先看SimpleStatementHandler的query過程。
// SimpleStatementHandler的query操作過程
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
// 獲取存放在boundSql中的sql執行語句
String sql = boundSql.getSql();
// 通過JDBC sql的statement,直接執行sql語句。入參在statement預編譯時進行了轉換並設定到statement中了。
statement.execute(sql);
// resultSetHandler處理查詢結果,並返回。這一部分十分複雜,但也體現了mybatis的設計精巧之處,可以相容很多複雜場景下的資料庫結果轉換。如資料庫列名和Java POJO屬性名不同時的對映,關聯資料庫的對映等。
return resultSetHandler.<E>handleResultSets(statement);
}
PrepareStatementHandler的query操作過程如下
// PrepareStatementHandler的query操作過程
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
// PREPARE方式下,sql statement進行了預編譯,並注入了入參。它是一個PreparedStatement型別
PreparedStatement ps = (PreparedStatement) statement;
// 直接呼叫JDBC PreparedStatement的execute方法操作資料庫。大家應該對這兒很熟悉了,JDBC的操作
ps.execute();
// 結果集處理,後面詳細分析
return resultSetHandler.<E> handleResultSets(ps);
}
query先從boundSql中獲取具體執行語句,然後通過JDBC的statement直接執行SQL語句。這兩步完成後,就從資料庫中查詢到了結果集了。從這兒可見,mybatis最底層還是通過JDBC來操作資料庫的。
mybatis對結果集的處理十分複雜,我們下面詳細分析。
2.4 ResultSetHandler處理資料庫結果集
通過JDBC完成資料庫的操作後,我們就拿到了原始的資料庫結果了。此時要將資料庫結果集ResultSet轉換為Java POJO。這一步通過mybatis四大元件之一的ResultSetHandler來實現。ResultSetHandler預設實現類為DefaultResultSetHandler,使用者也可以通過外掛的方式覆蓋它。外掛在xml配置檔案的plugins子節點下新增。下面詳細分析DefaultResultSetHandler的handleResultSets()方法。
// DefaultResultSetHandler通過handleResultSets處理資料庫結果集,處理後作為真正的結果返回。此處的關鍵是處理resultMap對映
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<Object>();
int resultSetCount = 0;
// 1 從JDBC操作資料庫後的statement中取出結果集ResultSet
ResultSetWrapper rsw = getFirstResultSet(stmt);
// 2 獲取resultMaps, mapper.xml中設定,並在mybatis初始化階段存入mappedStatement中。
// resultMap定義了jdbc列到Java屬性的對映關係,可以解決列名和Java屬性名不一致,關聯資料庫對映等諸多問題。
// 它是mybatis中比較複雜的地方,同時也大大擴充套件了mybatis的功能
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
// 3 一條條處理resultSet
while (rsw != null && resultMapCount > resultSetCount) {
// 取出一條resultMap,即結果對映
ResultMap resultMap = resultMaps.get(resultSetCount);
// 進行資料庫列到Java屬性的對映,後面詳細分析
handleResultSet(rsw, resultMap, multipleResults, null);
// 取出下一條resultSet
rsw = getNextResultSet(stmt);
// 清空nestedResultObjects,即巢狀的Result結果集
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
// 4 處理巢狀的resultMap,即對映結果中的某些子屬性也需要resultMap對映時
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
// 取出父ResultMapping,用於巢狀情況
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
// 通過巢狀ResultMap的id,取出ResultMap
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
// 處理ResultSet,後面詳細分析
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
// 5 構造成List,將處理後的結果集返回
return collapseSingleResultList(multipleResults);
}
handleResultSets流程如下
- 從JDBC操作資料庫後的statement中取出結果集ResultSet
- 獲取resultMaps, 它們定義了資料庫結果集到Java POJO的對映關係
- 一條條處理resultSet,呼叫handleResultSet做資料庫列到Java屬性的對映
- 處理巢狀的resultMap,即對映結果中的某些子屬性也需要resultMap對映時
- 構造成List,將處理後的結果集返回
這其中的關鍵是handleResultSet()方法進行資料庫列到Java屬性的對映,也是ORM關鍵所在。我們接著分析。
// 通過resultMap對映,處理資料庫結果集resultSet
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
if (parentMapping != null) {
// parentMapping不為空,表示處理的是巢狀resultMap中的子resultMap。handleRowValues後面詳細分析
handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
} else {
// 非巢狀resultMap
if (resultHandler == null) {
// 使用者沒有自定義resultHandler時,採用DefaultResultHandler。並將最終的處理結果新增到multipleResults中
DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
multipleResults.add(defaultResultHandler.getResultList());
} else {
// 使用者定義了resultHandler時,採用使用者自定義的resultHandler
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
// issue #228 (close resultsets)
closeResultSet(rsw.getResultSet());
}
}
handleResultSet針對巢狀resultMap和非巢狀resultMap做了分別處理,但都是呼叫的handleRowValues()方法,接著看。
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
if (resultMap.hasNestedResultMaps()) {
// 有巢狀resultMap時
ensureNoRowBounds();
checkResultHandler();
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
} else {
// 無巢狀resultMap時
handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
巢狀resultMap的處理比較麻煩,這兒不分析了,我們看非巢狀的,即handleRowValuesForSimpleResultMap。
// 非巢狀ResultMap的處理方法。根據resultMap一行行處理資料庫結果集到Java屬性的對映
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
// mybatis的邏輯分頁規則為跳過rowBounds中offset之前的部分,取limit行數的資料
// skipRows方法會跳過rowBounds中offset之前的部分。
skipRows(rsw.getResultSet(), rowBounds);
// 一行行處理資料庫結果集,直到取出的行數等於rowBounds的limit變數(邏輯分頁),或者所有行都取完了。
while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
// 處理resultMap中的discriminator,使用結果值來決定使用哪個結果對映。可以將不同的資料庫結果對映成不同的Java型別。此處不詳細分析了
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
// 處理這一行資料, 得到對映後的Java結果
Object rowValue = getRowValue(rsw, discriminatedResultMap);
// 使用resultHandler處理得到的Java結果,這才是最終返回的Java屬性值。
// 使用者可自定義resultHandler,否則使用DefaultResultHandler。
// 使用者可使用ResultSetHandler外掛來自定義結果處理方式,此處體現了mybatis設計精巧之處
storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
}
}
// 邏輯分頁rowBounds。skipRows方法會跳過rowBounds中offset之前的部分
private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
rs.absolute(rowBounds.getOffset());
}
} else {
for (int i = 0; i < rowBounds.getOffset(); i++) {
rs.next();
}
}
}
// 邏輯分頁rowBounds。shouldProcessMoreRows取limit條資料庫查詢結果
private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) throws SQLException {
return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
}
mybatis的邏輯分頁規則為跳過rowBounds中offset之前的部分,取limit行數的資料。通過skipRows()和shouldProcessMoreRows()兩個方法共同完成這個功能。遍歷resultSet,通過getRowValue()方法處理一行行資料。最後呼叫resultHandler來處理轉換後的結果。
storeObject結果處理的程式碼如下
// 利用resultHandler處理經過resultMap對映的Java結果
private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException {
if (parentMapping != null) {
// 巢狀的resultMap,也就是子resultSet結果。連結到父resultSet中,由父resultMap一起處理。不詳細分析了
linkToParents(rs, parentMapping, rowValue);
} else {
// 不是巢狀時,直接呼叫resultHandler進行最後的處理,後面詳細看
callResultHandler(resultHandler, resultContext, rowValue);
}
}
private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {
// 構建resultContext上下文,然後利用resultHandler處理。後面以DefaultResultHandler來分析
resultContext.nextResultObject(rowValue);
((ResultHandler<Object>) resultHandler).handleResult(resultContext);
}
// ResultHandler對對映後的結果做最後的處理
public void handleResult(ResultContext<? extends Object> context) {
// DefaultResultHandler對經過resultMap對映後的Java結果不做任何處理,僅僅新增到list中,最後將list返回給selectList()等方法。
list.add(context.getResultObject());
}
3 總結
mybatis操作資料庫的流程,也是它的四大元件Executor StatementHandler ParameterHandler ResultSetHandler的執行過程。其中Executor是排程器,StatementHandler為SQL語句執行器,ParameterHandler為入參執行器,ResultSetHandler為結果集對映執行器。四大元件分層合理,執行流程清晰,都有預設實現,同時使用者也可以利用plugin來覆蓋它。這些無一不體現了mybatis的靈活和設計精巧,值得我們平時設計構架時學習。