【mybatis】完全解讀mybatis JDBC事務

前言

        本文debug mybatis源碼版本:3.2.7

        mybatis提供了JDBC和MANAGED兩種事務管理,本文主要討論的是JDBC事務管理方式。

        如果對本文理解不夠詳細,請先看另一篇博文,模擬實現mybatis數據源和事務:《動態代理+ThreadLocal實現數據源及事務管理》

        本文主要討論的內容:

                1. mybatis JDBC事務的開始、提交、回滾和關閉

                2. mybatis JDBC事務中connection的多線程安全


一、mybatis的初始化核心:被拋棄的發動機SqlSessionManager

        可以這麼錯覺的認爲,使整個mybatis框架跑起來的是SqlSessionManager,他既是mybatis的“發動機”,也是mybatis的“方向盤”。但是這個類似乎已經被棄用了,被DefaultSqlSessionFactory和DefaultSqlSession替代了,但實際上他們幾乎是平等的關係,近乎1=2的概念。我也正在積極尋找被棄用的原因,至少目前從調試框架的結果來看,這個類的確沒有被使。但是這個類的思想對於更好的瞭解Factory和session還是很有幫助的。

1. SqlSessionFactoryBuilder和Configuration

        mybatis的起點是SqlSessionFactoryBuilder,他通過builder方法加載mybatis-config.xml配置文件,從而使得整個框架獲得“跑動“的環境條件。SqlSessionFactoryBuilder有9個builder方法,四個和字節流有關,四個和字符流有關,完全的一一對應,而所有和流有關的builder方法的最終落點是build(Configuration config),也就是說無論是字節流還是字符流,他們最終都是把讀取xml配置文件的流輸送給xml的解析對象XMLConfigBuilder,而XMLConfigBuilder對象的終極目的就是parse注入的流成爲Configuration對象,而Configuration對象幾乎將配置文件中的配置標籤一一對應的解析成了與之一致的對象,他就是mybatis框架的配置中心。

package org.apache.ibatis.session;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.Properties;
import org.apache.ibatis.builder.xml.XMLConfigBuilder;
import org.apache.ibatis.exceptions.ExceptionFactory;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.session.defaults.DefaultSqlSessionFactory;

public class SqlSessionFactoryBuilder {
	/**
	 * 所有的字符流builder的最終落點爲該方法
	 * 最終將流builder方法傳入的流注入到XMLConfigBuilder對象,並通過其parser方法將流解析成Configuration對象
	 * 解析生成的Configuration對象最終被傳入最後一個builder方法構造SqlSessionFactory
	 */
	public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
		try {
			XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
			return build(parser.parse());
		} catch (Exception e) {
			throw ExceptionFactory.wrapException("Error building SqlSession.", e);
		} finally {
			ErrorContext.instance().reset();
			try {
				reader.close();
			} catch (IOException e) {
				// Intentionally ignore. Prefer previous error.
			}
		}
	}
	/**
	 * 所有字節流有關的builder方法的最終落點,該方法的作用和字符流一致,只是傳入的流不同而已
	 */
	public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
		try {
			XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
			return build(parser.parse());
		} 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.
			}
		}
	}
	/**
	 * 所有與流有關的builder方法的最終落點,傳入一個由XMLConfigBuilder.parser生成的Configuration對象
	 * 最終通過Configuration對象生成SqlSessionFactory
	 * 實際上SqlSessionFactory有兩個子類,一個是DefaultSqlSessionFactory,另一個就是SqlSesisonManager
	 * 下一節詳解這兩個對象
	 */
	public SqlSessionFactory build(Configuration config) {
		return new DefaultSqlSessionFactory(config);
	}
}

        總結:所有和流有關係的8個builder方法,只是爲了把流注入XMLConfigBuilder對象,解析生成Configuration,從而從最終的builder(Configuration config)方法中構造出SqlSessionFactory(實際上是DefaultSqlSessionFactory對象,下一節詳解)。

