示例程式碼
之前的文章說過,對於MyBatis來說insert、update、delete是一組的,因為對於MyBatis來說它們都是update;select是一組的,因為對於MyBatis來說它就是select。
本文研究一下select的實現流程,示例程式碼為:
1 public void testSelectOne() { 2 System.out.println(mailDao.selectMailById(8)); 3 }
selectMailById方法的實現為:
1 public Mail selectMailById(long id) { 2 SqlSession ss = ssf.openSession(); 3 try { 4 return ss.selectOne(NAME_SPACE + "selectMailById", id); 5 } finally { 6 ss.close(); 7 } 8 }
我們知道MyBatis提供的select有selectList和selectOne兩個方法,但是本文只分析且只需要分析selectOne方法,原因後面說。
selectOne方法流程
先看一下SqlSession的selectOne方法流程,方法位於DefaultSqlSession中:
1 public <T> T selectOne(String statement, Object parameter) { 2 // Popular vote was to return null on 0 results and throw exception on too many. 3 List<T> list = this.<T>selectList(statement, parameter); 4 if (list.size() == 1) { 5 return list.get(0); 6 } else if (list.size() > 1) { 7 throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size()); 8 } else { 9 return null; 10 } 11 }
這裡就是為什麼我說selectOne與selectList兩個方法只需要分析selectList方法就可以了的原因,因為在MyBatis中所有selectOne操作最後都會轉換為selectList操作,無非就是操作完畢之後判斷一下結果集的個數,如果結果集個數超過一個就報錯。
接著看下第3行的selectList的程式碼實現,方法同樣位於DefaultSqlSession中:
1 public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { 2 try { 3 MappedStatement ms = configuration.getMappedStatement(statement); 4 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); 5 } catch (Exception e) { 6 throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); 7 } finally { 8 ErrorContext.instance().reset(); 9 } 10 }
第3行獲取MappedStatement就不說了,跟一下第4行Executor的query方法實現,這裡使用了一個裝飾器模式,給SimpleExecutor加上了快取功能,程式碼位於CachingExecutor中:
1 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { 2 BoundSql boundSql = ms.getBoundSql(parameterObject); 3 CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); 4 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); 5 }
第2行的程式碼獲取BoundSql,BoundSql中的內容上文已經說過了,最後也會有總結。
第3行的程式碼根據輸入引數構建快取Key。
第4行的程式碼執行查詢操作,看下程式碼實現,程式碼同樣位於CachingExecutor中:
1 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) 2 throws SQLException { 3 Cache cache = ms.getCache(); 4 if (cache != null) { 5 flushCacheIfRequired(ms); 6 if (ms.isUseCache() && resultHandler == null) { 7 ensureNoOutParams(ms, parameterObject, boundSql); 8 @SuppressWarnings("unchecked") 9 List<E> list = (List<E>) tcm.getObject(cache, key); 10 if (list == null) { 11 list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); 12 tcm.putObject(cache, key, list); // issue #578 and #116 13 } 14 return list; 15 } 16 } 17 return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); 18 }
這裡並沒有配置且引用Cache,因此不執行第4行的判斷,執行第17行的程式碼,程式碼位於SimpleExecutor的父類BaseExecutor中,原始碼實現為:
1 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { 2 ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); 3 if (closed) { 4 throw new ExecutorException("Executor was closed."); 5 } 6 if (queryStack == 0 && ms.isFlushCacheRequired()) { 7 clearLocalCache(); 8 } 9 List<E> list; 10 try { 11 queryStack++; 12 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; 13 if (list != null) { 14 handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); 15 } else { 16 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); 17 } 18 } finally { 19 queryStack--; 20 } 21 if (queryStack == 0) { 22 for (DeferredLoad deferredLoad : deferredLoads) { 23 deferredLoad.load(); 24 } 25 // issue #601 26 deferredLoads.clear(); 27 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { 28 // issue #482 29 clearLocalCache(); 30 } 31 } 32 return list; 33 }
這裡執行第16行的程式碼,queryFromDatabase方法實現為:
1 private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { 2 List<E> list; 3 localCache.putObject(key, EXECUTION_PLACEHOLDER); 4 try { 5 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); 6 } finally { 7 localCache.removeObject(key); 8 } 9 localCache.putObject(key, list); 10 if (ms.getStatementType() == StatementType.CALLABLE) { 11 localOutputParameterCache.putObject(key, parameter); 12 } 13 return list; 14 }
程式碼走到第5行,最終執行duQuery方法,方法的實現為:
1 public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { 2 Statement stmt = null; 3 try { 4 Configuration configuration = ms.getConfiguration(); 5 StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); 6 stmt = prepareStatement(handler, ms.getStatementLog()); 7 return handler.<E>query(stmt, resultHandler); 8 } finally { 9 closeStatement(stmt); 10 } 11 }
看到第4行~第6行的程式碼都和前文update是一樣的,就不說了,handler有印象的朋友應該記得是PreparedStatementHandler,下一部分就分析一下和update的區別,PreparedStatementHandler的query方法是如何實現的。
PreparedStatementHandler的query方法實現
跟一下PreparedStatementHandler的query方法跟到底,其最終實現為:
1 public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { 2 PreparedStatement ps = (PreparedStatement) statement; 3 ps.execute(); 4 return resultSetHandler.<E> handleResultSets(ps); 5 }
看到第3行執行查詢操作,第4行的程式碼處理結果集,將結果集轉換為List,handleResultSets方法實現為:
1 public List<Object> handleResultSets(Statement stmt) throws SQLException { 2 ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); 3 4 final List<Object> multipleResults = new ArrayList<Object>(); 5 6 int resultSetCount = 0; 7 ResultSetWrapper rsw = getFirstResultSet(stmt); 8 9 List<ResultMap> resultMaps = mappedStatement.getResultMaps(); 10 int resultMapCount = resultMaps.size(); 11 validateResultMapsCount(rsw, resultMapCount); 12 while (rsw != null && resultMapCount > resultSetCount) { 13 ResultMap resultMap = resultMaps.get(resultSetCount); 14 handleResultSet(rsw, resultMap, multipleResults, null); 15 rsw = getNextResultSet(stmt); 16 cleanUpAfterHandlingResultSet(); 17 resultSetCount++; 18 } 19 20 String[] resultSets = mappedStatement.getResultSets(); 21 if (resultSets != null) { 22 while (rsw != null && resultSetCount < resultSets.length) { 23 ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); 24 if (parentMapping != null) { 25 String nestedResultMapId = parentMapping.getNestedResultMapId(); 26 ResultMap resultMap = configuration.getResultMap(nestedResultMapId); 27 handleResultSet(rsw, resultMap, null, parentMapping); 28 } 29 rsw = getNextResultSet(stmt); 30 cleanUpAfterHandlingResultSet(); 31 resultSetCount++; 32 } 33 } 34 35 return collapseSingleResultList(multipleResults); 36 }
總結一下這個方法。
第7行程式碼,通過PreparedStatement的getResultSet方法獲取ResultSet,並將ResultSet包裝為ResultSetWrapper,ResultSetWrapper除了包含了ResultSet之外,還依次定義了資料庫返回的每條資料的每行列名、列對應的JDBC型別、列對應的Java Class的型別,除此之外最主要的是還包含了TypeHandlerRegister(型別處理器,所有的引數都是通過TypeHandler進行設定的)。
第9行程式碼,獲取該<select>標籤中定義的ResultMap,不過這裡我有點沒弄明白,一個<select>標籤按道理應該只能定義一個resultMap屬性,但是這裡卻獲取的是一個List<ResultMap>,不是很清楚。
第11行程式碼,做了一個校驗,即如果select出來有結果返回,但是沒有ResultMap或者ResultType與之對應的話,丟擲異常,道理很簡單,沒有這2者之一,MyBatis並不知道將返回轉成什麼樣子。
第12行~第18行的程式碼,將ResultSetWrapper中的值根據ResultMap,轉成Java物件,先儲存在multipleResults中,這是一個List<Object>。
第20行~第33行的程式碼,是用於處理<select>中定義的resultSets的,由於這裡沒有定義,因此跳過。
第35行的程式碼,將multipleResults,根據其size大小,如果size=1,獲取0號元素,強轉為List<Object>;如果size!=1,直接返回multipleResults。
總得來說這個方法,根據資料庫返回的結果,封裝為自定義的ResultMap的流程基本是沒問題的,只是這裡的一個問題是,為什麼要定義一個multipleResults,最後根據multipleResults的size來判斷並拆分最終的結果,還沒有完全搞懂,這部分還要留待後面的工作中隨著MyBatis應用的深入再去學習。
小結
前文已經對MyBatis配置檔案載入、CRUD操作都進行了分析,就從我自己的感覺來說,對整個流程基本有數,但是很多地方感覺還是有些印象不深,最主要的就是從什麼地方獲取什麼資料,獲取的資料在什麼地方使用,因此這裡做一個總結加深印象,主要總結的是MyBatis中重點的類中持有哪些內容。
首先是SqlSessionFactory,預設使用的是DefaultSqlSessionFactory,我們使用它來每次開啟一個SqlSession,SqlSessionFactory持有:
接著是Configuration,它是所有配置資訊最終儲存的位置,其中大部分的屬性尤其是布林型值都可以通過<settings>標籤進行配置,任何的操作(如開啟一個SqlSession、執行增刪改查等)都要從Configuration中拿相關資訊,Configuration持有的一些重要屬性有:
接著是Environment,它儲存的是配置的資料庫環境資訊,可以指定多個,但是最終只能使用一個,Environment持有的一些重要屬性有:
接著是MappedStatement,一個MappedStatement對應mapper檔案中的一個<insert>、<delete>、<update>、<select>,每次執行MyBatis操作的時候先獲取對應的MappedStatement,MappedStatement持有的一些重要屬性有:
接著是BoundSql,BoundSql中最重要儲存的就是當前要執行的SQL語句,其餘還有要設定的引數資訊與引數物件,BoundSql持有的屬性有:
最後是ParameterMapping,ParameterMapping是待設定的引數對映,儲存了待設定的引數的相關資訊,ParameterMapping持有的屬性有:
MyBatis中使用到的設計模式
下面來總結一下MyBatis中使用到的設計模式,有些設計模式可能在到目前位置的文章中沒有體現,但是在之後的【MyBatis原始碼分析】系列文章中也會體現,這裡一併先列舉出來:
1、建造者模式
程式碼示例為SqlSessionFactoryBuilder,程式碼片段:
1 public SqlSessionFactory build(Reader reader) { 2 return build(reader, null, null); 3 } 4 5 public SqlSessionFactory build(Reader reader, String environment) { 6 return build(reader, environment, null); 7 } 8 9 public SqlSessionFactory build(Reader reader, Properties properties) { 10 return build(reader, null, properties); 11 } 12 13 public SqlSessionFactory build(Reader reader, String environment, Properties properties) { 14 try { 15 XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); 16 return build(parser.parse()); 17 } catch (Exception e) { 18 throw ExceptionFactory.wrapException("Error building SqlSession.", e); 19 } finally { 20 ErrorContext.instance().reset(); 21 try { 22 reader.close(); 23 } catch (IOException e) { 24 // Intentionally ignore. Prefer previous error. 25 } 26 } 27 }
過載了大量的build方法,可以根據引數的不同構建出不同的SqlSessionFactory。
2、抽象工廠模式
程式碼示例為TransactionFactory,程式碼片段為:
1 public class JdbcTransactionFactory implements TransactionFactory { 2 3 @Override 4 public void setProperties(Properties props) { 5 } 6 7 @Override 8 public Transaction newTransaction(Connection conn) { 9 return new JdbcTransaction(conn); 10 } 11 12 @Override 13 public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) { 14 return new JdbcTransaction(ds, level, autoCommit); 15 } 16 }
抽象出事物工廠,不同的事物型別實現不同的事物工廠,像這裡就是Jdbc事物工廠,通過Jdbc事物工廠去返回事物介面的具體實現。
其它的像DataSourceFactory也是抽象工廠模式的實現。
3、模板模式
程式碼示例為BaseExecutor,程式碼片段:
1 protected abstract int doUpdate(MappedStatement ms, Object parameter) 2 throws SQLException; 3 4 protected abstract List<BatchResult> doFlushStatements(boolean isRollback) 5 throws SQLException; 6 7 protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) 8 throws SQLException; 9 10 protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) 11 throws SQLException;
BaseExecutor封裝好方法流程,子類例如SimpleExecutor去實現。
4、責任鏈模式
程式碼示例為InterceptorChain,程式碼片段為:
1 public class InterceptorChain { 2 3 private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); 4 5 public Object pluginAll(Object target) { 6 for (Interceptor interceptor : interceptors) { 7 target = interceptor.plugin(target); 8 } 9 return target; 10 } 11 12 public void addInterceptor(Interceptor interceptor) { 13 interceptors.add(interceptor); 14 } 15 16 public List<Interceptor> getInterceptors() { 17 return Collections.unmodifiableList(interceptors); 18 } 19 20 }
可以根據需要新增自己的Interceptor,最終按照定義的Interceptor的順序逐一巢狀執行。
5、裝飾器模式
程式碼示例為CachingExecutor,程式碼片段為:
1 public class CachingExecutor implements Executor { 2 3 private Executor delegate; 4 private TransactionalCacheManager tcm = new TransactionalCacheManager(); 5 6 public CachingExecutor(Executor delegate) { 7 this.delegate = delegate; 8 delegate.setExecutorWrapper(this); 9 } 10 11 ... 12 }
給Executor新增上了快取的功能,update與query的時候會根據使用者配置先嚐試操作快取。
在MyBatis中還有很多地方使用到了裝飾器模式,例如StatementHandler、Cache。
6、代理模式
程式碼示例為PooledConnection,程式碼片段為:
1 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 2 String methodName = method.getName(); 3 if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { 4 dataSource.pushConnection(this); 5 return null; 6 } else { 7 try { 8 if (!Object.class.equals(method.getDeclaringClass())) { 9 // issue #579 toString() should never fail 10 // throw an SQLException instead of a Runtime 11 checkConnection(); 12 } 13 return method.invoke(realConnection, args); 14 } catch (Throwable t) { 15 throw ExceptionUtil.unwrapThrowable(t); 16 } 17 } 18 }
這層代理的作用主要是為了讓Connection使用完畢之後從棧中彈出來。
MyBatis中的外掛也是使用代理模式實現的,這個在後面會說到。