Mybatis實現原理深入解析 (轉載)
原理分析之一:從JDBC到Mybatis
1.引言
本文主要講解JDBC怎麼演變到Mybatis的漸變過程,重點講解了爲什麼要將JDBC封裝成Mybaits這樣一個持久層框架。再而論述Mybatis作爲一個數據持久層框架本身有待改進之處。
2.JDBC實現查詢分析
我們先看看我們最熟悉也是最基礎的通過JDBC查詢數據庫數據,一般需要以下七個步驟:
(1) 加載JDBC驅動
(2) 建立並獲取數據庫連接
(3) 創建 JDBC Statements 對象
(4) 設置SQL語句的傳入參數
(5) 執行SQL語句並獲得查詢結果
(6) 對查詢結果進行轉換處理並將處理結果返回
(7) 釋放相關資源(關閉Connection,關閉Statement,關閉ResultSet)
以下是具體的實現代碼:
Java代碼
public static List<Map<String,Object>> queryForList(){
Connection connection = null;
ResultSet rs = null;
PreparedStatement stmt = null;
List<Map<String,Object>> resultList = new ArrayList<Map<String,Object>>();
try {
//加載JDBC驅動
Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();
String url = "jdbc:oracle:thin:@localhost:1521:ORACLEDB";
String user = "trainer";
String password = "trainer";
//獲取數據庫連接
connection = DriverManager.getConnection(url,user,password);
String sql = "select * from userinfo where user_id = ? ";
//創建Statement對象(每一個Statement爲一次數據庫執行請求)
stmt = connection.prepareStatement(sql);
//設置傳入參數
stmt.setString(1, "zhangsan");
//執行SQL語句
rs = stmt.executeQuery();
//處理查詢結果(將查詢結果轉換成List<Map>格式)
ResultSetMetaData rsmd = rs.getMetaData();
int num = rsmd.getColumnCount();
while(rs.next()){
Map map = new HashMap();
for(int i = 0;i < num;i++){
String columnName = rsmd.getColumnName(i+1);
map.put(columnName,rs.getString(columnName));
}
resultList.add(map);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
//關閉結果集
if (rs != null) {
rs.close();
rs = null;
}
//關閉執行
if (stmt != null) {
stmt.close();
stmt = null;
}
if (connection != null) {
connection.close();
connection = null;
}
} catch (SQLException e) {
e.printStackTrace();
}
}
return resultList;
}
3.JDBC演變到Mybatis過程
上面我們看到了實現JDBC有七個步驟,哪些步驟是可以進一步封裝的,減少我們開發的代碼量。
第一步優化:連接獲取和釋放
問題描述:
數據庫連接頻繁的開啓和關閉本身就造成了資源的浪費,影響系統的性能。
解決問題:
數據庫連接的獲取和關閉我們可以使用數據庫連接池來解決資源浪費的問題。通過連接池就可以反覆利用已經建立的連接去訪問數據庫了。減少連接的開啓和關閉的時間。
問題描述:
但是現在連接池多種多樣,可能存在變化,有可能採用DBCP的連接池,也有可能採用容器本身的JNDI數據庫連接池。
解決問題:
我們可以通過DataSource進行隔離解耦,我們統一從DataSource裏面獲取數據庫連接,DataSource具體由DBCP實現還是由容器的JNDI實現都可以,所以我們將DataSource的具體實現通過讓用戶配置來應對變化。
第二步優化:SQL統一存取
問題描述:
我們使用JDBC進行操作數據庫時,SQL語句基本都散落在各個JAVA類中,這樣有三個不足之處:
第一,可讀性很差,不利於維護以及做性能調優。
第二,改動Java代碼需要重新編譯、打包部署。
第三,不利於取出SQL在數據庫客戶端執行(取出後還得刪掉中間的Java代碼,編寫好的SQL語句寫好後還得通過+號在Java進行拼湊)。
解決問題:
我們可以考慮不把SQL語句寫到Java代碼中,那麼把SQL語句放到哪裏呢?首先需要有一個統一存放的地方,我們可以將這些SQL語句統一集中放到配置文件或者數據庫裏面(以key-value的格式存放)。然後通過SQL語句的key值去獲取對應的SQL語句。
既然我們將SQL語句都統一放在配置文件或者數據庫中,那麼這裏就涉及一個SQL語句的加載問題。
第三步優化:傳入參數映射和動態SQL
問題描述:
很多情況下,我們都可以通過在SQL語句中設置佔位符來達到使用傳入參數的目的,這種方式本身就有一定侷限性,它是按照一定順序傳入參數的,要與佔位符一一匹配。但是,如果我們傳入的參數是不確定的(比如列表查詢,根據用戶填寫的查詢條件不同,傳入查詢的參數也是不同的,有時是一個參數、有時可能是三個參數),那麼我們就得在後臺代碼中自己根據請求的傳入參數去拼湊相應的SQL語句,這樣的話還是避免不了在Java代碼裏面寫SQL語句的命運。既然我們已經把SQL語句統一存放在配置文件或者數據庫中了,怎麼做到能夠根據前臺傳入參數的不同,動態生成對應的SQL語句呢?
解決問題:
第一,我們先解決這個動態問題,按照我們正常的程序員思維是,通過if和else這類的判斷來進行是最直觀的,這個時候我們想到了JSTL中的<if test=””></if>這樣的標籤,那麼,能不能將這類的標籤引入到SQL語句中呢?假設可以,那麼我們這裏就需要一個專門的SQL解析器來解析這樣的SQL語句,但是,if判斷的變量來自於哪裏呢?傳入的值本身是可變的,那麼我們得爲這個值定義一個不變的變量名稱,而且這個變量名稱必須和對應的值要有對應關係,可以通過這個變量名稱找到對應的值,這個時候我們想到了key-value的Map。解析的時候根據變量名的具體值來判斷。
假如前面可以判斷沒有問題,那麼假如判斷的結果是true,那麼就需要輸出的標籤裏面的SQL片段,但是怎麼解決在標籤裏面使用變量名稱的問題呢?這裏我們需要使用一種有別於SQL的語法來嵌入變量(比如使用#變量名#)。這樣,SQL語句經過解析後就可以動態的生成符合上下文的SQL語句。
還有,怎麼區分開佔位符變量和非佔位變量?有時候我們單單使用佔位符是滿足不了的,佔位符只能爲查詢條件佔位,SQL語句其他地方使用不了。這裏我們可以使用#變量名#表示佔位符變量,使用$變量名$表示非佔位符變量。
第四步優化:結果映射和結果緩存
問題描述:
執行SQL語句、獲取執行結果、對執行結果進行轉換處理、釋放相關資源是一整套下來的。假如是執行查詢語句,那麼執行SQL語句後,返回的是一個ResultSet結果集,這個時候我們就需要將ResultSet對象的數據取出來,不然等到釋放資源時就取不到這些結果信息了。我們從前面的優化來看,以及將獲取連接、設置傳入參數、執行SQL語句、釋放資源這些都封裝起來了,只剩下結果處理這塊還沒有進行封裝,如果能封裝起來,每個數據庫操作都不用自己寫那麼一大堆Java代碼,直接調用一個封裝的方法就可以搞定了。
解決問題:
我們分析一下,一般對執行結果的有哪些處理,有可能將結果不做任何處理就直接返回,也有可能將結果轉換成一個JavaBean對象返回、一個Map返回、一個List返回等等,結果處理可能是多種多樣的。從這裏看,我們必須告訴SQL處理器兩點:第一,需要返回什麼類型的對象;第二,需要返回的對象的數據結構怎麼跟執行的結果映射,這樣才能將具體的值copy到對應的數據結構上。
接下來,我們可以進而考慮對SQL執行結果的緩存來提升性能。緩存數據都是key-value的格式,那麼這個key怎麼來呢?怎麼保證唯一呢?即使同一條SQL語句幾次訪問的過程中由於傳入參數的不同,得到的執行SQL語句也是不同的。那麼緩存起來的時候是多對。但是SQL語句和傳入參數兩部分合起來可以作爲數據緩存的key值。
第五步優化:解決重複SQL語句問題
問題描述:
由於我們將所有SQL語句都放到配置文件中,這個時候會遇到一個SQL重複的問題,幾個功能的SQL語句其實都差不多,有些可能是SELECT後面那段不同、有些可能是WHERE語句不同。有時候表結構改了,那麼我們就需要改多個地方,不利於維護。
解決問題:
當我們的代碼程序出現重複代碼時怎麼辦?將重複的代碼抽離出來成爲獨立的一個類,然後在各個需要使用的地方進行引用。對於SQL重複的問題,我們也可以採用這種方式,通過將SQL片段模塊化,將重複的SQL片段獨立成一個SQL塊,然後在各個SQL語句引用重複的SQL塊,這樣需要修改時只需要修改一處即可。
4. 優化總結:
我們總結一下上面對JDBC的優化和封裝:
(1) 使用數據庫連接池對連接進行管理
(2) SQL語句統一存放到配置文件
(3) SQL語句變量和傳入參數的映射以及動態SQL
(4) 動態SQL語句的處理
(5) 對數據庫操作結果的映射和結果緩存
(6) SQL語句的重複
5. Mybaits有待改進之處
問題描述:
Mybaits所有的數據庫操作都是基於SQL語句,導致什麼樣的數據庫操作都要寫SQL語句。一個應用系統要寫的SQL語句實在太多了。
改進方法:
我們對數據庫進行的操作大部分都是對錶數據的增刪改查,很多都是對單表的數據進行操作,由這點我們可以想到一個問題:單表操作可不可以不寫SQL語句,通過JavaBean的默認映射器生成對應的SQL語句,比如:一個類UserInfo對應於USER_INFO表, userId屬性對應於USER_ID字段。這樣我們就可以通過反射可以獲取到對應的表結構了,拼湊成對應的SQL語句顯然不是問題。
原理分析之二:框架整體設計
1.引言
本文主要講解Mybatis的整體程序設計,理清楚框架的主要脈絡。後面文章我們再詳細講解各個組件。
2.整體設計
2.1 總體流程
(1)加載配置並初始化
觸發條件:加載配置文件
配置來源於兩個地方,一處是配置文件,一處是Java代碼的註解,將SQL的配置信息加載成爲一個個MappedStatement對象(包括了傳入參數映射配置、執行的SQL語句、結果映射配置),存儲在內存中。
(2)接收調用請求
觸發條件:調用Mybatis提供的API
傳入參數:爲SQL的ID和傳入參數對象
處理過程:將請求傳遞給下層的請求處理層進行處理。
(3)處理操作請求
觸發條件:API接口層傳遞請求過來
傳入參數:爲SQL的ID和傳入參數對象
處理過程:
(A)根據SQL的ID查找對應的MappedStatement對象。
(B)根據傳入參數對象解析MappedStatement對象,得到最終要執行的SQL和執行傳入參數。
(C)獲取數據庫連接,根據得到的最終SQL語句和執行傳入參數到數據庫執行,並得到執行結果。
(D)根據MappedStatement對象中的結果映射配置對得到的執行結果進行轉換處理,並得到最終的處理結果。
(E)釋放連接資源。
(4)返回處理結果
將最終的處理結果返回。
2.2 功能架構設計
功能架構講解:
我們把Mybatis的功能架構分爲三層:
(1)API接口層:提供給外部使用的接口API,開發人員通過這些本地API來操縱數據庫。接口層一接收到調用請求就會調用數據處理層來完成具體的數據處理。
(2)數據處理層:負責具體的SQL查找、SQL解析、SQL執行和執行結果映射處理等。它主要的目的是根據調用的請求完成一次數據庫操作。
(3)基礎支撐層:負責最基礎的功能支撐,包括連接管理、事務管理、配置加載和緩存處理,這些都是共用的東西,將他們抽取出來作爲最基礎的組件。爲上層的數據處理層提供最基礎的支撐。
2.3 框架架構設計
框架架構講解:
(1)加載配置:配置來源於兩個地方,一處是配置文件,一處是Java代碼的註解,將SQL的配置信息加載成爲一個個MappedStatement對象(包括了傳入參數映射配置、執行的SQL語句、結果映射配置),存儲在內存中。
(2)SQL解析:當API接口層接收到調用請求時,會接收到傳入SQL的ID和傳入對象(可以是Map、JavaBean或者基本數據類型),Mybatis會根據SQL的ID找到對應的MappedStatement,然後根據傳入參數對象對MappedStatement進行解析,解析後可以得到最終要執行的SQL語句和參數。
(3) SQL執行:將最終得到的SQL和參數拿到數據庫進行執行,得到操作數據庫的結果。
(4)結果映射:將操作數據庫的結果按照映射的配置進行轉換,可以轉換成HashMap、JavaBean或者基本數據類型,並將最終結果返回。
原理分析之三:初始化(配置文件讀取和解析)
1. 準備工作
編寫測試代碼(具體請參考《Mybatis入門示例》),設置斷點,以Debug模式運行,具體代碼如下:
Java代碼
String resource = "mybatis.cfg.xml";
Reader reader = Resources.getResourceAsReader(resource);
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);
SqlSession session = ssf.openSession();
2.源碼分析
我們此次就對上面的代碼進行跟蹤和分析,let's go。
首先我們按照順序先看看第一行和第二行代碼,看看它主要完成什麼事情:
Java代碼
String resource = "mybatis.cfg.xml";
Reader reader = Resources.getResourceAsReader(resource);
讀取Mybaits的主配置配置文件,並返回該文件的輸入流,我們知道Mybatis所有的SQL語句都寫在XML配置文件裏面,所以第一步就需要讀取這些XML配置文件,這個不難理解,關鍵是讀取文件後怎麼存放。
我們接着看第三行代碼(如下),該代碼主要是讀取配置文件流並將這些配置信息存放到Configuration類中。
Java代碼
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);
Java代碼 SqlSessionFactoryBuilder的build的方法如下:
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
其實是調用該類的另一個build方法來執行的,具體代碼如下:
Java代碼
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
我們重點看一下里面兩行:
Java代碼
//創建一個配置文件流的解析對象XMLConfigBuilder,其實這裏是將環境和配置文件流賦予解析類
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
// 解析類對配置文件進行解析並將解析的內容存放到Configuration對象中,並返回SqlSessionFactory
return build(parser.parse());
這裏的XMLConfigBuilder初始化其實調用的代碼如下:
Java代碼
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
XMLConfigBuilder的parse方法執行代碼如下:
Java代碼
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each MapperConfigParser can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
解析的內容主要是在parseConfiguration方法中,它主要完成的工作是讀取配置文件的各個節點,然後將這些數據映射到內存配置對象Configuration中,我們看一下parseConfiguration方法內容:
Java代碼
private void parseConfiguration(XNode root) {
try {
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
propertiesElement(root.evalNode("properties"));
settingsElement(root.evalNode("settings"));
environmentsElement(root.evalNode("environments"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
最後的build方法其實是傳入配置對象進去,創建DefaultSqlSessionFactory實例出來. DefaultSqlSessionFactory是SqlSessionFactory的默認實現.
Java代碼
<a>public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}</a>
最後我們看一下第四行代碼:
Java代碼
SqlSession session = ssf.openSession();
通過調用DefaultSqlSessionFactory的openSession方法返回一個SqlSession實例,我們看一下具體是怎麼得到一個SqlSession實例的。首先調用openSessionFromDataSource方法。
Java代碼
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
下面我們看一下openSessionFromDataSource方法的邏輯:
Java代碼
<a>private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Connection connection = null;
try {
//獲取配置信息裏面的環境信息,這些環境信息都是包括使用哪種數據庫,連接數據庫的信息,事務
final Environment environment = configuration.getEnvironment();
//根據環境信息關於數據庫的配置獲取數據源
final DataSource dataSource = getDataSourceFromEnvironment(environment);
//根據環境信息關於事務的配置獲取事務工廠
TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
connection = dataSource.getConnection();
if (level != null) {
//設置連接的事務隔離級別
connection.setTransactionIsolation(level.getLevel());
}
//對connection進行包裝,使連接具備日誌功能,這裏用的是代理。
connection = wrapConnection(connection);
//從事務工廠獲取一個事務實例
Transaction tx = transactionFactory.newTransaction(connection, autoCommit);
//從配置信息中獲取一個執行器實例
Executor executor = configuration.newExecutor(tx, execType);
//返回SqlSession的一個默認實例
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeConnection(connection);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}</a>
傳入參數說明:
(1)ExecutorType:執行類型,ExecutorType主要有三種類型:SIMPLE, REUSE, BATCH,默認是SIMPLE,都在枚舉類ExecutorType裏面。
(2)TransactionIsolationLevel:事務隔離級別,都在枚舉類TransactionIsolationLevel中定義。
(3)autoCommit:是否自動提交,主要是事務提交的設置。
DefaultSqlSession是SqlSession的實現類,該類主要提供操作數據庫的方法給開發人員使用。
這裏總結一下上面的過程,總共由三個步驟:
步驟一:讀取Ibatis的主配置文件,並將文件讀成文件流形式(InputStream)。
步驟二:從主配置文件流中讀取文件的各個節點信息並存放到Configuration對象中。讀取mappers節點的引用文件,並將這些文件的各個節點信息存放到Configuration對象。
步驟三:根據Configuration對象的信息獲取數據庫連接,並設置連接的事務隔離級別等信息,將經過包裝數據庫連接對象SqlSession接口返回,DefaultSqlSession是SqlSession的實現類,所以這裏返回的是DefaultSqlSession,SqlSession接口裏面就是對外提供的各種數據庫操作。
原理分析之四:一次SQL查詢的源碼分析
上回我們講到Mybatis加載相關的配置文件進行初始化,這回我們講一下一次SQL查詢怎麼進行的。
準備工作
Mybatis完成一次SQL查詢需要使用的代碼如下:
Java代碼
<a>String resource = "mybatis.cfg.xml";
Reader reader = Resources.getResourceAsReader(resource);
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);
<strong> </strong>SqlSession session = ssf.openSession();
try {
UserInfo user = (UserInfo) session.selectOne("User.selectUser", "1");
System.out.println(user);
} catch (Exception e) {
e.printStackTrace();
} finally {
session.close();
}</a>
本次我們需要進行深入跟蹤分析的是:
Java代碼
SqlSession session = ssf.openSession();
UserInfo user = (UserInfo) session.selectOne("User.selectUser", "1");
源碼分析
第一步:打開一個會話,我們看看裏面具體做了什麼事情。
Java代碼
1
SqlSession session = ssf.openSession();
DefaultSqlSessionFactory的 openSession()方法內容如下:
Java代碼
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
跟進去,我們看一下openSessionFromDataSource方法到底做了啥:
Java代碼
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Connection connection = null;
try {
final Environment environment = configuration.getEnvironment();
final DataSource dataSource = getDataSourceFromEnvironment(environment);
TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
connection = dataSource.getConnection();
if (level != null) {
connection.setTransactionIsolation(level.getLevel());
}
connection = wrapConnection(connection);
Transaction tx = transactionFactory.newTransaction(connection, autoCommit);
Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeConnection(connection);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
這裏我們分析一下這裏所涉及的步驟:
(1)獲取前面我們加載配置文件的環境信息,並且獲取環境信息中配置的數據源。
(2)通過數據源獲取一個連接,對連接進行包裝代理(通過JDK的代理來實現日誌功能)。
(3)設置連接的事務信息(是否自動提交、事務級別),從配置環境中獲取事務工廠,事務工廠獲取一個新的事務。
(4)傳入事務對象獲取一個新的執行器,並傳入執行器、配置信息等獲取一個執行會話對象。
從上面的代碼我們可以得出,一次配置加載只能有且對應一個數據源。對於上述步驟,我們不難理解,我們重點看看新建執行器和DefaultSqlSession。
首先,我們看看newExecutor到底做了什麼?
Java代碼
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
上面代碼的執行步驟如下:
(1)判斷執行器類型,如果配置文件中沒有配置執行器類型,則採用默認執行類型ExecutorType.SIMPLE。
(2)根據執行器類型返回不同類型的執行器(執行器有三種,分別是 BatchExecutor、SimpleExecutor和CachingExecutor,後面我們再詳細看看)。
(3)跟執行器綁定攔截器插件(這裏也是使用代理來實現)。
DefaultSqlSession到底是幹什麼的呢?
DefaultSqlSession實現了SqlSession接口,裏面有各種各樣的SQL執行方法,主要用於SQL操作的對外接口,它會的調用執行器來執行實際的SQL語句。
接下來我們看看SQL查詢是怎麼進行的
Java代碼
UserInfo user = (UserInfo) session.selectOne("User.selectUser", "1");
實際調用的是DefaultSqlSession類的selectOne方法,該方法代碼如下:
Java代碼
public Object selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List list = selectList(statement, parameter);
if (list.size() == <span class="lit">1) {
return list.get(<span class="lit">0);
} else if (list.size() > <span class="lit">1) {
throw new TooManyResultsException(<span class="str">"Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
我們再看看selectList方法(實際上是調用該類的另一個selectList方法來實現的):
Java代碼
public List selectList(String statement, Object parameter) {
return selectList(statement, parameter, RowBounds.DEFAULT);
}
public List selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException(<span class="str">"Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
第二個selectList的執行步驟如下:
(1)根據SQL的ID到配置信息中找對應的MappedStatement,在之前配置被加載初始化的時候我們看到了系統會把配置文件中的SQL塊解析並放到一個MappedStatement裏面,並將MappedStatement對象放到一個Map裏面進行存放,Map的key值是該SQL塊的ID。
(2)調用執行器的query方法,傳入MappedStatement對象、SQL參數對象、範圍對象(此處爲空)和結果處理方式。
好了,目前只剩下一個疑問,那就是執行器到底怎麼執行SQL的呢?
上面我們知道了,默認情況下是採用SimpleExecutor執行的,我們看看這個類的doQuery方法:
Java代碼
public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, rowBounds, resultHandler);
stmt = prepareStatement(handler);
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
doQuery方法的內部執行步驟:
(1) 獲取配置信息對象。
(2)通過配置對象獲取一個新的StatementHandler,該類主要用來處理一次SQL操作。
(3)預處理StatementHandler對象,得到Statement對象。
(4)傳入Statement和結果處理對象,通過StatementHandler的query方法來執行SQL,並對執行結果進行處理。
我們看一下newStatementHandler到底做了什麼?
Java代碼
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
上面代碼的執行步驟:
(1)根據相關的參數獲取對應的StatementHandler對象。
(2)爲StatementHandler對象綁定攔截器插件。
RoutingStatementHandler類的構造方法RoutingStatementHandler如下:
Java代碼
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) {
switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler);
break;
default:
throw new ExecutorException(<span class="str">"Unknown statement type: " + ms.getStatementType());
}
}
根據 MappedStatement對象的StatementType來創建不同的StatementHandler,這個跟前面執行器的方式類似。StatementType有STATEMENT、PREPARED和CALLABLE三種類型,跟JDBC裏面的Statement類型一一對應。
我們看一下prepareStatement方法具體內容:
Java代碼
private Statement prepareStatement(StatementHandler handler) throws SQLException {
Statement stmt;
Connection connection = transaction.getConnection();
//從連接中獲取Statement對象
stmt = handler.prepare(connection);
//處理預編譯的傳入參數
handler.parameterize(stmt);
return stmt;
}