2. SqlSessionFactory和SqlSession:雙重規範的發動機SqlSessioinManager

        SqlSessionFactory是工廠,而SqlSessio n是會話,工廠意味着生產中心,而會話則意味着操作中心。

        SqlSessionManager既實現了SqlSessionFactory規範,也實現了SqlSession規範,實際上工廠和會話除了SqlSessionManager之外,還各有一個默認的子類,分別是DefaultSqlSessionFactory和DefaultSqlSession(關係如下圖所示)。他們三者之間的關係:SqlSessionManager通過裝飾器模式獲得了DefaultSqlSessionFactory的所有能力,即openSession的能力,通過SqlSessionManager獲得的SqlSession實際上全部來自DefaultSqlSessionFactory;SqlSessionManager在構造的時候就通過JDK動態代理獲得了DefaultSqlSession的代理對象,代理的內容是一個判斷是否存在本地SqlSession的生成邏輯,進而也獲得了DefaultSqlSession和數據庫交互的所有能力。


        因爲被棄用的緣故,在此不再討論起源碼的思想,只是建議如果想要深入瞭解的話,可以配合DefaultSqlSessionFactory和DefaultSqlSession來一起學習,效果更甚!

        而實際上整個mybatis框架中真正使用的是DefaultSqlSessionFactory和DefaultSqlSession,前者用於生產後者,即通過工廠openSession得到了DefaultSqlSession,而DefaultSqlSession封裝了和數據庫交互的所有邏輯,注意,是除了開啓事務之外的所有邏輯。


二、JDBC事務IUD(insert, update, delete)

        ---------------------------------------------------------

        a. 事務開啓的時機:事務到底在哪裏開啓?

        b. select到底有沒有開啓事務?

        c. 業務層中多個select到底的事務是怎麼樣的?

        d. IUD操作事務在什麼時候開啓的?

        e. 業務層中多個IUD操作的事務是什麼樣的?

        ........

        ---------------------------------------------------------

        如果你想知道,請往下看。本處以insert操作爲例進行進行討論,insert示例源碼如下,本節所有內容圍繞這段示例代碼展開。

SqlSession session = factory.openSession();
session.insert("test.insertUser", new User(null, "zhangsan", "123", "male", 26));
session.insert("test.insertUser", new User(null, "lisi", "123", "male", 26));
session.commit();
session.close();

        如以上示例代碼,進行了兩次insert(首先明確,事務不是在openSession中開啓),但是事務只開啓了一次,如果你不想更深入的瞭解,可以直接看debug的日誌驗證邏輯(如下圖debug日誌截圖),如果你想了解的更多,請往下看,爲什麼mybatis知道只在第一次IUD的時候開啓事務?


        CSDN的圖片上傳,我就艹了······日誌節選信息如下所示。

2015-05-10 01:24:54,209 DEBUG JdbcTransaction:132 - Opening JDBC Connection
2015-05-10 01:24:54,647 DEBUG PooledDataSource:380 - Created connection 319977154.
2015-05-10 01:24:54,647 DEBUG JdbcTransaction:98 - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@131276c2]
2015-05-10 01:24:54,647 DEBUG insertUser:139 - ==>  Preparing: insert into tbl_user(username, password, gender, age) values(?, ?, ?, ?) 
2015-05-10 01:24:54,710 DEBUG insertUser:139 - ==> Parameters: zhangsan(String), 123(String), male(String), 26(Integer)
2015-05-10 01:24:54,725 DEBUG insertUser:139 - <==    Updates: 1
2015-05-10 01:24:54,725 DEBUG insertUser:139 - ==>  Preparing: insert into tbl_user(username, password, gender, age) values(?, ?, ?, ?) 
2015-05-10 01:24:54,725 DEBUG insertUser:139 - ==> Parameters: lisi(String), 123(String), male(String), 26(Integer)
2015-05-10 01:24:54,725 DEBUG insertUser:139 - <==    Updates: 1
2015-05-10 01:24:54,725 DEBUG JdbcTransaction:69 - Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@131276c2]
2015-05-10 01:24:54,756 DEBUG JdbcTransaction:120 - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@131276c2]
2015-05-10 01:24:54,756 DEBUG JdbcTransaction:88 - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@131276c2]
2015-05-10 01:24:54,756 DEBUG PooledDataSource:334 - Returned connection 319977154 to pool.

1. 構建SqlSession

        要想進行數據庫操作,首先要獲得SqlSession對象,那麼factory.openSession到底做了什麼?
        爲了更好的理解以下代碼,你還需要自己先了解JdbcTransactionFactory、JdbcTransaction、BaseExecutor、SimpleExecutor、DefaultSqlSession的源碼,幸好這些類的源碼錶示的邏輯都不會太難。
package org.apache.ibatis.session.defaults;

import java.sql.Connection;
import java.sql.SQLException;

