Mybatis 中的事務

1. Mybaits中的事務接口Transaction

public interface Transaction {
    Connection getConnection() throws SQLException;
    void commit() throws SQLException;
    void rollback() throws SQLException;
    void close() throws SQLException;
}

有了文章開頭的分析,當你再次看到close()方法時,千萬別再認爲是關閉一個事務了,而是關閉一個conn連接,或者是把conn連接放回連接池內。

事務類層次結構圖:

                        

JdbcTransaction:單獨使用Mybatis時,默認的事務管理實現類,就和它的名字一樣,它就是我們常說的JDBC事務的極簡封裝,和編程使用mysql-connector-java-5.1.38-bin.jar事務驅動沒啥差別。其極簡封裝,僅是讓connection支持連接池而已。

ManagedTransaction:含義爲託管事務,空殼事務管理器,皮包公司。僅是提醒用戶,在其它環境中應用時,把事務託管給其它框架,比如託管給Spring,讓Spring去管理事務。

org.apache.ibatis.transaction.jdbc.JdbcTransaction.java部分源碼。

@Override
  public void close() throws SQLException {
    if (connection != null) {
      resetAutoCommit();
      if (log.isDebugEnabled()) {
        log.debug("Closing JDBC Connection [" + connection + "]");
      }
      connection.close();
    }
  }

面對上面這段代碼,我們不禁好奇,connection.close()之前,居然調用了一個resetAutoCommit(),含義爲重置autoCommit屬性值。connection.close()含義爲銷燬conn,既然要銷燬conn,爲何還多此一舉的調用一個resetAutoCommit()呢?消失之前多喝口水,真的沒有必要。

其實,原因是這樣的,connection.close()不意味着真的要銷燬conn,而是要把conn放回連接池,供下一次使用,既然還要使用,自然就需要重置AutoCommit屬性了。通過生成connection代理類,來實現重回連接池的功能。如果connection是普通的Connection實例,那麼代碼也是沒有問題的,雙重支持。

2. 事務工廠TransactionFactory

顧名思義,一個生產JdbcTransaction實例,一個生產ManagedTransaction實例。兩個毫無實際意義的工廠類,除了new之外,沒有其他代碼。

<transactionManager type="JDBC" />

mybatis-config.xml配置文件內,可配置事務管理類型。

3. Transaction的用法

無論是SqlSession,還是Executor,它們的事務方法,最終都指向了Transaction的事務方法,即都是由Transaction來完成事務提交、回滾的。

配一個簡單的時序圖。

                            

代碼樣例:

public static void main(String[] args) {
        SqlSession sqlSession = MybatisSqlSessionFactory.openSession();
        try {
            StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
            Student student = new Student();
            student.setName("yy");
            student.setEmail("[email protected]");
            student.setDob(new Date());
            student.setPhone(new PhoneNumber("123-2568-8947"));
            studentMapper.insertStudent(student);
            sqlSession.commit();
        } catch (Exception e) {
            sqlSession.rollback();
        } finally {
            sqlSession.close();
        }
    }

注:Executor在執行insertStudent(student)方法時,與事務的提交、回滾、關閉毫無瓜葛(方法內部不會提交、回滾事務),需要像上面的代碼一樣,手動顯示調用commit()、rollback()、close()等方法。

因此,後續在分析到類似insert()、update()等方法內部時,需要忘記事務的存在,不要試圖在insert()等方法內部尋找有關事務的任何方法。

4. 你可能關心的有關事務的幾種特殊場景表現(重要)

1. 一個conn生命週期內,可以存在無數多個事務。

// 執行了connection.setAutoCommit(false),並返回
            SqlSession sqlSession = MybatisSqlSessionFactory.openSession();
        try {
            StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

            Student student = new Student();
            student.setName("yy");
            student.setEmail("[email protected]");
            student.setDob(new Date());
            student.setPhone(new PhoneNumber("123-2568-8947"));

            studentMapper.insertStudent(student);
            // 提交
            sqlSession.commit();

            studentMapper.insertStudent(student);
            // 多次提交
            sqlSession.commit();
        } catch (Exception e) {
                // 回滾,只能回滾當前未提交的事務
            sqlSession.rollback();
        } finally {
            sqlSession.close();
        }

對於JDBC來說,autoCommit=false時,是自動開啓事務的,執行commit()後,該事務結束。以上代碼正常情況下,開啓了2個事務,向數據庫插入了2條數據。JDBC中不存在Hibernate中的session的概念,在JDBC中,insert了幾次,數據庫就會有幾條記錄,切勿混淆。而rollback(),只能回滾當前未提交的事務。

2. autoCommit=false,沒有執行commit(),僅執行close(),會發生什麼?

try {
    studentMapper.insertStudent(student);
} finally {
    sqlSession.close();
}

就像上面這樣的代碼,沒有commit(),固執的程序員總是好奇這樣的特例。

insert後,close之前,如果數據庫的事務隔離級別是read uncommitted,那麼,我們可以在數據庫中查詢到該條記錄。

接着執行sqlSession.close()時,經過SqlSession的判斷,決定執行rollback()操作,於是,事務回滾,數據庫記錄消失。

下面,我們看看org.apache.ibatis.session.defaults.DefaultSqlSession.java中的close()方法源碼。

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

事務是否回滾,依靠isCommitOrRollbackRequired(false)方法來判斷。

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

在上面的條件判斷中,!autoCommit=true(取反當然是true了),force=false,最終是否回滾事務,只有dirty參數了,dirty含義爲是否是髒數據。

 @Override
  public int insert(String statement, Object parameter) {
    return update(statement, parameter);
  }

  @Override
  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();
    }
  }

源碼很明確,只要執行update操作,就設置dirty=true。insert、delete最終也是執行update操作。只有在執行完commit()、rollback()、close()等方法後,纔會再次設置dirty=false。

  @Override
  public void commit(boolean force) {
    try {
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

因此,得出結論:autoCommit=false,但是沒有手動commit,在sqlSession.close()時,Mybatis會將事務進行rollback()操作,然後才執行conn.close()關閉連接,當然數據最終也就沒能持久化到數據庫中了。

3. autoCommit=false,沒有commit,也沒有close,會發生什麼?

studentMapper.insertStudent(student);

乾脆,就這一句話,即不commit,也不close。

結論:insert後,jvm結束前,如果事務隔離級別是read uncommitted,我們可以查到該條記錄。jvm結束後,事務被rollback(),記錄消失。通過斷點debug方式,你可以看到效果。

這說明JDBC驅動實現,已經考慮到這樣的特例情況,底層已經有相應的處理機制了。這也超出了我們的探究範圍。

但是,一萬個屌絲程序員會對你說:Don't do it like this. Go right way。

警告:請按正確的try-catch-finally編程方式處理事務

注:無參的openSession()方法,會自動設置autoCommit=false。

總結:Mybatis的JdbcTransaction,和純粹的Jdbc事務,幾乎沒有差別,它僅是擴展支持了連接池的connection。

另外,需要明確,無論你是否手動處理了事務,只要是對數據庫進行任何update操作(update、delete、insert),都一定是在事務中進行的,這是數據庫的設計規範之一。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章