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);
SqlSessionFactoryBuilder的build的方法如下:
Java程式碼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程式碼
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
最後我們看一下第四行程式碼:
Java程式碼SqlSession session = ssf.openSession();
通過呼叫DefaultSqlSessionFactory的openSession方法返回一個SqlSession例項,我們看一下具體是怎麼得到一個SqlSession例項的。首先呼叫openSessionFromDataSource方法。
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進行包裝,使連線具備日誌功能,這裡用的是代理。
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();
}
}
傳入引數說明:
(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程式碼 String resource = "mybatis.cfg.xml";
Reader reader = Resources.getResourceAsReader(resource);
SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);
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();
}
本次我們需要進行深入跟蹤分析的是:
Java程式碼SqlSession session = ssf.openSession();
UserInfo user = (UserInfo) session.selectOne("User.selectUser", "1");
原始碼分析
第一步:開啟一個會話,我們看看裡面具體做了什麼事情。
Java程式碼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() == 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;
}
}
我們再看看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("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("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;
}