import org.apache.ibatis.exceptions.ExceptionFactory;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.TransactionIsolationLevel;
import org.apache.ibatis.session.defaults.DefaultSqlSession;
import org.apache.ibatis.transaction.Transaction;
import org.apache.ibatis.transaction.TransactionFactory;
import org.apache.ibatis.transaction.managed.ManagedTransactionFactory;

public class DefaultSqlSessionFactory implements SqlSessionFactory {
	/**
	 * 獲取SqlSession的方法,轉調private方法
	 */
	public SqlSession openSession() {
		return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
	}
	/**
	 * 實際上在openSession的時候並沒有獲取Connection,也沒有開啓事務(設置自動提交爲false)
	 * 只是初始化了數據庫操作的必須要對象(和mybatis的架構有關)
	 * 1. 根據配置文件,初始化了事務對象,但是沒有開啓事務
	 * 2. 根據調用的openSession方法,初始化了執行器,默認是SimpleExecutor執行器
	 * 		mybatis所有的事務控制(開啓、提交、回滾、關閉)的最終落點都在Executor中
	 * 		實際上,Executor不僅僅是事務控制的最終落點,實際上緩存、sql語句的生成也在這裏,他纔是真正的“發動機”
	 */
	private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
		Transaction tx = null;
		try {
			final Environment environment = configuration.getEnvironment();
			//根據配置文件中配置的類型獲得事務工廠:JdbcTransactionFactory和ManagedTransactionFactory
			//事務工廠都很簡單,主要邏輯落在事務(JdbcTranasaction和ManagedTransaction中)
			//工廠的規範:org.apache.ibatis.transaction.TransactionFactory
			final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
			//從事務工廠獲取事務對象(JdbcTranasaction或ManagedTransaction)
			//此處只是獲取了事務對象,並沒有開啓事務,也沒有獲得連接
			tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
			//獲得執行器對象,事務被注入到了執行器中,後續事務的所有操作最終落點都在執行器當中
			//微觀來看,執行器纔是mybatis的“發動機”
			final Executor executor = configuration.newExecutor(tx, execType);
			//返回SqlSessionFactory對象
			return new DefaultSqlSession(configuration, executor, autoCommit);
		} catch (Exception e) {
			closeTransaction(tx); // may have fetched a connection so lets call
									// close()
			throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
		} finally {
			ErrorContext.instance().reset();
		}
	}
}
        本文主要討論事務,那麼在openConnection的時候並沒有獲取連接,更沒有開啓事務,只是根據配置文件構建了事務對象,更關鍵的點是執行器的構造方法,構建出來的事務對象通過構造方法被注入到了執行器當中,這一點對後續mybatis的事務體系相當重要。

2. 一次提交:事務的開啓

        連接和事務並沒有在openConnection中打開和開啓,而是延遲到了第一次操作的時候。事務的開啓,實際上是在執行器

package org.apache.ibatis.session.defaults;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.binding.BindingException;
import org.apache.ibatis.exceptions.ExceptionFactory;
import org.apache.ibatis.exceptions.TooManyResultsException;
import org.apache.ibatis.executor.BatchResult;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.result.DefaultMapResultHandler;
import org.apache.ibatis.executor.result.DefaultResultContext;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;

public class DefaultSqlSession implements SqlSession {
	/**
	 * 數據庫操作的頂級接口
	 */
	public int insert(String statement, Object parameter) {
		return update(statement, parameter);
	}
	/**
	 * 最終,所有的IUD操作都落點在此
	 * 真正執行參數映射、sql解析、sql執行已經返回值映射的,還是執行器
	 * 最終開啓事務的還是執行器,因此將轉到執行器當中繼續追蹤
	 */
	public int update(String statement, Object parameter) {
		try {
			dirty = true;
			MappedStatement ms = configuration.getMappedStatement(statement);
			return executor.update(ms, wrapCollection(parameter));
		} catch (Exception e) {
			throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
		} finally {
			ErrorContext.instance().reset();
		}
	}
}
        下面開始進行事務開啓的追蹤,注意源碼註釋中的數字編號。



3. 回滾

4. 關閉

5. 多線程中Connection的安全保證機制

        如果是SqlSessionManager,則是基於ThreadLocal,但是這種方式已經被拋棄了;SqlSessionManager被DefaultSqlSessionFactory和DefaultSqlSessoin替代了以後,現在是給予方法棧的多線程數據安全。

三、JDBC事務select


發佈了82 篇原創文章 · 獲贊 95 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章