Mybatis【2.2】-- Mybatis關於建立SqlSession原始碼分析的幾點疑問?

第十六封發表於2020-11-28

程式碼直接放在Github倉庫【https://github.com/Damaer/Mybatis-Learning 】,可直接執行,就不佔篇幅了。

1.為什麼我們使用SQLSessionFactoryBuilder的時候不需要自己關閉流?

我們看我們的程式碼:

public class StudentDaoImpl implements IStudentDao {
    private SqlSession sqlSession;
	public void insertStu(Student student) {
		try {
			InputStream inputStream;
			inputStream = Resources.getResourceAsStream("mybatis.xml");
			SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
			sqlSession=sqlSessionFactory.openSession();
			sqlSession.insert("insertStudent",student);
			sqlSession.commit();
		} catch (IOException e) {
			e.printStackTrace();
		}finally {
		    if(sqlSession!=null){
		        sqlSession.close();
            }
        }
	}
}

當我們使用inputStream = Resources.getResourceAsStream("mybatis.xml");的時候,我們並需要去關閉inputstream,我們可以檢視原始碼,首先看到SqlSessionFactoryBuilder().build()這個方法:

    // 將inputstream傳遞進去,呼叫了另一個分裝的build()方法
    public SqlSessionFactory build(InputStream inputStream) {
        return this.build((InputStream)inputStream, (String)null, (Properties)null);
    }

跟進去,我們再來看另一個build方法,裡面有一個finally模組,無論怎麼樣都會執行close方法,所以這就是為什麼我們在使用的時候為什麼不用關閉inputstream的原因:因為這個流是在finally程式碼塊中被關閉了。

    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        SqlSessionFactory var5;
        try {
            XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
            var5 = this.build(parser.parse());
        } catch (Exception var14) {
            throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
        } finally {
            ErrorContext.instance().reset();
            try {
                // 關閉流
                inputStream.close();
            } catch (IOException var13) {
                ;
            }

        }
        return var5;
    }

2. Sqlsession是如何建立的?

語句裡面執行程式碼:使用SQLSessionFactory去開啟一個session,這裡的session我們可以初步理解為一個sql的會話,類似我們想要發資訊給別人,肯定需要開啟一個和別人的會話。

sqlSession=sqlSessionFactory.openSession();

我們需要檢視原始碼,我們發現opensession是sqlSessionFactory的一個介面方法,sqlSessionFactory是一個介面。

public interface SqlSessionFactory {
    // 在這裡只貼出了一個方法,其他的就不貼了
    SqlSession openSession();
    }

idea選中該方法,ctrl + alt +B,我們可以發現有DefaultSqlSessionFactory,和SqlSessionManager這兩個類實現了SqlSessionFactory這個介面

那麼我們需要跟進去DefaultSqlSessionFactory這個類的openSesseion方法,在裡面呼叫了一個封裝好的方法:openSessionFromDataSource()

    public SqlSession openSession() {
        return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
    }

當然在DefaultSqlSessionFactory這個類裡面還有一個方法,引數是autoCommit,也就是可以指定是否自動提交:

    public SqlSession openSession(boolean autoCommit) {
        return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, autoCommit);
    }

我們再跟進去原始碼,我們會發現有一個引數是autoCommit,也就是自動提交,我們可以看到上一步傳值是false,也就是不會自動提交,通過configuration(主配置)獲取environment(執行環境),然後通過environment(環境)開啟和獲取一個事務工廠,通過事務工廠獲取事務物件Transaction,通過事務物件建立一個執行器executor,Executor是一個介面,實現類有比如SimpleExecutor,BatchExecutor,ReuseExecutor,所以我們下面程式碼裡的execType,是指定它的型別,生成指定型別的Executor,把引用給介面物件,有了執行器之後就可以return一個DefaultSqlSession物件了。

    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        DefaultSqlSession var8;
        try {
            // configuration是主配置檔案
            Environment environment = this.configuration.getEnvironment();
            // 獲取事務工廠,事務管理器可以使jdbc之類的
            TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
            // 獲取事務物件Transaction
            tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
            // 通過事務物件建立一個執行器executor
            Executor executor = this.configuration.newExecutor(tx, execType);
            // DefaultSqlSession是SqlSession實現類,建立一個DefaultSqlSession並返回
            var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
        } catch (Exception var12) {
            this.closeTransaction(tx);
            throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);
        } finally {
            ErrorContext.instance().reset();
        }
        return var8;
    }

