【深入淺出MyBatis系列十二】終結篇:MyBatis原理深入解析
- 深入淺出MyBatis系列
- 【深入淺出MyBatis系列一】MyBatis入門
- 【深入淺出MyBatis系列二】配置簡介(MyBatis源碼篇)
- 【深入淺出MyBatis系列三】Mapper映射文件配置
- 【深入淺出MyBatis系列四】強大的動態SQL
- 【深入淺出MyBatis系列五】SQL執行流程分析(源碼篇)
- 【深入淺出MyBatis系列六】插件原理
- 【深入淺出MyBatis系列七】分頁插件
- 【深入淺出MyBatis系列八】SQL自動生成插件
- 【深入淺出MyBatis系列九】改造Cache插件
- 【深入淺出MyBatis系列十】與Spring集成
- 【深入淺出MyBatis系列十一】緩存源碼分析
- 【深入淺出MyBatis系列十二】終結篇:MyBatis原理深入解析
#1 引言# 本文主要講解JDBC怎麼演變到Mybatis的漸變過程,重點講解了爲什麼要將JDBC封裝成Mybaits這樣一個持久層框架。再而論述Mybatis作爲一個數據持久層框架本身有待改進之處。
#2 JDBC實現查詢分析# 我們先看看我們最熟悉也是最基礎的通過JDBC查詢數據庫數據,一般需要以下七個步驟:
加載JDBC驅動;
建立並獲取數據庫連接;
創建 JDBC Statements 對象;
設置SQL語句的傳入參數;
執行SQL語句並獲得查詢結果;
對查詢結果進行轉換處理並將處理結果返回;
釋放相關資源(關閉Connection,關閉Statement,關閉ResultSet);
以下是具體的實現代碼:
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有七個步驟,哪些步驟是可以進一步封裝的,減少我們開發的代碼量。
##3.1 第一步優化:連接獲取和釋放##
- 問題描述:
數據庫連接頻繁的開啓和關閉本身就造成了資源的浪費,影響系統的性能
。
解決問題:
數據庫連接的獲取和關閉我們可以使用數據庫連接池來解決資源浪費的問題
。通過連接池就可以反覆利用已經建立的連接去訪問數據庫了。減少連接的開啓和關閉的時間。
- 問題描述:
但是現在連接池多種多樣,可能存在變化
,有可能採用DBCP的連接池,也有可能採用容器本身的JNDI數據庫連接池。
解決問題:
我們可以通過DataSource進行隔離解耦
,我們統一從DataSource裏面獲取數據庫連接,DataSource具體由DBCP實現還是由容器的JNDI實現都可以
,所以我們將DataSource的具體實現通過讓用戶配置來應對變化。
##3.2 第二步優化:SQL統一存取##
- 問題描述:
我們使用JDBC進行操作數據庫時,SQL語句基本都散落在各個JAVA類中
,這樣有三個不足之處:
第一,可讀性很差,不利於維護以及做性能調優。
第二,改動Java代碼需要重新編譯、打包部署。
第三,不利於取出SQL在數據庫客戶端執行(取出後還得刪掉中間的Java代碼,編寫好的SQL語句寫好後還得通過+號在Java進行拼湊)。
解決問題:
我們可以考慮不把SQL語句寫到Java代碼中,那麼把SQL語句放到哪裏呢?首先需要有一個統一存放的地方,我們可以將這些SQL語句統一集中放到配置文件或者數據庫裏面(以key-value的格式存放)
。然後通過SQL語句的key值去獲取對應的SQL語句。
既然我們將SQL語句都統一放在配置文件或者數據庫中,那麼這裏就涉及一個SQL語句的加載問題
。
##3.3 第三步優化:傳入參數映射和動態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語句其他地方使用不了。這裏我們可以使用#變量名#表示佔位符變量,使用$變量名$表示非佔位符變量
。
##3.4 第四步優化:結果映射和結果緩存##
- 問題描述:
執行SQL語句、獲取執行結果、對執行結果進行轉換處理、釋放相關資源是一整套下來的。假如是執行查詢語句,那麼執行SQL語句後,返回的是一個ResultSet結果集,這個時候我們就需要將ResultSet對象的數據取出來,不然等到釋放資源時就取不到這些結果信息了
。我們從前面的優化來看,以及將獲取連接、設置傳入參數、執行SQL語句、釋放資源這些都封裝起來了,只剩下結果處理這塊還沒有進行封裝,如果能封裝起來,每個數據庫操作都不用自己寫那麼一大堆Java代碼,直接調用一個封裝的方法就可以搞定了。
解決問題:
我們分析一下,一般對執行結果的有哪些處理,有可能將結果不做任何處理就直接返回,也有可能將結果轉換成一個JavaBean對象返回、一個Map返回、一個List返回等等
,結果處理可能是多種多樣的。從這裏看,我們必須告訴SQL處理器兩點:第一,需要返回什麼類型的對象;第二,需要返回的對象的數據結構怎麼跟執行的結果映射
,這樣才能將具體的值copy到對應的數據結構上。
接下來,我們可以進而考慮對SQL執行結果的緩存來提升性能
。緩存數據都是key-value的格式,那麼這個key怎麼來呢
?怎麼保證唯一呢?即使同一條SQL語句幾次訪問的過程中由於傳入參數的不同,得到的執行SQL語句也是不同的。那麼緩存起來的時候是多對。但是SQL語句和傳入參數兩部分合起來可以作爲數據緩存的key值
。
##3.5 第五步優化:解決重複SQL語句問題##
- 問題描述:
由於我們將所有SQL語句都放到配置文件中,這個時候會遇到一個SQL重複的問題
,幾個功能的SQL語句其實都差不多,有些可能是SELECT後面那段不同、有些可能是WHERE語句不同。有時候表結構改了,那麼我們就需要改多個地方,不利於維護。
解決問題:
當我們的代碼程序出現重複代碼時怎麼辦?將重複的代碼抽離出來成爲獨立的一個類,然後在各個需要使用的地方進行引用
。對於SQL重複的問題,我們也可以採用這種方式,通過將SQL片段模塊化,將重複的SQL片段獨立成一個SQL塊,然後在各個SQL語句引用重複的SQL塊
,這樣需要修改時只需要修改一處即可。
#4 Mybaits有待改進之處#
- 問題描述:
Mybaits所有的數據庫操作都是基於SQL語句,導致什麼樣的數據庫操作都要寫SQL語句
。一個應用系統要寫的SQL語句實在太多了。
改進方法:
我們對數據庫進行的操作大部分都是對錶數據的增刪改查,很多都是對單表的數據進行操作,由這點我們可以想到一個問題:單表操作可不可以不寫SQL語句,通過JavaBean的默認映射器生成對應的SQL語句
,比如:一個類UserInfo對應於USER_INFO表, userId屬性對應於USER_ID字段。這樣我們就可以通過反射可以獲取到對應的表結構了,拼湊成對應的SQL語句顯然不是問題
。
#5 MyBatis框架整體設計#
##5.1 接口層-和數據庫交互的方式# MyBatis和數據庫的交互有兩種方式:
使用傳統的MyBatis提供的API;
使用Mapper接口;
###5.1.1 使用傳統的MyBatis提供的API### 這是傳統的傳遞Statement Id 和查詢參數給 SqlSession 對象,使用 SqlSession對象完成和數據庫的交互
;MyBatis 提供了非常方便和簡單的API,供用戶實現對數據庫的增刪改查數據操作,以及對數據庫連接信息和MyBatis 自身配置信息的維護操作。
上述使用MyBatis 的方法,是創建一個和數據庫打交道的SqlSession對象,然後根據Statement Id 和參數來操作數據庫
,這種方式固然很簡單和實用,但是它不符合面嚮對象語言的概念和麪向接口編程的編程習慣
。由於面向接口的編程是面向對象的大趨勢,MyBatis 爲了適應這一趨勢,增加了第二種使用MyBatis 支持接口(Interface)調用方式。
###5.1.2 使用Mapper接口### MyBatis 將配置文件中的每一個<mapper> 節點抽象爲一個 Mapper 接口
,而這個接口中聲明的方法和跟<mapper> 節點中的<select|update|delete|insert> 節點項對應
,即<select|update|delete|insert> 節點的id值爲Mapper 接口中的方法名稱,parameterType 值表示Mapper 對應方法的入參類型
,而resultMap 值則對應了Mapper 接口表示的返回值類型或者返回結果集的元素類型
。
根據MyBatis 的配置規範配置好後,通過SqlSession.getMapper(XXXMapper.class)
方法,MyBatis 會根據相應的接口聲明的方法信息,通過動態代理機制生成一個Mapper 實例
,我們使用Mapper 接口的某一個方法時,MyBatis 會根據這個方法的方法名和參數類型,確定Statement Id,底層還是通過SqlSession.select("statementId",parameterObject);或者SqlSession.update("statementId",parameterObject); 等等來實現對數據庫的操作, MyBatis 引用Mapper 接口這種調用方式,純粹是爲了滿足面向接口編程的需要。(其實還有一個原因是在於,面向接口的編程,使得用戶在接口上可以使用註解來配置SQL語句,這樣就可以脫離XML配置文件,實現“0配置”)。
##5.2 數據處理層## 數據處理層可以說是MyBatis 的核心,從大的方面上講,它要完成兩個功能:
通過傳入參數構建動態SQL語句;
SQL語句的執行以及封裝查詢結果集成List<E>
###5.2.1 參數映射和動態SQL語句生成### 動態語句生成可以說是MyBatis框架非常優雅的一個設計,MyBatis 通過傳入的參數值,使用 Ognl 來動態地構造SQL語句
,使得MyBatis 有很強的靈活性和擴展性。
參數映射指的是對於java 數據類型和jdbc數據類型之間的轉換
:這裏有包括兩個過程:查詢階段,我們要將java類型的數據,轉換成jdbc類型的數據,通過 preparedStatement.setXXX() 來設值
;另一個就是對resultset查詢結果集的jdbcType 數據轉換成java 數據類型
。
###5.2.2 SQL語句的執行以及封裝查詢結果集成List<E>###
動態SQL語句生成之後,MyBatis 將執行SQL語句,並將可能返回的結果集轉換成List<E> 列表。MyBatis 在對結果集的處理中,支持結果集關係一對多和多對一的轉換
,並且有兩種支持方式,一種爲嵌套查詢語句的查詢,還有一種是嵌套結果集的查詢
。
##5.3 框架支撐層##
- 事務管理機制
事務管理機制對於ORM框架而言是不可缺少的一部分
,事務管理機制的質量也是考量一個ORM框架是否優秀的一個標準。
- 連接池管理機制
由於創建一個數據庫連接所佔用的資源比較大, 對於數據吞吐量大和訪問量非常大的應用而言
,連接池的設計就顯得非常重要。
- 緩存機制
爲了提高數據利用率和減小服務器和數據庫的壓力,MyBatis 會對於一些查詢提供會話級別的數據緩存
,會將對某一次查詢,放置到SqlSession 中,在允許的時間間隔內,對於完全相同的查詢,MyBatis 會直接將緩存結果返回給用戶,而不用再到數據庫中查找。
- SQL語句的配置方式
傳統的MyBatis 配置SQL 語句方式就是使用XML文件進行配置的,但是這種方式不能很好地支持面向接口編程的理念,爲了支持面向接口的編程,MyBatis 引入了Mapper接口的概念,面向接口的引入,對使用註解來配置SQL 語句成爲可能,用戶只需要在接口上添加必要的註解即可,不用再去配置XML文件了
,但是,目前的MyBatis 只是對註解配置SQL 語句提供了有限的支持,某些高級功能還是要依賴XML配置文件配置SQL 語句。
##5.4 引導層## 引導層是配置和啓動MyBatis配置信息的方式
。MyBatis 提供兩種方式來引導MyBatis :基於XML配置文件的方式和基於Java API 的方式
。
##5.5 主要構件及其相互關係## 從MyBatis代碼實現的角度來看,MyBatis的主要的核心部件有以下幾個:
SqlSession 作爲MyBatis工作的主要頂層API,表示和數據庫交互的會話,完成必要數據庫增刪改查功能
Executor MyBatis執行器,是MyBatis 調度的核心,負責SQL語句的生成和查詢緩存的維護
StatementHandler 封裝了JDBC Statement操作,負責對JDBC statement 的操作,如設置參數、將Statement結果集轉換成List集合。
ParameterHandler 負責對用戶傳遞的參數轉換成JDBC Statement 所需要的參數,
ResultSetHandler 負責將JDBC返回的ResultSet結果集對象轉換成List類型的集合;
TypeHandler 負責java數據類型和jdbc數據類型之間的映射和轉換
MappedStatement MappedStatement維護了一條<select|update|delete|insert>節點的封裝,
SqlSource 負責根據用戶傳遞的parameterObject,動態地生成SQL語句,將信息封裝到BoundSql對象中,並返回
BoundSql 表示動態生成的SQL語句以及相應的參數信息
Configuration MyBatis所有的配置信息都維持在Configuration對象之中。
它們的關係如下圖所示:
#6 SqlSession工作過程分析#
- 開啓一個數據庫訪問會話---創建SqlSession對象
SqlSession sqlSession = factory.openSession();
MyBatis封裝了對數據庫的訪問,把對數據庫的會話和事務控制放到了SqlSession對象中。
- 爲SqlSession傳遞一個配置的Sql語句 的Statement Id和參數,然後返回結果:
List<Employee> result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",params);
上述的"com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",是配置在EmployeesMapper.xml 的Statement ID,params 是傳遞的查詢參數。
讓我們來看一下sqlSession.selectList()方法的定義:
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//1.根據Statement Id,在mybatis 配置對象Configuration中查找和配置文件相對應的MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
//2. 將查詢任務委託給MyBatis 的執行器 Executor
List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
return result;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
MyBatis在初始化的時候,會將MyBatis的配置信息全部加載到內存中,使用org.apache.ibatis.session.Configuration實例來維護
。使用者可以使用sqlSession.getConfiguration()方法來獲取。MyBatis的配置文件中配置信息的組織格式和內存中對象的組織格式幾乎完全對應的
。上述例子中的
<select id="selectByMinSalary" resultMap="BaseResultMap" parameterType="java.util.Map" >
select
EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, SALARY
from LOUIS.EMPLOYEES
<if test="min_salary != null">
where SALARY < #{min_salary,jdbcType=DECIMAL}
</if>
</select>
加載到內存中會生成一個對應的MappedStatement對象,然後會以key="com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary" ,value爲MappedStatement對象的形式維護到Configuration的一個Map中
。當以後需要使用的時候,只需要通過Id值來獲取就可以了。
從上述的代碼中我們可以看到SqlSession的職能是:SqlSession根據Statement ID, 在mybatis配置對象Configuration中獲取到對應的MappedStatement對象,然後調用mybatis執行器來執行具體的操作
。
- MyBatis執行器Executor根據SqlSession傳遞的參數執行query()方法(由於代碼過長,讀者只需閱讀我註釋的地方即可):
/**
* BaseExecutor 類部分代碼
*
*/
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 1. 根據具體傳入的參數,動態地生成需要執行的SQL語句,用BoundSql對象表示
BoundSql boundSql = ms.getBoundSql(parameter);
// 2. 爲當前的查詢創建一個緩存Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
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());
if (closed) throw new ExecutorException("Executor was closed.");
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 3.緩存中沒有值,直接從數據庫中讀取數據
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
deferredLoads.clear(); // issue #601
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache(); // issue #482
}
}
return list;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//4. 執行查詢,返回List 結果,然後 將查詢的結果放入緩存之中
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;
}
/**
*
*SimpleExecutor類的doQuery()方法實現
*
*/
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();
//5. 根據既有的參數,創建StatementHandler對象來執行查詢操作
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//6. 創建java.Sql.Statement對象,傳遞給StatementHandler對象
stmt = prepareStatement(handler, ms.getStatementLog());
//7. 調用StatementHandler.query()方法,返回List結果集
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
上述的Executor.query()方法幾經轉折,最後會創建一個StatementHandler對象,然後將必要的參數傳遞給StatementHandler
,使用StatementHandler來完成對數據庫的查詢,最終返回List結果集。
從上面的代碼中我們可以看出,Executor的功能和作用是:
根據傳遞的參數,完成SQL語句的動態解析,生成BoundSql對象,供StatementHandler使用;
爲查詢創建緩存,以提高性能;
創建JDBC的Statement連接對象,傳遞給StatementHandler對象,返回List查詢結果;
- StatementHandler對象負責設置Statement對象中的查詢參數、處理JDBC返回的resultSet,將resultSet加工爲List 集合返回:
接着上面的Executor第六步,看一下:prepareStatement() 方法的實現:
/**
*
*SimpleExecutor類的doQuery()方法實現
*
*/
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 handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 1.準備Statement對象,並設置Statement對象的參數
stmt = prepareStatement(handler, ms.getStatementLog());
// 2. StatementHandler執行query()方法,返回List結果
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection);
//對創建的Statement對象設置參數,即設置SQL 語句中 ? 設置爲指定的參數
handler.parameterize(stmt);
return stmt;
}
以上我們可以總結StatementHandler對象主要完成兩個工作:
對於JDBC的PreparedStatement類型的對象,創建的過程中,我們使用的是SQL語句字符串會包含 若干個? 佔位符,我們其後再對佔位符進行設值。 StatementHandler通過parameterize(statement)方法對Statement進行設值;
StatementHandler通過List<E> query(Statement statement, ResultHandler resultHandler)方法來完成執行Statement,和將Statement對象返回的resultSet封裝成List;
- StatementHandler 的parameterize(statement) 方法的實現:
/**
* StatementHandler 類的parameterize(statement) 方法實現
*/
public void parameterize(Statement statement) throws SQLException {
//使用ParameterHandler對象來完成對Statement的設值
parameterHandler.setParameters((PreparedStatement) statement);
}
/**
*
*ParameterHandler類的setParameters(PreparedStatement ps) 實現
* 對某一個Statement進行設置參數
*/
public void setParameters(PreparedStatement ps) throws SQLException {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
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);
}
// 每一個Mapping都有一個TypeHandler,根據TypeHandler來對preparedStatement進行設置參數
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull();
// 設置參數
typeHandler.setParameter(ps, i + 1, value, jdbcType);
}
}
}
}
從上述的代碼可以看到,StatementHandler 的parameterize(Statement) 方法調用了 ParameterHandler的setParameters(statement) 方法, ParameterHandler的setParameters(Statement)方法負責 根據我們輸入的參數,對statement對象的 ? 佔位符處進行賦值
。
- StatementHandler 的List<E> query(Statement statement, ResultHandler resultHandler)方法的實現:
/**
* PreParedStatement類的query方法實現
*/
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
// 1.調用preparedStatemnt。execute()方法,然後將resultSet交給ResultSetHandler處理
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
//2. 使用ResultHandler來處理ResultSet
return resultSetHandler.<E> handleResultSets(ps);
}
/**
*ResultSetHandler類的handleResultSets()方法實現
*
*/
public List<Object> handleResultSets(Statement stmt) throws SQLException {
final List<Object> multipleResults = new ArrayList<Object>();
int resultSetCount = 0;
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
//將resultSet
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
String[] resultSets = mappedStatement.getResulSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}
從上述代碼我們可以看出,StatementHandler 的List<E> query(Statement statement, ResultHandler resultHandler)方法的實現,是調用了ResultSetHandler的handleResultSets(Statement) 方法。ResultSetHandler的handleResultSets(Statement) 方法會將Statement語句執行後生成的resultSet 結果集轉換成List<E> 結果集
:
public List<Object> handleResultSets(Statement stmt) throws SQLException {
final List<Object> multipleResults = new ArrayList<Object>();
int resultSetCount = 0;
ResultSetWrapper rsw = getFirstResultSet(stmt);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
//將resultSet
handleResultSet(rsw, resultMap, multipleResults, null);
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
String[] resultSets = mappedStatement.getResulSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}
#7 MyBatis初始化機制# ##7.1 MyBatis的初始化做了什麼## 任何框架的初始化,無非是加載自己運行時所需要的配置信息
。MyBatis的配置信息,大概包含以下信息,其高層級結構如下:
MyBatis的上述配置信息會配置在XML配置文件中,那麼,這些信息被加載進入MyBatis內部,MyBatis是怎樣維護的呢?
MyBatis採用了一個非常直白和簡單的方式---使用 org.apache.ibatis.session.Configuration
對象作爲一個所有配置信息的容器,Configuration對象的組織結構和XML配置文件的組織結構幾乎完全一樣
(當然,Configuration對象的功能並不限於此,它還負責創建一些MyBatis內部使用的對象,如Executor等,這將在後續的文章中討論)。如下圖所示:
MyBatis根據初始化好Configuration信息,這時候用戶就可以使用MyBatis進行數據庫操作了。可以這麼說,MyBatis初始化的過程,就是創建 Configuration對象的過程
。
MyBatis的初始化可以有兩種方式:
基於XML配置文件:基於XML配置文件的方式是將MyBatis的所有配置信息放在XML文件中,MyBatis通過加載並XML配置文件,將配置文信息組裝成內部的Configuration對象。
基於Java API:這種方式不使用XML配置文件,需要MyBatis使用者在Java代碼中,手動創建Configuration對象,然後將配置參數set 進入Configuration對象中。
接下來我們將通過 基於XML配置文件方式的MyBatis初始化,深入探討MyBatis是如何通過配置文件構建Configuration對象,並使用它。
##7.2 基於XML配置文件創建Configuration對象## 現在就從使用MyBatis的簡單例子入手,深入分析一下MyBatis是怎樣完成初始化的,都初始化了什麼。看以下代碼:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
List list = sqlSession.selectList("com.foo.bean.BlogMapper.queryAllBlogInfo");
有過MyBatis使用經驗的讀者會知道,上述語句的作用是執行com.foo.bean.BlogMapper.queryAllBlogInfo 定義的SQL語句,返回一個List結果集。總的來說,上述代碼經歷了mybatis初始化 -->創建SqlSession -->執行SQL語句
返回結果三個過程。
上述代碼的功能是根據配置文件mybatis-config.xml 配置文件,創建SqlSessionFactory對象,然後產生SqlSession,執行SQL語句。而mybatis的初始化就發生在第三句:SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
現在就讓我們看看第三句到底發生了什麼。
- MyBatis初始化基本過程:
SqlSessionFactoryBuilder根據傳入的數據流生成Configuration對象,然後根據Configuration對象創建默認的SqlSessionFactory實例。
初始化的基本過程如下序列圖所示:
由上圖所示,mybatis初始化要經過簡單的以下幾步:
調用SqlSessionFactoryBuilder對象的build(inputStream)方法;
SqlSessionFactoryBuilder會根據輸入流inputStream等信息創建XMLConfigBuilder對象;
SqlSessionFactoryBuilder調用XMLConfigBuilder對象的parse()方法;
XMLConfigBuilder對象返回Configuration對象;
SqlSessionFactoryBuilder根據Configuration對象創建一個DefaultSessionFactory對象;
SqlSessionFactoryBuilder返回 DefaultSessionFactory對象給Client,供Client使用。
SqlSessionFactoryBuilder相關的代碼如下所示:
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//2. 創建XMLConfigBuilder對象用來解析XML配置文件,生成Configuration對象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//3. 將XML配置文件內的信息解析成Java對象Configuration對象
Configuration config = parser.parse();
//4. 根據Configuration對象創建出SqlSessionFactory對象
return build(config);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
// 從此處可以看出,MyBatis內部通過Configuration對象來創建SqlSessionFactory,用戶也可以自己通過API構造好Configuration對象,調用此方法創SqlSessionFactory
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
上述的初始化過程中,涉及到了以下幾個對象:
SqlSessionFactoryBuilder : SqlSessionFactory的構造器,用於創建SqlSessionFactory,採用了Builder設計模式
Configuration :該對象是mybatis-config.xml文件中所有mybatis配置信息
SqlSessionFactory:SqlSession工廠類,以工廠形式創建SqlSession對象,採用了Factory工廠設計模式
XmlConfigParser :負責將mybatis-config.xml配置文件解析成Configuration對象,共SqlSessonFactoryBuilder使用,創建SqlSessionFactory
- 創建Configuration對象的過程: 接着上述的 MyBatis初始化基本過程討論,
當SqlSessionFactoryBuilder執行build()方法,調用了XMLConfigBuilder的parse()方法,然後返回了Configuration對象
。那麼parse()方法是如何處理XML文件,生成Configuration對象的呢?
-
- XMLConfigBuilder會
將XML配置文件的信息轉換爲Document對象
,而XML配置定義文件DTD轉換成XMLMapperEntityResolver對象
,然後將二者封裝到XpathParser對象中
,XpathParser的作用是提供根據Xpath表達式獲取基本的DOM節點Node信息的操作
。如下圖所示:
- XMLConfigBuilder會
-
- 之後XMLConfigBuilder調用parse()方法:
會從XPathParser中取出 <configuration>節點對應的Node對象,然後解析此Node節點的子Node
:properties, settings, typeAliases,typeHandlers, objectFactory, objectWrapperFactory, plugins, environments,databaseIdProvider, mappers:
- 之後XMLConfigBuilder調用parse()方法:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//源碼中沒有這一句,只有 parseConfiguration(parser.evalNode("/configuration"));
//爲了讓讀者看得更明晰,源碼拆分爲以下兩句
XNode configurationNode = parser.evalNode("/configuration");
parseConfiguration(configurationNode);
return configuration;
}
/**
* 解析 "/configuration"節點下的子節點信息,然後將解析的結果設置到Configuration對象中
*/
private void parseConfiguration(XNode root) {
try {
//1.首先處理properties 節點
propertiesElement(root.evalNode("properties")); //issue #117 read properties first
//2.處理typeAliases
typeAliasesElement(root.evalNode("typeAliases"));
//3.處理插件
pluginElement(root.evalNode("plugins"));
//4.處理objectFactory
objectFactoryElement(root.evalNode("objectFactory"));
//5.objectWrapperFactory
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//6.settings
settingsElement(root.evalNode("settings"));
//7.處理environments
environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
//8.database
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//9.typeHandlers
typeHandlerElement(root.evalNode("typeHandlers"));
//10.mappers
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
注意:在上述代碼中,還有一個非常重要的地方,就是解析XML配置文件子節點<mappers>的方法mapperElements(root.evalNode("mappers")), 它將解析我們配置的Mapper.xml配置文件,Mapper配置文件可以說是MyBatis的核心
,MyBatis的特性和理念都體現在此Mapper的配置和設計上。
-
- 然後將這些值解析出來設置到Configuration對象中:
解析子節點的過程這裏就不一一介紹了,用戶可以參照MyBatis源碼仔細揣摩,我們就看上述的environmentsElement(root.evalNode("environments")); 方法是如何將environments的信息解析出來,設置到Configuration對象中的:
/**
* 解析environments節點,並將結果設置到Configuration對象中
* 注意:創建envronment時,如果SqlSessionFactoryBuilder指定了特定的環境(即數據源);
* 則返回指定環境(數據源)的Environment對象,否則返回默認的Environment對象;
* 這種方式實現了MyBatis可以連接多數據源
*/
private void environmentsElement(XNode context) throws Exception {
if (context != null)
{
if (environment == null)
{
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren())
{
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id))
{
//1.創建事務工廠 TransactionFactory
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
//2.創建數據源DataSource
DataSource dataSource = dsFactory.getDataSource();
//3. 構造Environment對象
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
//4. 將創建的Envronment對象設置到configuration 對象中
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
private boolean isSpecifiedEnvironment(String id)
{
if (environment == null)
{
throw new BuilderException("No environment specified.");
}
else if (id == null)
{
throw new BuilderException("Environment requires an id attribute.");
}
else if (environment.equals(id))
{
return true;
}
return false;
}
-
- 返回Configuration對象:
將上述的MyBatis初始化基本過程的序列圖細化:
##7.3 基於Java API手動加載XML配置文件創建Configuration對象,並使用SqlSessionFactory對象## 我們可以使用XMLConfigBuilder手動解析XML配置文件來創建Configuration對象,代碼如下:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 手動創建XMLConfigBuilder,並解析創建Configuration對象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, null,null);
Configuration configuration=parse();
// 使用Configuration對象創建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
// 使用MyBatis
SqlSession sqlSession = sqlSessionFactory.openSession();
List list = sqlSession.selectList("com.foo.bean.BlogMapper.queryAllBlogInfo");
##7.4 涉及到的設計模式## 初始化的過程涉及到創建各種對象,所以會使用一些創建型的設計模式。在初始化的過程中,Builder模式運用的比較多
。
###7.4.1 Builder模式應用1: SqlSessionFactory的創建### 對於創建SqlSessionFactory時,會根據情況提供不同的參數,其參數組合可以有以下幾種
:
由於構造時參數不定,可以爲其創建一個構造器Builder,將SqlSessionFactory的構建過程和表示分開
:
MyBatis將SqlSessionFactoryBuilder和SqlSessionFactory相互獨立。
###7.4.2 Builder模式應用2: 數據庫連接環境Environment對象的創建### 在構建Configuration對象的過程中,XMLConfigParser解析 mybatis XML配置文件節點<environment>節點時,會有以下相應的代碼:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
//是和默認的環境相同時,解析之
if (isSpecifiedEnvironment(id)) {
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
//使用了Environment內置的構造器Builder,傳遞id 事務工廠和數據源
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
在Environment內部,定義了靜態內部Builder類:
public final class Environment {
private final String id;
private final TransactionFactory transactionFactory;
private final DataSource dataSource;
public Environment(String id, TransactionFactory transactionFactory, DataSource dataSource) {
if (id == null) {
throw new IllegalArgumentException("Parameter 'id' must not be null");
}
if (transactionFactory == null) {
throw new IllegalArgumentException("Parameter 'transactionFactory' must not be null");
}
this.id = id;
if (dataSource == null) {
throw new IllegalArgumentException("Parameter 'dataSource' must not be null");
}
this.transactionFactory = transactionFactory;
this.dataSource = dataSource;
}
public static class Builder {
private String id;
private TransactionFactory transactionFactory;
private DataSource dataSource;
public Builder(String id) {
this.id = id;
}
public Builder transactionFactory(TransactionFactory transactionFactory) {
this.transactionFactory = transactionFactory;
return this;
}
public Builder dataSource(DataSource dataSource) {
this.dataSource = dataSource;
return this;
}
public String id() {
return this.id;
}
public Environment build() {
return new Environment(this.id, this.transactionFactory, this.dataSource);
}
}
public String getId() {
return this.id;
}
public TransactionFactory getTransactionFactory() {
return this.transactionFactory;
}
public DataSource getDataSource() {
return this.dataSource;
}
}
#8 MyBatis數據源與連接池# ##8.1 MyBatis數據源DataSource分類## MyBatis數據源實現是在以下四個包中:
MyBatis把數據源DataSource分爲三種:
UNPOOLED 不使用連接池的數據源
POOLED 使用連接池的數據源
JNDI 使用JNDI實現的數據源
即:
相應地,MyBatis內部分別定義了實現了java.sql.DataSource接口的UnpooledDataSource,PooledDataSource類來表示UNPOOLED、POOLED類型的數據源
。 如下圖所示:
對於JNDI類型的數據源DataSource,則是通過JNDI上下文中取值。
##8.2 數據源DataSource的創建過程## MyBatis數據源DataSource對象的創建發生在MyBatis初始化的過程中
。下面讓我們一步步地瞭解MyBatis是如何創建數據源DataSource的。
在mybatis的XML配置文件中,使用<dataSource>元素來配置數據源:
- MyBatis在初始化時,解析此文件,根據<dataSource>的type屬性來創建相應類型的的數據源DataSource,即:
type=”POOLED” :MyBatis會創建PooledDataSource實例
type=”UNPOOLED” :MyBatis會創建UnpooledDataSource實例
type=”JNDI” :MyBatis會從JNDI服務上查找DataSource實例,然後返回使用
- 順便說一下,
MyBatis是通過工廠模式來創建數據源DataSource對象的
,MyBatis定義了抽象的工廠接口:org.apache.ibatis.datasource.DataSourceFactory,通過其getDataSource()方法返回數據源DataSource:
public interface DataSourceFactory {
void setProperties(Properties props);
// 生產DataSource
DataSource getDataSource();
}
上述三種不同類型的type,則有對應的以下dataSource工廠:
POOLED PooledDataSourceFactory
UNPOOLED UnpooledDataSourceFactory
JNDI JndiDataSourceFactory
其類圖如下所示:
- MyBatis創建了DataSource實例後,會
將其放到Configuration對象內的Environment對象中
,供以後使用。
##8.3 DataSource什麼時候創建Connection對象## 當我們需要創建SqlSession對象並需要執行SQL語句時,這時候MyBatis纔會去調用dataSource對象來創建java.sql.Connection對象。也就是說,java.sql.Connection對象的創建一直延遲到執行SQL語句的時候
。
比如,我們有如下方法執行一個簡單的SQL語句:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
sqlSession.selectList("SELECT * FROM STUDENTS");
前4句都不會導致java.sql.Connection對象的創建,只有當第5句sqlSession.selectList("SELECT * FROM STUDENTS")
,纔會觸發MyBatis在底層執行下面這個方法來創建java.sql.Connection對象:
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
connection = dataSource.getConnection();
if (level != null) {
connection.setTransactionIsolation(level.getLevel());
}
setDesiredAutoCommit(autoCommmit);
}
##8.4 不使用連接池的UnpooledDataSource## 當 <dataSource>的type屬性被配置成了”UNPOOLED”,MyBatis首先會實例化一個UnpooledDataSourceFactory工廠實例,然後通過.getDataSource()方法返回一個UnpooledDataSource實例對象引用,我們假定爲dataSource。
使用UnpooledDataSource的getConnection(),每調用一次就會產生一個新的Connection實例對象
。
UnPooledDataSource的getConnection()方法實現如下:
/*
* UnpooledDataSource的getConnection()實現
*/
public Connection getConnection() throws SQLException
{
return doGetConnection(username, password);
}
private Connection doGetConnection(String username, String password) throws SQLException
{
//封裝username和password成properties
Properties props = new Properties();
if (driverProperties != null)
{
props.putAll(driverProperties);
}
if (username != null)
{
props.setProperty("user", username);
}
if (password != null)
{
props.setProperty("password", password);
}
return doGetConnection(props);
}
/*
* 獲取數據連接
*/
private Connection doGetConnection(Properties properties) throws SQLException
{
//1.初始化驅動
initializeDriver();
//2.從DriverManager中獲取連接,獲取新的Connection對象
Connection connection = DriverManager.getConnection(url, properties);
//3.配置connection屬性
configureConnection(connection);
return connection;
}
如上代碼所示,UnpooledDataSource會做以下事情:
初始化驅動:判斷driver驅動是否已經加載到內存中,如果還沒有加載,則會動態地加載driver類,並實例化一個Driver對象,使用DriverManager.registerDriver()方法將其註冊到內存中,以供後續使用。
創建Connection對象:使用DriverManager.getConnection()方法創建連接。
配置Connection對象:設置是否自動提交autoCommit和隔離級別isolationLevel。
返回Connection對象。
上述的序列圖如下所示:
總結:從上述的代碼中可以看到,我們每調用一次getConnection()方法,都會通過DriverManager.getConnection()返回新的java.sql.Connection實例
。
##8.5 爲什麼要使用連接池?##
- 創建一個java.sql.Connection實例對象的代價
首先讓我們來看一下創建一個java.sql.Connection對象的資源消耗。我們通過連接Oracle數據庫,創建創建Connection對象,來看創建一個Connection對象、執行SQL語句各消耗多長時間。代碼如下:
public static void main(String[] args) throws Exception
{
String sql = "select * from hr.employees where employee_id < ? and employee_id >= ?";
PreparedStatement st = null;
ResultSet rs = null;
long beforeTimeOffset = -1L; //創建Connection對象前時間
long afterTimeOffset = -1L; //創建Connection對象後時間
long executeTimeOffset = -1L; //創建Connection對象後時間
Connection con = null;
Class.forName("oracle.jdbc.driver.OracleDriver");
beforeTimeOffset = new Date().getTime();
System.out.println("before:\t" + beforeTimeOffset);
con = DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:xe", "louluan", "123456");
afterTimeOffset = new Date().getTime();
System.out.println("after:\t\t" + afterTimeOffset);
System.out.println("Create Costs:\t\t" + (afterTimeOffset - beforeTimeOffset) + " ms");
st = con.prepareStatement(sql);
//設置參數
st.setInt(1, 101);
st.setInt(2, 0);
//查詢,得出結果集
rs = st.executeQuery();
executeTimeOffset = new Date().getTime();
System.out.println("Exec Costs:\t\t" + (executeTimeOffset - afterTimeOffset) + " ms");
}
上述程序的執行結果爲:
從此結果可以清楚地看出,創建一個Connection對象,用了250 毫秒;而執行SQL的時間用了170毫秒
。
創建一個Connection對象用了250毫秒!這個時間對計算機來說可以說是一個非常奢侈的!
這僅僅是一個Connection對象就有這麼大的代價,設想一下另外一種情況:如果我們在Web應用程序中,爲用戶的每一個請求就操作一次數據庫,當有10000個在線用戶併發操作的話,對計算機而言,僅僅創建Connection對象不包括做業務的時間就要損耗10000×250ms= 250 0000 ms = 2500 s = 41.6667 min,竟然要41分鐘!!!如果對高用戶羣體使用這樣的系統,簡直就是開玩笑!
- 問題分析:
創建一個java.sql.Connection對象的代價是如此巨大,是因爲創建一個Connection對象的過程,在底層就相當於和數據庫建立的通信連接,在建立通信連接的過程,消耗了這麼多的時間,而往往我們建立連接後(即創建Connection對象後),就執行一個簡單的SQL語句,然後就要拋棄掉,這是一個非常大的資源浪費!
- 解決方案:
對於需要頻繁地跟數據庫交互的應用程序,可以在創建了Connection對象,並操作完數據庫後,可以不釋放掉資源,而是將它放到內存中
,當下次需要操作數據庫時,可以直接從內存中取出Connection對象,不需要再創建了,這樣就極大地節省了創建Connection對象的資源消耗。由於內存也是有限和寶貴的,這又對我們對內存中的Connection對象怎麼有效地維護提出了很高的要求
。我們將在內存中存放Connection對象的容器稱之爲連接池(Connection Pool)。下面讓我們來看一下MyBatis的線程池是怎樣實現的。
##8.6 使用了連接池的PooledDataSource## 同樣地,我們也是使用PooledDataSource的getConnection()方法來返回Connection對象。現在讓我們看一下它的基本原理:
PooledDataSource將java.sql.Connection對象包裹成PooledConnection對象放到了PoolState類型的容器中維護
。 MyBatis將連接池中的PooledConnection分爲兩種狀態:空閒狀態(idle)和活動狀態(active)
,這兩種狀態的PooledConnection對象分別被存儲到PoolState容器內的idleConnections和activeConnections兩個List集合中
:
idleConnections:
空閒(idle)狀態PooledConnection對象被放置到此集合中,表示當前閒置的沒有被使用的PooledConnection集合,調用PooledDataSource的getConnection()方法時,會優先從此集合中取PooledConnection對象。當用完一個java.sql.Connection對象時,MyBatis會將其包裹成PooledConnection對象放到此集合中。
activeConnections:
活動(active)狀態的PooledConnection對象被放置到名爲activeConnections的ArrayList中,表示當前正在被使用的PooledConnection集合,調用PooledDataSource的getConnection()方法時,會優先從idleConnections集合中取PooledConnection對象,如果沒有,則看此集合是否已滿,如果未滿,PooledDataSource會創建出一個PooledConnection,添加到此集合中,並返回
。
PoolState連接池的大致結構如下所示:
- 獲取java.sql.Connection對象的過程
下面讓我們看一下PooledDataSource 的getConnection()方法獲取Connection對象的實現:
public Connection getConnection() throws SQLException {
return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}
public Connection getConnection(String username, String password) throws SQLException {
return popConnection(username, password).getProxyConnection();
}
上述的popConnection()方法,會從連接池中返回一個可用的PooledConnection對象,然後再調用getProxyConnection()方法最終返回Conection對象
。(至於爲什麼會有getProxyConnection(),請關注下一節)。
現在讓我們看一下popConnection()方法到底做了什麼:
先看是否有空閒(idle)狀態下的PooledConnection對象,如果有,就直接返回一個可用的PooledConnection對象;否則進行第2步。
查看活動狀態的PooledConnection池activeConnections是否已滿;如果沒有滿,則創建一個新的PooledConnection對象,然後放到activeConnections池中,然後返回此PooledConnection對象;否則進行第三步;
看最先進入activeConnections池中的PooledConnection對象是否已經過期:如果已經過期,從activeConnections池中移除此對象,然後創建一個新的PooledConnection對象,添加到activeConnections中,然後將此對象返回;否則進行第4步。
線程等待,循環2步
/*
* 傳遞一個用戶名和密碼,從連接池中返回可用的PooledConnection
*/
private PooledConnection popConnection(String username, String password) throws SQLException
{
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();
int localBadConnectionCount = 0;
while (conn == null)
{
synchronized (state)
{
if (state.idleConnections.size() > 0)
{
// 連接池中有空閒連接,取出第一個
conn = state.idleConnections.remove(0);
if (log.isDebugEnabled())
{
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
}
else
{
// 連接池中沒有空閒連接,則取當前正在使用的連接數小於最大限定值,
if (state.activeConnections.size() < poolMaximumActiveConnections)
{
// 創建一個新的connection對象
conn = new PooledConnection(dataSource.getConnection(), this);
@SuppressWarnings("unused")
//used in logging, if enabled
Connection realConn = conn.getRealConnection();
if (log.isDebugEnabled())
{
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
}
else
{
// Cannot create new connection 當活動連接池已滿,不能創建時,取出活動連接池的第一個,即最先進入連接池的PooledConnection對象
// 計算它的校驗時間,如果校驗時間大於連接池規定的最大校驗時間,則認爲它已經過期了,利用這個PoolConnection內部的realConnection重新生成一個PooledConnection
//
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > poolMaximumCheckoutTime)
{
// Can claim overdue connection
state.claimedOverdueConnectionCount++;
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
state.accumulatedCheckoutTime += longestCheckoutTime;
state.activeConnections.remove(oldestActiveConnection);
if (!oldestActiveConnection.getRealConnection().getAutoCommit())
{
oldestActiveConnection.getRealConnection().rollback();
}
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
oldestActiveConnection.invalidate();
if (log.isDebugEnabled())
{
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
}
else
{
//如果不能釋放,則必須等待有
// Must wait
try
{
if (!countedWait)
{
state.hadToWaitCount++;
countedWait = true;
}
if (log.isDebugEnabled())
{
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
state.wait(poolTimeToWait);
state.accumulatedWaitTime += System.currentTimeMillis() - wt;
}
catch (InterruptedException e)
{
break;
}
}
}
}
//如果獲取PooledConnection成功,則更新其信息
if (conn != null)
{
if (conn.isValid())
{
if (!conn.getRealConnection().getAutoCommit())
{
conn.getRealConnection().rollback();
}
conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
conn.setCheckoutTimestamp(System.currentTimeMillis());
conn.setLastUsedTimestamp(System.currentTimeMillis());
state.activeConnections.add(conn);
state.requestCount++;
state.accumulatedRequestTime += System.currentTimeMillis() - t;
}
else
{
if (log.isDebugEnabled())
{
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
state.badConnectionCount++;
localBadConnectionCount++;
conn = null;
if (localBadConnectionCount > (poolMaximumIdleConnections + 3))
{
if (log.isDebugEnabled())
{
log.debug("PooledDataSource: Could not get a good connection to the database.");
}
throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
}
}
}
}
}
if (conn == null)
{
if (log.isDebugEnabled())
{
log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
return conn;
}
對應的處理流程圖如下所示:
如上所示,對於PooledDataSource的getConnection()方法內,先是調用類PooledDataSource的popConnection()方法返回了一個PooledConnection對象,然後調用了PooledConnection的getProxyConnection()來返回Connection對象
。
- java.sql.Connection對象的回收
當我們的程序中使用完Connection對象時,如果不使用數據庫連接池,我們一般會調用 connection.close()方法
,關閉connection連接,釋放資源。如下所示:
private void test() throws ClassNotFoundException, SQLException
{
String sql = "select * from hr.employees where employee_id < ? and employee_id >= ?";
PreparedStatement st = null;
ResultSet rs = null;
Connection con = null;
Class.forName("oracle.jdbc.driver.OracleDriver");
try
{
con = DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:xe", "louluan", "123456");
st = con.prepareStatement(sql);
//設置參數
st.setInt(1, 101);
st.setInt(2, 0);
//查詢,得出結果集
rs = st.executeQuery();
//取數據,省略
//關閉,釋放資源
con.close();
}
catch (SQLException e)
{
con.close();
e.printStackTrace();
}
}
調用過close()方法的Connection對象所持有的資源會被全部釋放掉,Connection對象也就不能再使用
。
那麼,如果我們使用了連接池,我們在用完了Connection對象時,需要將它放在連接池中,該怎樣做呢?
爲了和一般的使用Conneciton對象的方式保持一致,我們希望當Connection使用完後,調用.close()方法,而實際上Connection資源並沒有被釋放,而實際上被添加到了連接池中。這樣可以做到嗎?答案是可以。上述的要求從另外一個角度來描述就是:能否提供一種機制,讓我們知道Connection對象調用了什麼方法,從而根據不同的方法自定義相應的處理機制。恰好代理機制就可以完成上述要求
.
怎樣實現Connection對象調用了close()方法,而實際是將其添加到連接池中:
這是要使用代理模式,爲真正的Connection對象創建一個代理對象,代理對象所有的方法都是調用相應的真正Connection對象的方法實現。當代理對象執行close()方法時,要特殊處理,不調用真正Connection對象的close()方法,而是將Connection對象添加到連接池中
。
MyBatis的PooledDataSource的PoolState內部維護的對象是PooledConnection類型的對象,而PooledConnection則是對真正的數據庫連接java.sql.Connection實例對象的包裹器
。
PooledConnection對象內持有一個真正的數據庫連接java.sql.Connection實例對象和一個java.sql.Connection的代理
,其部分定義如下:
class PooledConnection implements InvocationHandler {
//......
//所創建它的datasource引用
private PooledDataSource dataSource;
//真正的Connection對象
private Connection realConnection;
//代理自己的代理Connection
private Connection proxyConnection;
//......
}
PooledConenction實現了InvocationHandler接口,並且,proxyConnection對象也是根據這個它來生成的代理對象:
public PooledConnection(Connection connection, PooledDataSource dataSource) {
this.hashCode = connection.hashCode();
this.realConnection = connection;
this.dataSource = dataSource;
this.createdTimestamp = System.currentTimeMillis();
this.lastUsedTimestamp = System.currentTimeMillis();
this.valid = true;
this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}
實際上,我們調用PooledDataSource的getConnection()方法返回的就是這個proxyConnection對象。當我們調用此proxyConnection對象上的任何方法時,都會調用PooledConnection對象內invoke()方法
。
讓我們看一下PooledConnection類中的invoke()方法定義:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
//當調用關閉的時候,回收此Connection到PooledDataSource中
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
dataSource.pushConnection(this);
return null;
} else {
try {
if (!Object.class.equals(method.getDeclaringClass())) {
checkConnection();
}
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
從上述代碼可以看到,當我們使用了pooledDataSource.getConnection()返回的Connection對象的close()方法時,不會調用真正Connection的close()方法,而是將此Connection對象放到連接池中
。
##8.7 JNDI類型的數據源DataSource## 對於JNDI類型的數據源DataSource的獲取就比較簡單,MyBatis定義了一個JndiDataSourceFactory工廠來創建通過JNDI形式生成的DataSource。下面讓我們看一下JndiDataSourceFactory的關鍵代碼:
if (properties.containsKey(INITIAL_CONTEXT) && properties.containsKey(DATA_SOURCE))
{
//從JNDI上下文中找到DataSource並返回
Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT));
dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE));
}
else if (properties.containsKey(DATA_SOURCE))
{
//從JNDI上下文中找到DataSource並返回
dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE));
}
#9 MyBatis事務管理機制# ##9.1 概述## 對數據庫的事務而言,應該具有以下幾點:創建(create)、提交(commit)、回滾(rollback)、關閉(close)
。對應地,MyBatis將事務抽象成了Transaction接口:
MyBatis的事務管理分爲兩種形式:
- 使用JDBC的事務管理機制:即利用java.sql.Connection對象完成對事務的提交(commit())、回滾(rollback())、關閉(close())等。
- 使用MANAGED的事務管理機制:這種機制MyBatis自身不會去實現事務管理,而是讓程序的容器如(JBOSS,Weblogic)來實現對事務的管理。
這兩者的類圖如下所示:
##9.2 事務的配置、創建和使用##
- 事務的配置
我們在使用MyBatis時,一般會在MyBatisXML配置文件中定義類似如下的信息:
<environment>節點定義了連接某個數據庫的信息,其子節點<transactionManager> 的type 會決定我們用什麼類型的事務管理機制
。
- 事務工廠的創建
MyBatis事務的創建是交給TransactionFactory 事務工廠來創建的,如果我們將<transactionManager>的type 配置爲"JDBC",那麼,在MyBatis初始化解析<environment>節點時,會根據type="JDBC"創建一個JdbcTransactionFactory工廠,其源碼如下:
/**
* 解析<transactionManager>節點,創建對應的TransactionFactory
* @param context
* @return
* @throws Exception
*/
private TransactionFactory transactionManagerElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type");
Properties props = context.getChildrenAsProperties();
/*
* 在Configuration初始化的時候,會通過以下語句,給JDBC和MANAGED對應的工廠類
* typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
* typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
* 下述的resolveClass(type).newInstance()會創建對應的工廠實例
*/
TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a TransactionFactory.");
}
如上述代碼所示,如果type = "JDBC",則MyBatis會創建一個JdbcTransactionFactory.class 實例;如果type="MANAGED",則MyBatis會創建一個MangedTransactionFactory.class實例。
MyBatis對<transactionManager>節點的解析會生成TransactionFactory實例;而對<dataSource>解析會生成datasouce實例,作爲<environment>節點,會根據TransactionFactory和DataSource實例創建一個Environment對象
,代碼如下所示:
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
if (environment == null) {
environment = context.getStringAttribute("default");
}
for (XNode child : context.getChildren()) {
String id = child.getStringAttribute("id");
//是和默認的環境相同時,解析之
if (isSpecifiedEnvironment(id)) {
//1.解析<transactionManager>節點,決定創建什麼類型的TransactionFactory
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
//2. 創建dataSource
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
//3. 使用了Environment內置的構造器Builder,傳遞id 事務工廠TransactionFactory和數據源DataSource
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}
Environment表示着一個數據庫的連接,生成後的Environment對象會被設置到Configuration實例中
,以供後續的使用。
上述一直在講事務工廠TransactionFactory來創建的Transaction,現在讓我們看一下MyBatis中的TransactionFactory的定義吧。
- 事務工廠TransactionFactory
事務工廠Transaction定義了創建Transaction的兩個方法:一個是通過指定的Connection對象創建Transaction
,另外是通過數據源DataSource來創建Transaction
。與JDBC 和MANAGED兩種Transaction相對應,TransactionFactory有兩個對應的實現的子類:
- 事務Transaction的創建
通過事務工廠TransactionFactory很容易獲取到Transaction對象實例。我們以JdbcTransaction爲例,看一下JdbcTransactionFactory是怎樣生成JdbcTransaction的,代碼如下:
public class JdbcTransactionFactory implements TransactionFactory {
public void setProperties(Properties props) {
}
/**
* 根據給定的數據庫連接Connection創建Transaction
* @param conn Existing database connection
* @return
*/
public Transaction newTransaction(Connection conn) {
return new JdbcTransaction(conn);
}
/**
* 根據DataSource、隔離級別和是否自動提交創建Transacion
*
* @param ds
* @param level Desired isolation level
* @param autoCommit Desired autocommit
* @return
*/
public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
return new JdbcTransaction(ds, level, autoCommit);
}
}
如上說是,JdbcTransactionFactory會創建JDBC類型的Transaction,即JdbcTransaction。類似地,ManagedTransactionFactory也會創建ManagedTransaction。下面我們會分別深入JdbcTranaction 和ManagedTransaction,看它們到底是怎樣實現事務管理的。
- JdbcTransaction
JdbcTransaction直接使用JDBC的提交和回滾事務管理機制
。它依賴與從dataSource中取得的連接connection 來管理transaction 的作用域,connection對象的獲取被延遲到調用getConnection()方法。如果autocommit設置爲on,開啓狀態的話,它會忽略commit和rollback。
直觀地講,就是JdbcTransaction是使用的java.sql.Connection 上的commit和rollback功能,JdbcTransaction只是相當於對java.sql.Connection事務處理進行了一次包裝(wrapper),Transaction的事務管理都是通過java.sql.Connection實現的
。JdbcTransaction的代碼實現如下:
public class JdbcTransaction implements Transaction {
private static final Log log = LogFactory.getLog(JdbcTransaction.class);
//數據庫連接
protected Connection connection;
//數據源
protected DataSource dataSource;
//隔離級別
protected TransactionIsolationLevel level;
//是否爲自動提交
protected boolean autoCommmit;
public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {
dataSource = ds;
level = desiredLevel;
autoCommmit = desiredAutoCommit;
}
public JdbcTransaction(Connection connection) {
this.connection = connection;
}
public Connection getConnection() throws SQLException {
if (connection == null) {
openConnection();
}
return connection;
}
/**
* commit()功能 使用connection的commit()
* @throws SQLException
*/
public void commit() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Committing JDBC Connection [" + connection + "]");
}
connection.commit();
}
}
/**
* rollback()功能 使用connection的rollback()
* @throws SQLException
*/
public void rollback() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Rolling back JDBC Connection [" + connection + "]");
}
connection.rollback();
}
}
/**
* close()功能 使用connection的close()
* @throws SQLException
*/
public void close() throws SQLException {
if (connection != null) {
resetAutoCommit();
if (log.isDebugEnabled()) {
log.debug("Closing JDBC Connection [" + connection + "]");
}
connection.close();
}
}
protected void setDesiredAutoCommit(boolean desiredAutoCommit) {
try {
if (connection.getAutoCommit() != desiredAutoCommit) {
if (log.isDebugEnabled()) {
log.debug("Setting autocommit to " + desiredAutoCommit + " on JDBC Connection [" + connection + "]");
}
connection.setAutoCommit(desiredAutoCommit);
}
} catch (SQLException e) {
// Only a very poorly implemented driver would fail here,
// and there's not much we can do about that.
throw new TransactionException("Error configuring AutoCommit. "
+ "Your driver may not support getAutoCommit() or setAutoCommit(). "
+ "Requested setting: " + desiredAutoCommit + ". Cause: " + e, e);
}
}
protected void resetAutoCommit() {
try {
if (!connection.getAutoCommit()) {
// MyBatis does not call commit/rollback on a connection if just selects were performed.
// Some databases start transactions with select statements
// and they mandate a commit/rollback before closing the connection.
// A workaround is setting the autocommit to true before closing the connection.
// Sybase throws an exception here.
if (log.isDebugEnabled()) {
log.debug("Resetting autocommit to true on JDBC Connection [" + connection + "]");
}
connection.setAutoCommit(true);
}
} catch (SQLException e) {
log.debug("Error resetting autocommit to true "
+ "before closing the connection. Cause: " + e);
}
}
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
connection = dataSource.getConnection();
if (level != null) {
connection.setTransactionIsolation(level.getLevel());
}
setDesiredAutoCommit(autoCommmit);
}
}
- ManagedTransaction
ManagedTransaction讓容器來管理事務Transaction的整個生命週期,意思就是說,使用ManagedTransaction的commit和rollback功能不會對事務有任何的影響,它什麼都不會做,它將事務管理的權利移交給了容器來實現
。看如下Managed的實現代碼大家就會一目瞭然:
/**
*
* 讓容器管理事務transaction的整個生命週期
* connection的獲取延遲到getConnection()方法的調用
* 忽略所有的commit和rollback操作
* 默認情況下,可以關閉一個連接connection,也可以配置它不可以關閉一個連接
* 讓容器來管理transaction的整個生命週期
* @see ManagedTransactionFactory
*/
public class ManagedTransaction implements Transaction {
private static final Log log = LogFactory.getLog(ManagedTransaction.class);
private DataSource dataSource;
private TransactionIsolationLevel level;
private Connection connection;
private boolean closeConnection;
public ManagedTransaction(Connection connection, boolean closeConnection) {
this.connection = connection;
this.closeConnection = closeConnection;
}
public ManagedTransaction(DataSource ds, TransactionIsolationLevel level, boolean closeConnection) {
this.dataSource = ds;
this.level = level;
this.closeConnection = closeConnection;
}
public Connection getConnection() throws SQLException {
if (this.connection == null) {
openConnection();
}
return this.connection;
}
public void commit() throws SQLException {
// Does nothing
}
public void rollback() throws SQLException {
// Does nothing
}
public void close() throws SQLException {
if (this.closeConnection && this.connection != null) {
if (log.isDebugEnabled()) {
log.debug("Closing JDBC Connection [" + this.connection + "]");
}
this.connection.close();
}
}
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
this.connection = this.dataSource.getConnection();
if (this.level != null) {
this.connection.setTransactionIsolation(this.level.getLevel());
}
}
}
注意:如果我們使用MyBatis構建本地程序,即不是WEB程序
,若將type設置成"MANAGED",那麼,我們執行的任何update操作,即使我們最後執行了commit操作,數據也不會保留,不會對數據庫造成任何影響
。因爲我們將MyBatis配置成了“MANAGED”,即MyBatis自己不管理事務,而我們又是運行的本地程序,沒有事務管理功能
,所以對數據庫的update操作都是無效的。
#10 MyBatis關聯查詢# MyBatis 提供了高級的關聯查詢功能,可以很方便地將數據庫獲取的結果集映射到定義的Java Bean 中。下面通過一個實例,來展示一下Mybatis對於常見的一對多和多對一關係複雜映射是怎樣處理的。
設計一個簡單的博客系統,一個用戶可以開多個博客,在博客中可以發表文章,允許發表評論,可以爲文章加標籤。博客系統主要有以下幾張表構成:
Author表:作者信息表,記錄作者的信息,用戶名和密碼,郵箱等。
Blog表:博客表,一個作者可以開多個博客,即Author和Blog的關係是一對多。
Post表:文章記錄表,記錄文章發表時間,標題,正文等信息;一個博客下可以有很多篇文章,Blog 和Post的關係是一對多。
Comments表:文章評論表,記錄文章的評論,一篇文章可以有很多個評論:Post和Comments的對應關係是一對多。
Tag表:標籤表,表示文章的標籤分類,一篇文章可以有多個標籤,而一個標籤可以應用到不同的文章上,所以Tag和Post的關係是多對多的關係;(Tag和Post的多對多關係通過Post_Tag表體現)
Post_Tag表:記錄 文章和標籤的對應關係。
一般情況下,我們會根據每一張表的結構 創建與此相對應的JavaBean(或者Pojo),來完成對錶的基本CRUD操作。
上述對單個表的JavaBean定義有時候不能滿足業務上的需求。在業務上,一個Blog對象應該有其作者的信息和一個文章列表,如下圖所示:
如果想得到這樣的類的實例,則最起碼要有一下幾步:
通過Blog 的id 到Blog表裏查詢Blog信息,將查詢到的blogId 和title 賦到Blog對象內;
根據查詢到到blog信息中的authorId 去 Author表獲取對應的author信息,獲取Author對象,然後賦到Blog對象內;
根據 blogId 去 Post表裏查詢 對應的 Post文章列表,將List<Post>對象賦到Blog對象中;
這樣的話,在底層最起碼調用三次查詢語句,請看下列的代碼:
/*
* 通過blogId獲取BlogInfo對象
*/
public static BlogInfo ordinaryQueryOnTest(String blogId)
{
BigDecimal id = new BigDecimal(blogId);
SqlSession session = sqlSessionFactory.openSession();
BlogInfo blogInfo = new BlogInfo();
//1.根據blogid 查詢Blog對象,將值設置到blogInfo中
Blog blog = (Blog)session.selectOne("com.foo.bean.BlogMapper.selectByPrimaryKey",id);
blogInfo.setBlogId(blog.getBlogId());
blogInfo.setTitle(blog.getTitle());
//2.根據Blog中的authorId,進入數據庫查詢Author信息,將結果設置到blogInfo對象中
Author author = (Author)session.selectOne("com.foo.bean.AuthorMapper.selectByPrimaryKey",blog.getAuthorId());
blogInfo.setAuthor(author);
//3.查詢posts對象,設置進blogInfo中
List posts = session.selectList("com.foo.bean.PostMapper.selectByBlogId",blog.getBlogId());
blogInfo.setPosts(posts);
//以JSON字符串的形式將對象打印出來
JSONObject object = new JSONObject(blogInfo);
System.out.println(object.toString());
return blogInfo;
}
從上面的代碼可以看出,想獲取一個BlogInfo對象比較麻煩,總共要調用三次數據庫查詢,得到需要的信息,然後再組裝BlogInfo對象。
##10.1 嵌套語句查詢## mybatis提供了一種機制,叫做嵌套語句查詢
,可以大大簡化上述的操作,加入配置及代碼如下:
<resultMap type="com.foo.bean.BlogInfo" id="BlogInfo">
<id column="blog_id" property="blogId" />
<result column="title" property="title" />
<association property="author" column="blog_author_id"
javaType="com.foo.bean.Author" select="com.foo.bean.AuthorMapper.selectByPrimaryKey">
</association>
<collection property="posts" column="blog_id" ofType="com.foo.bean.Post"
select="com.foo.bean.PostMapper.selectByBlogId">
</collection>
</resultMap>
<select id="queryBlogInfoById" resultMap="BlogInfo" parameterType="java.math.BigDecimal">
SELECT
B.BLOG_ID,
B.TITLE,
B.AUTHOR_ID AS BLOG_AUTHOR_ID
FROM LOULUAN.BLOG B
where B.BLOG_ID = #{blogId,jdbcType=DECIMAL}
</select>
/*
* 通過blogId獲取BlogInfo對象
*/
public static BlogInfo nestedQueryOnTest(String blogId)
{
BigDecimal id = new BigDecimal(blogId);
SqlSession session = sqlSessionFactory.openSession();
BlogInfo blogInfo = new BlogInfo();
blogInfo = (BlogInfo)session.selectOne("com.foo.bean.BlogMapper.queryBlogInfoById",id);
JSONObject object = new JSONObject(blogInfo);
System.out.println(object.toString());
return blogInfo;
}
通過上述的代碼完全可以實現前面的那個查詢。這裏我們在代碼裏只需要 blogInfo = (BlogInfo)session.selectOne("com.foo.bean.BlogMapper.queryBlogInfoById",id);一句即可獲取到複雜的blogInfo對象。
嵌套語句查詢的原理:
在上面的代碼中,Mybatis會執行以下流程:
先執行 queryBlogInfoById 對應的語句從Blog表裏獲取到ResultSet結果集;
取出ResultSet下一條有效記錄,然後根據resultMap定義的映射規格,通過這條記錄的數據來構建對應的一個BlogInfo 對象。
當要對BlogInfo中的author屬性進行賦值的時候,發現有一個關聯的查詢,此時Mybatis會先執行這個select查詢語句,得到返回的結果,將結果設置到BlogInfo的author屬性上;
對BlogInfo的posts進行賦值時,也有上述類似的過程。
重複2步驟,直至ResultSet. next () == false;
以下是blogInfo對象構造賦值過程示意圖:
這種關聯的嵌套查詢,有一個非常好的作用就是:可以重用select語句,通過簡單的select語句之間的組合來構造複雜的對象
。上面嵌套的兩個select語句com.foo.bean.AuthorMapper.selectByPrimaryKey和com.foo.bean.PostMapper.selectByBlogId完全可以獨立使用。
N+1問題:
它的弊端也比較明顯:即所謂的N+1問題。關聯的嵌套查詢顯示得到一個結果集,然後根據這個結果集的每一條記錄進行關聯查詢。
現在假設嵌套查詢就一個(即resultMap 內部就一個association標籤),現查詢的結果集返回條數爲N,那麼關聯查詢語句將會被執行N次,加上自身返回結果集查詢1次,共需要訪問數據庫N+1次。如果N比較大的話,這樣的數據庫訪問消耗是非常大的!所以使用這種嵌套語句查詢的使用者一定要考慮慎重考慮,確保N值不會很大。
以上面的例子爲例,select 語句本身會返回com.foo.bean.BlogMapper.queryBlogInfoById 條數爲1 的結果集,由於它有兩條關聯的語句查詢,它需要共訪問數據庫 1*(1+1)=3次數據庫。
##10.2 嵌套結果查詢## 嵌套語句的查詢會導致數據庫訪問次數不定,進而有可能影響到性能
。Mybatis還支持一種嵌套結果的查詢:即對於一對多,多對多,多對一的情況的查詢,Mybatis通過聯合查詢,將結果從數據庫內一次性查出來
,然後根據其一對多,多對一,多對多的關係和ResultMap中的配置,進行結果的轉換,構建需要的對象。
重新定義BlogInfo的結果映射 resultMap:
<resultMap type="com.foo.bean.BlogInfo" id="BlogInfo">
<id column="blog_id" property="blogId"/>
<result column="title" property="title"/>
<association property="author" column="blog_author_id" javaType="com.foo.bean.Author">
<id column="author_id" property="authorId"/>
<result column="user_name" property="userName"/>
<result column="password" property="password"/>
<result column="email" property="email"/>
<result column="biography" property="biography"/>
</association>
<collection property="posts" column="blog_post_id" ofType="com.foo.bean.Post">
<id column="post_id" property="postId"/>
<result column="blog_id" property="blogId"/>
<result column="create_time" property="createTime"/>
<result column="subject" property="subject"/>
<result column="body" property="body"/>
<result column="draft" property="draft"/>
</collection>
</resultMap>
對應的sql語句如下:
<select id="queryAllBlogInfo" resultMap="BlogInfo">
SELECT
B.BLOG_ID,
B.TITLE,
B.AUTHOR_ID AS BLOG_AUTHOR_ID,
A.AUTHOR_ID,
A.USER_NAME,
A.PASSWORD,
A.EMAIL,
A.BIOGRAPHY,
P.POST_ID,
P.BLOG_ID AS BLOG_POST_ID ,
P.CREATE_TIME,
P.SUBJECT,
P.BODY,
P.DRAFT
FROM BLOG B
LEFT OUTER JOIN AUTHOR A
ON B.AUTHOR_ID = A.AUTHOR_ID
LEFT OUTER JOIN POST P
ON P.BLOG_ID = B.BLOG_ID
</select>
/*
* 獲取所有Blog的所有信息
*/
public static BlogInfo nestedResultOnTest()
{
SqlSession session = sqlSessionFactory.openSession();
BlogInfo blogInfo = new BlogInfo();
blogInfo = (BlogInfo)session.selectOne("com.foo.bean.BlogMapper.queryAllBlogInfo");
JSONObject object = new JSONObject(blogInfo);
System.out.println(object.toString());
return blogInfo;
}
嵌套結果查詢的執行步驟:
根據表的對應關係,進行join操作,獲取到結果集;
根據結果集的信息和BlogInfo 的resultMap定義信息,對返回的結果集在內存中進行組裝、賦值,構造BlogInfo;
返回構造出來的結果List<BlogInfo> 結果。
對於關聯的結果查詢,如果是多對一的關係
,則通過形如 <association property="author" column="blog_author_id" javaType="com.foo.bean.Author"> 進行配置,Mybatis會通過column屬性對應的author_id 值去從內存中取數據,並且封裝成Author對象;
如果是一對多的關係,就如Blog和Post之間的關係
,通過形如 <collection property="posts" column="blog_post_id" ofType="com.foo.bean.Post">進行配置,MyBatis通過 blog_Id去內存中取Post對象,封裝成List<Post>;
對於關聯結果的查詢,只需要查詢數據庫一次,然後對結果的整合和組裝全部放在了內存中。
#11 MyBatis一級緩存實現# ##11.1 什麼是一級緩存? 爲什麼使用一級緩存?## 每當我們使用MyBatis開啓一次和數據庫的會話,MyBatis會創建出一個SqlSession對象表示一次數據庫會話
。
在對數據庫的一次會話中,我們有可能會反覆地執行完全相同的查詢語句,如果不採取一些措施的話,每一次查詢都會查詢一次數據庫,而我們在極短的時間內做了完全相同的查詢,那麼它們的結果極有可能完全相同,由於查詢一次數據庫的代價很大,這有可能造成很大的資源浪費。
爲了解決這一問題,減少資源的浪費,MyBatis會在表示會話的SqlSession對象中建立一個簡單的緩存,將每次查詢到的結果結果緩存起來,當下次查詢的時候,如果判斷先前有個完全一樣的查詢,會直接從緩存中直接將結果取出,返回給用戶,不需要再進行一次數據庫查詢了。
如下圖所示,MyBatis會在一次會話的表示----一個SqlSession對象中創建一個本地緩存(local cache),對於每一次查詢,都會嘗試根據查詢的條件去本地緩存中查找是否在緩存中,如果在緩存中,就直接從緩存中取出,然後返回給用戶;否則,從數據庫讀取數據,將查詢結果存入緩存並返回給用戶
。
對於會話(Session)級別的數據緩存,我們稱之爲一級數據緩存,簡稱一級緩存。
##11.2 MyBatis中的一級緩存是怎樣組織的?(即SqlSession中的緩存是怎樣組織的?)## 由於MyBatis使用SqlSession對象表示一次數據庫的會話,那麼,對於會話級別的一級緩存也應該是在SqlSession中控制的
。
實際上, MyBatis只是一個MyBatis對外的接口,SqlSession將它的工作交給了Executor執行器這個角色來完成,負責完成對數據庫的各種操作
。當創建了一個SqlSession對象時,MyBatis會爲這個SqlSession對象創建一個新的Executor執行器,而緩存信息就被維護在這個Executor執行器中
,MyBatis將緩存和對緩存相關的操作封裝成了Cache接口中。SqlSession、Executor、Cache
之間的關係如下列類圖所示:
如上述的類圖所示,Executor接口的實現類BaseExecutor中擁有一個Cache接口的實現類PerpetualCache,則對於BaseExecutor對象而言,它將使用PerpetualCache對象維護緩存
。
綜上,SqlSession對象、Executor對象、Cache對象
之間的關係如下圖所示:
由於Session級別的一級緩存實際上就是使用PerpetualCache維護的,那麼PerpetualCache是怎樣實現的呢?
PerpetualCache實現原理其實很簡單,其內部就是通過一個簡單的HashMap<k,v> 來實現的,沒有其他的任何限制
。如下是PerpetualCache的實現代碼:
package org.apache.ibatis.cache.impl;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
/**
* 使用簡單的HashMap來維護緩存
* @author Clinton Begin
*/
public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
public String getId() {
return id;
}
public int getSize() {
return cache.size();
}
public void putObject(Object key, Object value) {
cache.put(key, value);
}
public Object getObject(Object key) {
return cache.get(key);
}
public Object removeObject(Object key) {
return cache.remove(key);
}
public void clear() {
cache.clear();
}
public ReadWriteLock getReadWriteLock() {
return null;
}
public boolean equals(Object o) {
if (getId() == null) throw new CacheException("Cache instances require an ID.");
if (this == o) return true;
if (!(o instanceof Cache)) return false;
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
public int hashCode() {
if (getId() == null) throw new CacheException("Cache instances require an ID.");
return getId().hashCode();
}
}
##11.3 一級緩存的生命週期有多長?##
-
MyBatis在開啓一個數據庫會話時,會創建一個新的SqlSession對象,SqlSession對象中會有一個新的Executor對象,Executor對象中持有一個新的PerpetualCache對象;
當會話結束時,SqlSession對象及其內部的Executor對象還有PerpetualCache對象也一併釋放掉
。 -
如果
SqlSession調用了close()方法
,會釋放掉一級緩存PerpetualCache對象,一級緩存將不可用; -
如果
SqlSession調用了clearCache()
,會清空PerpetualCache對象中的數據,但是該對象仍可使用; -
SqlSession中
執行了任何一個update操作(update()、delete()、insert())
,都會清空PerpetualCache對象的數據,但是該對象可以繼續使用
;
##11.4 SqlSession 一級緩存的工作流程##
- 對於某個查詢,
根據statementId,params,rowBounds來構建一個key值
,根據這個key值去緩存Cache中取出對應的key值存儲的緩存結果; - 判斷從Cache中根據特定的key值取的數據數據是否爲空,即是否命中;
- 如果命中,則直接將緩存結果返回;
- 如果沒命中: 4.1 去數據庫中查詢數據,得到查詢結果; 4.2 將key和查詢到的結果分別作爲key,value對存儲到Cache中; 4.3 將查詢結果返回;
- 結束。
##11.5 Cache接口的設計以及CacheKey的定義## 如下圖所示,MyBatis定義了一個org.apache.ibatis.cache.Cache接口作爲其Cache提供者的SPI(Service Provider Interface)
,所有的MyBatis內部的Cache緩存,都應該實現這一接口
。MyBatis定義了一個PerpetualCache實現類實現了Cache接口,實際上,在SqlSession對象裏的Executor對象內維護的Cache類型實例對象,就是PerpetualCache子類創建的
。
(MyBatis內部還有很多Cache接口的實現,一級緩存只會涉及到這一個PerpetualCache子類,Cache的其他實現將會放到二級緩存中介紹)。
我們知道,Cache最核心的實現其實就是一個Map,將本次查詢使用的特徵值作爲key,將查詢結果作爲value存儲到Map中。現在最核心的問題出現了:怎樣來確定一次查詢的特徵值?
換句話說就是:怎樣判斷某兩次查詢是完全相同的查詢?
也可以這樣說:如何確定Cache中的key值?
MyBatis認爲,對於兩次查詢,如果以下條件都完全一樣,那麼就認爲它們是完全相同的兩次查詢:
傳入的 statementId
查詢時要求的結果集中的結果範圍 (結果的範圍通過rowBounds.offset和rowBounds.limit表示)
這次查詢所產生的最終要傳遞給JDBC java.sql.Preparedstatement的Sql語句字符串(boundSql.getSql() )
傳遞給java.sql.Statement要設置的參數值
現在分別解釋上述四個條件:
-
傳入的statementId,對於MyBatis而言,你要使用它,
必須需要一個statementId,它代表着你將執行什麼樣的Sql
; -
MyBatis自身提供的分頁功能是通過RowBounds來實現的,它通過rowBounds.offset和rowBounds.limit來過濾查詢出來的結果集,這種分頁功能是基於查詢結果的再過濾,而不是進行數據庫的物理分頁;
-
由於MyBatis底層還是依賴於JDBC實現的,那麼,對於兩次完全一模一樣的查詢,MyBatis要保證對於底層JDBC而言,也是完全一致的查詢纔行。而對於JDBC而言,兩次查詢,只要傳入給JDBC的SQL語句完全一致,傳入的參數也完全一致,就認爲是兩次查詢是完全一致的。
-
上述的第3個條件正是要求保證傳遞給JDBC的SQL語句完全一致;第4條則是保證傳遞給JDBC的參數也完全一致;即3、4兩條MyBatis最本質的要求就是:
調用JDBC的時候,傳入的SQL語句要完全相同,傳遞給JDBC的參數值也要完全相同
。
綜上所述,CacheKey由以下條件決定:statementId + rowBounds + 傳遞給JDBC的SQL + 傳遞給JDBC的參數值
;
- CacheKey的創建
對於每次的查詢請求,Executor都會根據傳遞的參數信息以及動態生成的SQL語句,將上面的條件根據一定的計算規則,創建一個對應的CacheKey對象。
我們知道創建CacheKey的目的,就兩個:
根據CacheKey作爲key,去Cache緩存中查找緩存結果;
如果查找緩存命中失敗,則通過此CacheKey作爲key,將從數據庫查詢到的結果作爲value,組成key,value對存儲到Cache緩存中;
CacheKey的構建被放置到了Executor接口的實現類BaseExecutor中,定義如下:
/**
* 所屬類: org.apache.ibatis.executor.BaseExecutor
* 功能 : 根據傳入信息構建CacheKey
*/
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) throw new ExecutorException("Executor was closed.");
CacheKey cacheKey = new CacheKey();
//1.statementId
cacheKey.update(ms.getId());
//2. rowBounds.offset
cacheKey.update(rowBounds.getOffset());
//3. rowBounds.limit
cacheKey.update(rowBounds.getLimit());
//4. SQL語句
cacheKey.update(boundSql.getSql());
//5. 將每一個要傳遞給JDBC的參數值也更新到CacheKey中
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
for (int i = 0; i < parameterMappings.size(); i++) { // mimic DefaultParameterHandler logic
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
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);
}
//將每一個要傳遞給JDBC的參數值也更新到CacheKey中
cacheKey.update(value);
}
}
return cacheKey;
}
- CacheKey的hashcode生成算法
剛纔已經提到,Cache接口的實現,本質上是使用的HashMap<k,v>,而構建CacheKey的目的就是爲了作爲HashMap<k,v>中的key值。而HashMap是通過key值的hashcode 來組織和存儲的,那麼,構建CacheKey的過程實際上就是構造其hashCode的過程
。下面的代碼就是CacheKey的核心hashcode生成算法,感興趣的話可以看一下:
public void update(Object object) {
if (object != null && object.getClass().isArray()) {
int length = Array.getLength(object);
for (int i = 0; i < length; i++) {
Object element = Array.get(object, i);
doUpdate(element);
}
} else {
doUpdate(object);
}
}
private void doUpdate(Object object) {
//1. 得到對象的hashcode;
int baseHashCode = object == null ? 1 : object.hashCode();
//對象計數遞增
count++;
checksum += baseHashCode;
//2. 對象的hashcode 擴大count倍
baseHashCode *= count;
//3. hashCode * 拓展因子(默認37)+拓展擴大後的對象hashCode值
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
MyBatis認爲的完全相同的查詢,不是指使用sqlSession查詢時傳遞給算起來Session的所有參數值完完全全相同,你只要保證statementId,rowBounds,最後生成的SQL語句,以及這個SQL語句所需要的參數完全一致就可以了。
##11.6 一級緩存的性能分析##
- MyBatis對會話(Session)級別的一級緩存設計的比較簡單,就簡單地使用了HashMap來維護,並沒有對HashMap的容量和大小進行限制
讀者有可能就覺得不妥了:如果我一直使用某一個SqlSession對象查詢數據,這樣會不會導致HashMap太大,而導致 java.lang.OutOfMemoryError錯誤啊?
讀者這麼考慮也不無道理,不過MyBatis的確是這樣設計的。
MyBatis這樣設計也有它自己的理由:
a. 一般而言SqlSession的生存時間很短。一般情況下使用一個SqlSession對象執行的操作不會太多,執行完就會消亡;
b. 對於某一個SqlSession對象而言,只要執行update操作(update、insert、delete),都會將這個SqlSession對象中對應的一級緩存清空掉,所以一般情況下不會出現緩存過大,影響JVM內存空間的問題;
c. 可以手動地釋放掉SqlSession對象中的緩存。
- 一級緩存是一個粗粒度的緩存,沒有更新緩存和緩存過期的概念
MyBatis的一級緩存就是使用了簡單的HashMap,MyBatis只負責將查詢數據庫的結果存儲到緩存中去, 不會去判斷緩存存放的時間是否過長、是否過期,因此也就沒有對緩存的結果進行更新這一說了。
根據一級緩存的特性,在使用的過程中,我認爲應該注意:
對於數據變化頻率很大,並且需要高時效準確性的數據要求,我們使用SqlSession查詢的時候,要控制好SqlSession的生存時間,SqlSession的生存時間越長,它其中緩存的數據有可能就越舊,從而造成和真實數據庫的誤差;同時對於這種情況,用戶也可以手動地適時清空SqlSession中的緩存;
對於只執行、並且頻繁執行大範圍的select操作的SqlSession對象,SqlSession對象的生存時間不應過長。
#12 MyBatis二級緩存實現# MyBatis的二級緩存是Application級別的緩存
,它可以提高對數據庫查詢的效率,以提高應用的性能。 ##12.1 MyBatis的緩存機制整體設計以及二級緩存的工作模式##
如上圖所示,當開一個會話時,一個SqlSession對象會使用一個Executor對象來完成會話操作,MyBatis的二級緩存機制的關鍵就是對這個Executor對象做文章
。如果用戶配置了"cacheEnabled=true"
,那麼MyBatis在爲SqlSession對象創建Executor對象時,會對Executor對象加上一個裝飾者:CachingExecutor
,這時SqlSession使用CachingExecutor對象來完成操作請求。CachingExecutor對於查詢請求,會先判斷該查詢請求在Application級別的二級緩存中是否有緩存結果
,如果有查詢結果,則直接返回緩存結果;如果緩存中沒有,再交給真正的Executor對象來完成查詢操作,之後CachingExecutor會將真正Executor返回的查詢結果放置到緩存中
,然後在返回給用戶。
CachingExecutor是Executor的裝飾者,以增強Executor的功能,使其具有緩存查詢的功能,這裏用到了設計模式中的裝飾者模式
,CachingExecutor和Executor的接口的關係如下類圖所示:
##12.2 MyBatis二級緩存的劃分## MyBatis並不是簡單地對整個Application就只有一個Cache緩存對象,它將緩存劃分的更細,即是Mapper級別的,即每一個Mapper都可以擁有一個Cache對象,具體如下:
- 爲每一個Mapper分配一個Cache緩存對象(使用<cache>節點配置)
MyBatis將Application級別的二級緩存細分到Mapper級別
,即對於每一個Mapper.xml,如果在其中使用了<cache> 節點,則MyBatis會爲這個Mapper創建一個Cache緩存對象,如下圖所示:
注:上述的每一個Cache對象,都會有一個自己所屬的namespace命名空間,並且會將Mapper的 namespace作爲它們的ID;
- 多個Mapper共用一個Cache緩存對象(使用<cache-ref>節點配置)
如果你想讓多個Mapper公用一個Cache的話,你可以使用<cache-ref namespace="">節點,來指定你的這個Mapper使用到了哪一個Mapper的Cache緩存。
##12.3 使用二級緩存,必須要具備的條件## MyBatis對二級緩存的支持粒度很細,它會指定某一條查詢語句是否使用二級緩存
。
雖然在Mapper中配置了<cache>,並且爲此Mapper分配了Cache對象,這並不表示我們使用Mapper中定義的查詢語句查到的結果都會放置到Cache對象之中
,我們必須指定Mapper中的某條選擇語句是否支持緩存,即如下所示,在<select> 節點中配置useCache="true",Mapper纔會對此Select的查詢支持緩存特性
,否則,不會對此Select查詢,不會經過Cache緩存。如下所示,Select語句配置了useCache="true",則表明這條Select語句的查詢會使用二級緩存。
<select id="selectByMinSalary" resultMap="BaseResultMap" parameterType="java.util.Map" useCache="true">
總之,要想使某條Select查詢支持二級緩存,你需要保證:
MyBatis支持二級緩存的總開關:全局配置變量參數 cacheEnabled=true
該select語句所在的Mapper,配置了<cache> 或<cached-ref>節點,並且有效
該select語句的參數 useCache=true
##12.4 一級緩存和二級緩存的使用順序## 請注意,如果你的MyBatis使用了二級緩存,並且你的Mapper和select語句也配置使用了二級緩存,那麼在執行select查詢的時候,MyBatis會先從二級緩存中取輸入,其次纔是一級緩存,即MyBatis查詢數據的順序是:二級緩存 ———> 一級緩存 ——> 數據庫
。
##12.5 二級緩存實現的選擇## MyBatis對二級緩存的設計非常靈活,它自己內部實現了一系列的Cache緩存實現類,並提供了各種緩存刷新策略如LRU,FIFO等等
;另外,MyBatis還允許用戶自定義Cache接口實現,用戶是需要實現org.apache.ibatis.cache.Cache接口,然後將Cache實現類配置在<cache type="">節點的type屬性上即可;除此之外,MyBatis還支持跟第三方內存緩存庫如Memecached的集成,總之,使用MyBatis的二級緩存有三個選擇:
MyBatis自身提供的緩存實現;
用戶自定義的Cache接口實現;
跟第三方內存緩存庫的集成;
##12.6 MyBatis自身提供的二級緩存的實現## MyBatis自身提供了豐富的,並且功能強大的二級緩存的實現,它擁有一系列的Cache接口裝飾者,可以滿足各種對緩存操作和更新的策略。
MyBatis定義了大量的Cache的裝飾器來增強Cache緩存的功能,如下類圖所示。
對於每個Cache而言,都有一個容量限制,MyBatis各供了各種策略來對Cache緩存的容量進行控制,以及對Cache中的數據進行刷新和置換。MyBatis主要提供了以下幾個刷新和置換策略:
LRU:(Least Recently Used),最近最少使用算法,即如果緩存中容量已經滿了,會將緩存中最近最少被使用的緩存記錄清除掉,然後添加新的記錄;
FIFO:(First in first out),先進先出算法,如果緩存中的容量已經滿了,那麼會將最先進入緩存中的數據清除掉;
Scheduled:指定時間間隔清空算法,該算法會以指定的某一個時間間隔將Cache緩存中的數據清空;
#13 如何細粒度地控制你的MyBatis二級緩存# ##13.1 一個關於MyBatis的二級緩存的實際問題## 現有AMapper.xml中定義了對數據庫表 ATable 的CRUD操作,BMapper定義了對數據庫表BTable的CRUD操作;
假設 MyBatis 的二級緩存開啓,並且 AMapper 中使用了二級緩存,AMapper對應的二級緩存爲ACache;
除此之外,AMapper 中還定義了一個跟BTable有關的查詢語句,類似如下所述:
<select id="selectATableWithJoin" resultMap="BaseResultMap" useCache="true">
select * from ATable left join BTable on ....
</select>
執行以下操作:
- 執行AMapper中的"selectATableWithJoin" 操作,此時會將查詢到的結果放置到AMapper對應的二級緩存ACache中;
- 執行BMapper中對BTable的更新操作(update、delete、insert)後,BTable的數據更新;
- 再執行1完全相同的查詢,這時候會直接從AMapper二級緩存ACache中取值,將ACache中的值直接返回;
好,問題就出現在第3步上:
由於AMapper的“selectATableWithJoin” 對應的SQL語句需要和BTable進行join查找,而在第 2 步BTable的數據已經更新了,但是第 3 步查詢的值是第 1 步的緩存值,已經極有可能跟真實數據庫結果不一樣,即ACache中緩存數據過期了!
總結來看,就是:
對於某些使用了 join連接的查詢,如果其關聯的表數據發生了更新,join連接的查詢由於先前緩存的原因,導致查詢結果和真實數據不同步;
從MyBatis的角度來看,這個問題可以這樣表述:
對於某些表執行了更新(update、delete、insert)操作後,如何去清空跟這些表有關聯的查詢語句所造成的緩存;
##13.2 當前MyBatis二級緩存的工作機制##
MyBatis二級緩存的一個重要特點:即鬆散的Cache緩存管理和維護
一個Mapper中定義的增刪改查操作只能影響到自己關聯的Cache對象
。如上圖所示的Mapper namespace1中定義的若干CRUD語句,產生的緩存只會被放置到相應關聯的Cache1中,即Mapper namespace2,namespace3,namespace4 中的CRUD的語句不會影響到Cache1。
可以看出,Mapper之間的緩存關係比較鬆散,相互關聯的程度比較弱。
現在再回到上面描述的問題,如果我們將AMapper和BMapper共用一個Cache對象
,那麼,當BMapper執行更新操作時,可以清空對應Cache中的所有的緩存數據,這樣的話,數據不是也可以保持最新嗎?
確實這個也是一種解決方案,不過,它會使緩存的使用效率變的很低!
AMapper和BMapper的任意的更新操作都會將共用的Cache清空,會頻繁地清空Cache,導致Cache實際的命中率和使用率就變得很低了,所以這種策略實際情況下是不可取的。
最理想的解決方案就是:
**對於某些表執行了更新(update、delete、insert)操作後,如何去清空跟這些表有關聯的查詢語句所造成的緩存;**這樣,就是以很細的粒度管理MyBatis內部的緩存,使得緩存的使用率和準確率都能大大地提升。
##13.3 mybatis-enhanced-cache插件的設計和工作原理## 該插件主要由兩個構件組成:EnhancedCachingExecutor和EnhancedCachingManager
。源碼地址:https://github.com/LuanLouis/mybatis-enhanced-cache。
EnhancedCachingExecutor是針對於Executor的攔截器,攔截Executor的幾個關鍵的方法;EnhancedCachingExecutor主要做以下幾件事:
-
每當有Executor執行query操作時, 1.1 記錄下該查詢StatementId和CacheKey,然後將其添加到EnhancedCachingManager中; 1.2 記錄下該查詢StatementId和此StatementId所屬Mapper內的Cache緩存對象引用,添加到EnhancedCachingManager中;
-
每當Executor執行了update操作時,將此update操作的StatementId傳遞給EnhancedCachingManager,讓EnhancedCachingManager根據此update的StatementId的配置,去清空指定的查詢語句所產生的緩存;
另一個構件:EnhancedCachingManager,它也是本插件的核心,它維護着以下幾樣東西:
-
整個MyBatis的所有查詢所產生的CacheKey集合(以statementId分類);
-
所有的使用過了的查詢的statementId 及其對應的Cache緩存對象的引用;
-
update類型的StatementId和查詢StatementId集合的映射,用於當Update類型的語句執行時,根據此映射決定應該清空哪些查詢語句產生的緩存;
如下圖所示:
原理很簡單,就是 當執行了某個update操作時,根據配置信息去清空指定的查詢語句在Cache中所產生的緩存數據。
##13.4 mybatis-enhanced-cache 插件的使用實例##
- 配置MyBatis配置文件
<plugins>
<plugin interceptor="org.luanlouis.mybatis.plugin.cache.EnhancedCachingExecutor">
<property name="dependency" value="dependencys.xml"/>
<property name="cacheEnabled" value="true"/>
</plugin>
</plugins>
其中,<property name="dependency"> 中的value屬性是 StatementId之間的依賴關係的配置文件路徑。
- 配置StatementId之間的依賴關係
<?xml version="1.0" encoding="UTF-8"?>
<dependencies>
<statements>
<statement id="com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey">
<observer id="com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments" />
</statement>
</statements>
</dependencies>
<statement>節點配置的是更新語句的statementId,其內的子節點<observer> 配置的是當更新語句執行後,應當清空緩存的查詢語句的StatementId。子節點<observer>可以有多個。
如上的配置,則說明,如果"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey" 更新語句執行後,由 “com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments” 語句所產生的放置在Cache緩存中的數據都都會被清空。
- 配置DepartmentsMapper.xml 和EmployeesMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.louis.mybatis.dao.DepartmentsMapper" >
<cache></cache>
<resultMap id="BaseResultMap" type="com.louis.mybatis.model.Department" >
<id column="DEPARTMENT_ID" property="departmentId" jdbcType="DECIMAL" />
<result column="DEPARTMENT_NAME" property="departmentName" jdbcType="VARCHAR" />
<result column="MANAGER_ID" property="managerId" jdbcType="DECIMAL" />
<result column="LOCATION_ID" property="locationId" jdbcType="DECIMAL" />
</resultMap>
<sql id="Base_Column_List" >
DEPARTMENT_ID, DEPARTMENT_NAME, MANAGER_ID, LOCATION_ID
</sql>
<update id="updateByPrimaryKey" parameterType="com.louis.mybatis.model.Department" >
update HR.DEPARTMENTS
set DEPARTMENT_NAME = #{departmentName,jdbcType=VARCHAR},
MANAGER_ID = #{managerId,jdbcType=DECIMAL},
LOCATION_ID = #{locationId,jdbcType=DECIMAL}
where DEPARTMENT_ID = #{departmentId,jdbcType=DECIMAL}
</update>
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
select
<include refid="Base_Column_List" />
from HR.DEPARTMENTS
where DEPARTMENT_ID = #{departmentId,jdbcType=DECIMAL}
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.louis.mybatis.dao.EmployeesMapper">
<cache eviction="LRU" flushInterval="100000" size="10000"/>
<resultMap id="BaseResultMap" type="com.louis.mybatis.model.Employee">
<id column="EMPLOYEE_ID" jdbcType="DECIMAL" property="employeeId" />
<result column="FIRST_NAME" jdbcType="VARCHAR" property="firstName" />
<result column="LAST_NAME" jdbcType="VARCHAR" property="lastName" />
<result column="EMAIL" jdbcType="VARCHAR" property="email" />
<result column="PHONE_NUMBER" jdbcType="VARCHAR" property="phoneNumber" />
<result column="HIRE_DATE" jdbcType="DATE" property="hireDate" />
<result column="JOB_ID" jdbcType="VARCHAR" property="jobId" />
<result column="SALARY" jdbcType="DECIMAL" property="salary" />
<result column="COMMISSION_PCT" jdbcType="DECIMAL" property="commissionPct" />
<result column="MANAGER_ID" jdbcType="DECIMAL" property="managerId" />
<result column="DEPARTMENT_ID" jdbcType="DECIMAL" property="departmentId" />
</resultMap>
<sql id="Base_Column_List">
EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY,
COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID
</sql>
<select id="selectWithDepartments" parameterType="java.lang.Integer" resultMap="BaseResultMap" useCache="true" >
select
*
from HR.EMPLOYEES t left join HR.DEPARTMENTS S ON T.DEPARTMENT_ID = S.DEPARTMENT_ID
where EMPLOYEE_ID = #{employeeId,jdbcType=DECIMAL}
</select>
</mapper>
- 測
結果分析:
從上述的結果可以看出,前四次執行了“com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments”語句,EmployeesMapper對應的Cache緩存中存儲的結果緩存有1個增加到4個。
當執行了"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey"後,EmployeeMapper對應的緩存Cache結果被清空了,即"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey"更新語句引起了EmployeeMapper中的"com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments"緩存的清空。