#0 系列目錄#java
#1 引言# 本文主要講解JDBC怎麼演變到Mybatis的漸變過程,重點講解了爲何要將JDBC封裝成Mybaits這樣一個持久層框架。再而論述Mybatis做爲一個數據持久層框架自己有待改進之處。git
#2 JDBC實現查詢分析# 咱們先看看咱們最熟悉也是最基礎的經過JDBC查詢數據庫數據,通常須要如下七個步驟:程序員
加載JDBC驅動;github
創建並獲取數據庫鏈接;算法
建立 JDBC Statements 對象;sql
設置SQL語句的傳入參數;數據庫
執行SQL語句並得到查詢結果;apache
對查詢結果進行轉換處理並將處理結果返回;編程
釋放相關資源(關閉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 會直接將緩存結果返回給用戶,而不用再到數據庫中查找。
傳統的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 = factory.openSession();
MyBatis封裝了對數據庫的訪問,把對數據庫的會話和事務控制放到了SqlSession對象中。
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執行器來執行具體的操做
。
/** * 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查詢結果;
接着上面的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) 方法實現 */ 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對象的 ? 佔位符處進行賦值
。
/** * 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);
如今就讓咱們看看第三句到底發生了什麼。
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
當SqlSessionFactoryBuilder執行build()方法,調用了XMLConfigBuilder的parse()方法,而後返回了Configuration對象
。那麼parse()方法是如何處理XML文件,生成Configuration對象的呢?將XML配置文件的信息轉換爲Document對象
,而XML配置定義文件DTD轉換成XMLMapperEntityResolver對象
,而後將兩者封裝到XpathParser對象中
,XpathParser的做用是提供根據Xpath表達式獲取基本的DOM節點Node信息的操做
。以下圖所示:會從XPathParser中取出 <configuration>節點對應的Node對象,而後解析此Node節點的子Node
:properties, settings, typeAliases,typeHandlers, objectFactory, objectWrapperFactory, plugins, environments,databaseIdProvider, mappers: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的配置和設計上。
解析子節點的過程這裏就不一一介紹了,用戶能夠參照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; }
將上述的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>元素來配置數據源:
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
其類圖以下所示:
將其放到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對象的資源消耗。咱們經過鏈接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鏈接池的大體結構以下所示:
下面讓咱們看一下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對象
。
當咱們的程序中使用完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的事務管理分爲兩種形式:
這二者的類圖以下所示:
##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的定義吧。
事務工廠Transaction定義了建立Transaction的兩個方法:一個是經過指定的Connection對象建立Transaction
,另外是經過數據源DataSource來建立Transaction
。與JDBC 和MANAGED兩種Transaction相對應,TransactionFactory有兩個對應的實現的子類:
經過事務工廠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直接使用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讓容器來管理事務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值存儲的緩存結果;##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的參數也徹底一致;即三、4兩條MyBatis最本質的要求就是:調用JDBC的時候,傳入的SQL語句要徹底相同,傳遞給JDBC的參數值也要徹底相同
。
綜上所述,CacheKey由如下條件決定:statementId + rowBounds + 傳遞給JDBC的SQL + 傳遞給JDBC的參數值
;
對於每次的查詢請求,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; }
剛纔已經提到,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 一級緩存的性能分析##
讀者有可能就以爲不妥了:若是我一直使用某一個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對象,具體以下:
MyBatis將Application級別的二級緩存細分到Mapper級別
,即對於每個Mapper.xml,若是在其中使用了<cache> 節點,則MyBatis會爲這個Mapper建立一個Cache緩存對象,以下圖所示:
注:上述的每個Cache對象,都會有一個本身所屬的namespace命名空間,而且會將Mapper的 namespace做爲它們的ID;
若是你想讓多個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>
執行如下操做:
好,問題就出如今第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 插件的使用實例##
<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之間的依賴關係的配置文件路徑。
<?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緩存中的數據都都會被清空。
<?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>
public class SelectDemo3 { private static final Logger loger = Logger.getLogger(SelectDemo3.class); public static void main(String[] args) throws Exception { InputStream inputStream = Resources.getResourceAsStream("mybatisConfig.xml"); SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(inputStream); SqlSession sqlSession = factory.openSession(true); SqlSession sqlSession2 = factory.openSession(true); //3.使用SqlSession查詢 Map<String,Object> params = new HashMap<String,Object>(); params.put("employeeId",10); //a.查詢工資低於10000的員工 Date first = new Date(); //第一次查詢 List<Employee> result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params); sqlSession.commit(); checkCacheStatus(sqlSession); params.put("employeeId", 11); result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params); sqlSession.commit(); checkCacheStatus(sqlSession); params.put("employeeId", 12); result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params); sqlSession.commit(); checkCacheStatus(sqlSession); params.put("employeeId", 13); result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params); sqlSession.commit(); checkCacheStatus(sqlSession); Department department = sqlSession.selectOne("com.louis.mybatis.dao.DepartmentsMapper.selectByPrimaryKey",10); department.setDepartmentName("updated"); sqlSession2.update("com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey", department); sqlSession.commit(); checkCacheStatus(sqlSession); } public static void checkCacheStatus(SqlSession sqlSession) { loger.info("------------Cache Status------------"); Iterator<String> iter = sqlSession.getConfiguration().getCacheNames().iterator(); while(iter.hasNext()) { String it = iter.next(); loger.info(it+":"+sqlSession.getConfiguration().getCache(it).getSize()); } loger.info("------------------------------------"); } }
結果分析:
從上述的結果能夠看出,前四次執行了「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"緩存的清空。