我們跟 var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);這句程式碼,我們這是初始化函式賦值於各個成員變數,我們發現裡面有一個dirty成員,這是幹什麼用的呢?從名字上來講我們理解是髒的,這裡既然設定為false,那就是不髒的意思。那到底什麼是髒呢?髒是指記憶體裡面的資料與資料庫裡面的資料存在不一致的問題,如果一致就是不髒的
後面會解釋這個dirty的作用之處,到這裡一個SqlSession就建立完成了。

    public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
        this.configuration = configuration;
        this.executor = executor;
        this.dirty = false;
        this.autoCommit = autoCommit;
    }

3.增刪改是怎麼執行的

我們使用到這句程式碼:

sqlSession.insert("insertStudent",student);

我們發現同樣是介面方法,上面我們知道SqlSession其實是DefaultSqlSession所實現的介面,那麼我們跟進去DefaultSqlSession的insert()方法,我們發現其實inset方法底層也是實現了update這個方法,同樣的delete方法在底層也是呼叫了update這個方法,增,刪,改本質上都是改

public int insert(String statement, Object parameter) {
    return this.update(statement, parameter);
}
public int update(String statement) {
    return this.update(statement, (Object)null);
}

那麼我們現在跟進去update方法中,dirty變成ture,表明即將改資料,所以資料庫資料與記憶體中資料不一致了,statement是我們穿過來的id,這樣就可以通過id拿到statement的物件,然後就通過執行器執行修改的操作:

    public int update(String statement, Object parameter) {
        int var4;
        try {
            // dirty變成ture,表明資料和資料庫資料不一致,需要更新
            this.dirty = true;
            // 通過statement的id把statement從配置中拿到對映關係
            MappedStatement ms = this.configuration.getMappedStatement(statement);
            // 執行器執行修改的操作
            var4 = this.executor.update(ms, this.wrapCollection(parameter));
        } catch (Exception var8) {
            throw ExceptionFactory.wrapException("Error updating database.  Cause: " + var8, var8);
        } finally {
            ErrorContext.instance().reset();
        }
        return var4;
    }

4.SqlSession.commit()為什麼可以提交事務(transaction)?

首先,我們使用到的原始碼,同樣選擇DefaultSqlSession這個介面的方法,我們發現commit裡面呼叫了另一個commit方法,傳進去一個false的值:

    public void commit() {
        this.commit(false);
    }

我們跟進去,發現上面傳進去的false是變數force,裡面呼叫了一個isCommitOrRollbackRequired(force)方法,執行的結果返回給commit方法當引數。

public void commit(boolean force) {
    try {
        this.executor.commit(this.isCommitOrRollbackRequired(force));
        // 提交之後dirty置為false,因為資料庫與記憶體的資料一致了。
        this.dirty = false;
    } catch (Exception var6) {
        throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + var6, var6);
    } finally {
        ErrorContext.instance().reset();
    }
}

我們跟進去isCommitOrRollbackRequired(force)這個方法,這個方法從命名上是需要提交還是回滾的意思。在前面我們知道autoCommit是false,那麼取反之後就是true,關於dirty我們知道前面我們執行過insert()方法,insert的底層呼叫了update方法,將dirty置為true,表示即將修改資料,那我們知道!this.autoCommit && this.dirty的值就是true,那麼就短路了,所以整個表示式的值就是true。

private boolean isCommitOrRollbackRequired(boolean force) {
    return !this.autoCommit && this.dirty || force;
}

返回上一層的,我們知道this.isCommitOrRollbackRequired(force)的返回值是true。

this.executor.commit(this.isCommitOrRollbackRequired(force));

跟進去commit方法,這個commit方法是一個介面方法,實現介面的有BaseExecutor,還有CachingExecutor,我們選擇BaseExecutor這個介面實現類:

// required是true
public void commit(boolean required) throws SQLException {
    // 如果已經 關閉,那麼就沒有辦法提交,丟擲異常
    if (this.closed) {
        throw new ExecutorException("Cannot commit, transaction is already closed");
    } else {
        this.clearLocalCache();
        this.flushStatements();
        // 如果required是true,那麼就提交事務
        if (required) {
            this.transaction.commit();
        }
    }
}

5.為什麼sqlsession關閉就不需要回滾了?

假如我們在上面已經提交過了,那麼dirty的值就為false。我們使用的是sqlSession.close();,跟進去原始碼,同樣是介面,我們跟DefaoultSqlsession的方法,同樣呼叫了isCommitOrRollbackRequired()這個方法:

    public void close() {
        try {
            this.executor.close(this.isCommitOrRollbackRequired(false));
            this.dirty = false;
        } finally {
            ErrorContext.instance().reset();
        }
    }

我們跟進去isCommitOrRollbackRequired(false)這個方法,我們知道force傳進來的值是false,autoCommit是false(只要我們使用無參的sqlSessionFactory.openSession();),取反之後!autoCommit是true,但是dirty已經是false,所以!this.autoCommit && this.dirty的值是false,那麼force也是false,所以整一個表示式就是false:

    private boolean isCommitOrRollbackRequired(boolean force) {
        return !this.autoCommit && this.dirty || force;
    }

我們返回上一層,executor.close()方法,引數是false:

this.executor.close(this.isCommitOrRollbackRequired(false));

跟進去close()方法,forceRollback的值是false,我們發現有一個this.rollback(forceRollback)

public void close(boolean forceRollback) {
        try {
            try {
                this.rollback(forceRollback);
            } finally {
                // 最後如果事務不為空,那麼我們就關閉事務
                if (this.transaction != null) {
                    this.transaction.close();
                }
            }
        } catch (SQLException var11) {
            log.warn("Unexpected exception on closing transaction.  Cause: " + var11);
        } finally {
            this.transaction = null;
            this.deferredLoads = null;
            this.localCache = null;
            this.localOutputParameterCache = null;
            this.closed = true;
        }
    }

我們跟進去rollback()這個方法,我們可以發現required是fasle,所以 this.transaction.rollback();是不會執行的,這個因為我們在前面做了提交了,所以是不用回滾的:

     public void rollback(boolean required) throws SQLException {
        if (!this.closed) {
            try {
                this.clearLocalCache();
                this.flushStatements(true);
            } finally {
                if (required) {
                    this.transaction.rollback();
                }

            }
        }

    }

假如我們現在執行完insert()方法,但是沒有使用commit(),那麼現在的dirty就是true,也就是資料庫資料與記憶體的資料不一致。我們再執行close()方法的時候,dirty是true,!this.autoCommit是true,那麼整個表示式就是true。

    private boolean isCommitOrRollbackRequired(boolean force) {
        return !this.autoCommit && this.dirty || force;
    }

返回上一層,close的引數就會變成true

this.executor.close(this.isCommitOrRollbackRequired(false));

close()方法裡面呼叫了 this.rollback(forceRollback);,引數為true,我們跟進去,可以看到確實執行了回滾:

     public void rollback(boolean required) throws SQLException {
        if (!this.closed) {
            try {
                this.clearLocalCache();
                this.flushStatements(true);
            } finally {
                if (required) {
                    this.transaction.rollback();
                }

            }
        }

    }

所以只要我們執行了提交(commit),那麼關閉的時候就不會執行回滾,只要沒有提交事務,就會發生回滾,所以裡面的dirty是很重要的。

【作者簡介】
秦懷,公眾號【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。這個世界希望一切都很快,更快,但是我希望自己能走好每一步,寫好每一篇文章,期待和你們一起交流。

此文章僅代表自己(本菜鳥)學習積累記錄,或者學習筆記,如有侵權,請聯絡作者核實刪除。人無完人,文章也一樣,文筆稚嫩,在下不才,勿噴,如果有錯誤之處,還望指出,感激不盡~

相關文章