Mybatis源碼解析之Exexutor

深入認識MyBatis執行體系

本文主要講解MyBatis的Executor的執行體系

Jdbc的執行過程

Jdbc常用的三種Statement

  1. Statement:一般不用了,只能處理靜態sql
  2. PreparedStatement:可以對Sql預編譯,防止Sql注入
  3. CallableStatement:執行存儲過程(不過現在存儲過程一般不推薦用)

ParparedStatement的執行過程

Jdbc的執行過程總體來說分爲以下四步:

  1. 獲取連接
  2. 預編譯SQL
  3. 執行SQL
  4. 讀取結果

圖示如下:

JDBC執行過程

關於防止sql注入的圖示:

防止Sql注入

Jdbc常用的代碼演示

代碼準備

依賴

    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.3</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.19</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

數據庫腳本

create table student
(
	id bigint auto_increment
		primary key,
	name varchar(50) null,
	age int null
);

INSERT INTO student (id, name, age) VALUES (1, 'daxiong', 24);
INSERT INTO student (id, name, age) VALUES (2, '魯班', 35);

數據庫連接的創建代碼

 private Connection connection;
    @Before
    public void init() throws SQLException {
        //DATASOURCE爲數據庫連接信息的常量類
        connection = DriverManager.getConnection(DATASOURCE.URL, DATASOURCE.USER, DATASOURCE.PASSWORD);
    }
    @After
    public void close() throws SQLException {
        connection.close();
    }

查詢的簡單例子

 @Test
    public void preparedStatementSelectTest() throws SQLException {
        // 預編譯
        String selectSql = "SELECT name,age FROM student WHERE `name`=? ";
        PreparedStatement preparedStatement = connection.prepareStatement(selectSql);
        //添加參數
        preparedStatement.setString(1,"daxiong");
        //執行sql
        preparedStatement.execute();
        //獲取結果集
        ResultSet resultSet = preparedStatement.getResultSet();
        while (resultSet.next()) {
            String resultMsg = String.format("查詢到的名字爲:【%s】,age爲:【%d】",
                    resultSet.getString(1),
                    resultSet.getInt(2));
            System.out.println(resultMsg);
        }
        resultSet.close();
        preparedStatement.close();;
    }

結果

在這裏插入圖片描述

批量操作

PreparedStatement:

1) addBatch()將一組參數添加到PreparedStatement對象內部。

2) executeBatch()將一批參數提交給數據庫來執行,如果全部命令執行成功,則返回更新計數組成的數組。

Statement:

1) addBatch(String sql)方法會在批處理緩存中加入一條sql語句。

2) executeBatch()執行批處理緩存中的所有sql語句。

注意:PreparedStatement中使用批量更新時,要先設置好參數後再使用addBatch()方法加入緩存。批量更新中只能使用更改刪除插入語句

PreparedStatement批量插入代碼

    @Test
    public void preparedStatementBatchTest() throws SQLException {
        //插入語句
        String sql = "INSERT INTO student (`name`,age) VALUES (?,?)";
        //通過insert創建預編譯聲明
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        long startTime = System.currentTimeMillis();
        //每一次遍歷添加一組參數
        for (int i = 0; i < 100; i++) {
            preparedStatement.setString(1, UUID.randomUUID().toString());
            preparedStatement.setInt(2, (int)(Math.random()*10+1) );
            preparedStatement.addBatch();
        }
        // 批處理  一次發射
        preparedStatement.executeBatch();
        System.out.println(String.format("批量處理使用了:【%d】ms",System.currentTimeMillis() - startTime));
        preparedStatement.close();
    }

Mybatis的執行過程

SqlSession(Sql會話)

​ SqlSession爲mybatis對外的大管家,採用門面模式(外觀模式)提供了基礎的增刪改查的API,也提供了提交、關閉會話以及緩存相關的輔助Api

SqlSession中有兩個重要的成員變量

  • configuration: 當前會話的配置
  • executor:具體處理數據庫操作的執行器

創建SqlSession的過程

通過DefaultSqlSessionFactory實例化的SqlSession是線程不安全的。提供了多個重載的openSesson的方法

SqlSession openSession();
//是否自動提交
SqlSession openSession(boolean autoCommit);
//通過數據庫連接獲取SqlSessonn
SqlSession openSession(Connection connection);
//事務的隔離級別
SqlSession openSession(TransactionIsolationLevel level);
//執行器的類型
SqlSession openSession(ExecutorType execType);

SqlSession openSession(ExecutorType execType, boolean autoCommit);

SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);

SqlSession openSession(ExecutorType execType, Connection connection);

通過數據庫獲取SqlSession

 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      //獲取當前的環境信息  
      final Environment environment = configuration.getEnvironment();
      //獲取事務工廠
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      //事務的創建
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //執行器的創建
      final Executor executor = configuration.newExecutor(tx, execType);
      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();
    }
  }

通過連接獲取SqlSession

  private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
    try {
      boolean autoCommit;
      try {
        autoCommit = connection.getAutoCommit();
      } catch (SQLException e) {
        // Failover to true, as most poor drivers
        // or databases won't support transactions
        autoCommit = true;
      }
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      final Transaction tx = transactionFactory.newTransaction(connection);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

其中比較關鍵的就是Executor executor = configuration.newExecutor(tx, execType);

 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
     //判斷參數,如果爲null取默認的SimpleExecutor
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
     
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    //判斷二級緩存是否開啓,開啓時創建CachingExecutor對原有的Executor進行裝飾
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

通過SqlSessionManager.startSession()創建的是線程安全的。

//維護了ThreadLocal變量
private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();

public void startManagedSession() { this.localSqlSession.set(openSession()); }

Executor(執行器)

​ SqlSession是面向程序的,而Executor是面向數據庫的,SqlSession的所有的方法都會調用到自己內部註冊的Executor類的query和update方法執行具體的方法,採用的是模板方法的設計模式。執行器的種類有:基礎執行器、簡單執行器、重用執行器和批處理執行器,此外通過裝飾器形式添加了一個緩存執行器。對應功能包括緩存處理、事物處理、重用處理以及批處理,而這些都是會話中多個SQL執行中有共性地方。執行器存在的意義就是去處理這些共性。

Executor的繼承類圖和簡介:
在這裏插入圖片描述

BaseExecutor (基礎執行器)

BaseExecutor採用的模板方法的設計模式實現了Executor接口,並處理了一級緩存的相關邏輯(在後續緩存章節講解)。

下面的四個方法是真正執行Sql語句的方法,BaseExecutor中並沒有實現其邏輯,都由其子類實現

 protected abstract int doUpdate(MappedStatement ms, Object parameter)
      throws SQLException;

  protected abstract List<BatchResult> doFlushStatements(boolean isRollback)
      throws SQLException;

  protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException;

  protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
      throws SQLException;

SimpleExecutor(簡單執行器)

SimpleExecutor爲最基礎的執行器,每一次查詢都會創建一個PreparedStatament,再沒有配置ExecutortType或者配置的爲null時,默認的都會使用此執行器。

ReuseExecutor (可重用執行器)

ReuseExecutor 爲可重用執行器,當執行的相同的sql語句時會重用Statement,實現邏輯也比較簡單。

  1. 內部維護一個HashMap來緩存Statement
  2. 執行query或者update前,先判斷當前的sql語句在map中是否已經存在,如果存在返回緩存的Statement,不存在編譯後緩存,key值爲sql,value爲Statement

具體代碼如下:

//用於緩存Statement的HashMap  
private final Map<String, Statement> statementMap = new HashMap<>();
//繼承自BaseExecutor的相關的執行邏輯的方法
@Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    Statement stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.query(stmt, resultHandler);
  }
//準備Statement的方法
 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
     //判斷當前sql是否已經緩存
    if (hasStatementFor(sql)) {
        //命中時直接返回
      stmt = getStatement(sql);
        //應用事務的超時時間,如果查詢的超時時間爲0,或者事務的超時時間小於查詢的超時時間時,設置查詢的超時時間爲事務的超時時間。
      applyTransactionTimeout(stmt);
    } else {
        //準備Statement放入緩存
      Connection connection = getConnection(statementLog);
      stmt = handler.prepare(connection, transaction.getTimeout());
      putStatement(sql, stmt);
    }
    handler.parameterize(stmt);
    return stmt;
  }
  //判斷是否命中緩存
  private boolean hasStatementFor(String sql) {
    try {
      return statementMap.keySet().contains(sql) && !statementMap.get(sql).getConnection().isClosed();
    } catch (SQLException e) {
      return false;
    }
  }
  //獲取緩存中的Statement
  private Statement getStatement(String s) {
    return statementMap.get(s);
  }
  //放置緩存中的Statement	
  private void putStatement(String sql, Statement stmt) {
    statementMap.put(sql, stmt);
  }

BaseExecutor中更新查詢的超時時間

//應用事務的超時時間,如果查詢的超時時間爲0,或者事務的超時時間小於查詢的超時時間時,設置查詢的超時時間爲事務的超時時間。 
protected void applyTransactionTimeout(Statement statement) throws SQLException {
    StatementUtil.applyTransactionTimeout(statement, statement.getQueryTimeout(), transaction.getTimeout());
  }

StatementUtil的applyTransactionTimeout方法

public static void applyTransactionTimeout(Statement statement, Integer queryTimeout, Integer transactionTimeout) throws SQLException {
  if (transactionTimeout == null){
    return;
  }
  Integer timeToLiveOfQuery = null;
  if (queryTimeout == null || queryTimeout == 0) {
    timeToLiveOfQuery = transactionTimeout;
  } else if (transactionTimeout < queryTimeout) {
    timeToLiveOfQuery = transactionTimeout;
  }
  if (timeToLiveOfQuery != null) {
    statement.setQueryTimeout(timeToLiveOfQuery);
  }
}

BatchExecutor(批處理執行器)

BatchExecutor可以批量執行非查詢操作,當相同和連續的Statement執行時,會公用同一個Statement,但必須要執行SqlSession的flushStatements纔會生效,具體實現的代碼如下:

聲明的記錄緩存的變量

//執行的Statement記錄  
private final List<Statement> statementList = new ArrayList<>();
//當前執行的語句
private String currentSql;
//處理結果集合
private final List<BatchResult> batchResultList = new ArrayList<>();
//當前的Statement
private MappedStatement currentStatement;

具體的判斷邏輯

 public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    final Configuration configuration = ms.getConfiguration();
    final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
    final BoundSql boundSql = handler.getBoundSql();
    final String sql = boundSql.getSql();
    final Statement stmt;
     //判斷此次執行的Sql和Statement和記錄的是否相同
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
      int last = statementList.size() - 1;
      //命中時,獲取當前的最後一個Statement
      stmt = statementList.get(last);
      applyTransactionTimeout(stmt);
      handler.parameterize(stmt);//fix Issues 322
      BatchResult batchResult = batchResultList.get(last);
      batchResult.addParameterObject(parameterObject);
    } else {
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);    //fix Issues 322
      //爲命中時,給current和currentStatement賦值
      currentSql = sql;
      currentStatement = ms;
      statementList.add(stmt);
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
  }

批量提交操作

 public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
    try {
      List<BatchResult> results = new ArrayList<>();
      if (isRollback) {
        return Collections.emptyList();
      }
        //遍歷Statement集合
      for (int i = 0, n = statementList.size(); i < n; i++) {
        Statement stmt = statementList.get(i);
        applyTransactionTimeout(stmt);
        BatchResult batchResult = batchResultList.get(i);
        try {
          //執行結構封裝
          batchResult.setUpdateCounts(stmt.executeBatch());
          MappedStatement ms = batchResult.getMappedStatement();
          List<Object> parameterObjects = batchResult.getParameterObjects();
          KeyGenerator keyGenerator = ms.getKeyGenerator();
          if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
            Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
            jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
          } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
            for (Object parameter : parameterObjects) {
              keyGenerator.processAfter(this, ms, stmt, parameter);
            }
          }
          // Close statement to close cursor #1109
          closeStatement(stmt);
        } catch (BatchUpdateException e) {
          StringBuilder message = new StringBuilder();
          message.append(batchResult.getMappedStatement().getId())
              .append(" (batch index #")
              .append(i + 1)
              .append(")")
              .append(" failed.");
          if (i > 0) {
            message.append(" ")
                .append(i)
                .append(" prior sub executor(s) completed successfully, but will be rolled back.");
          }
          throw new BatchExecutorException(message.toString(), e, results, batchResult);
        }
        results.add(batchResult);
      }
      return results;
    } finally {
      for (Statement stmt : statementList) {
        closeStatement(stmt);
      }
      currentSql = null;
      statementList.clear();
      batchResultList.clear();
    }
  }

以上三種執行器如何指定

配置文件中配置默認的executor

  <settings>
        <setting name="defaultExecutorType" value="REUSE"/>
    </settings>

創建SqlSesson中使用帶有ExecutorType的方法

  SqlSession openSession(ExecutorType execType);

  SqlSession openSession(ExecutorType execType, boolean autoCommit);

  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);

  SqlSession openSession(ExecutorType execType, Connection connection);

CachingExecutor(二級緩存執行器)

CachingExecutor使用了裝飾者模式,對其他三種執行器進行了包裝,使其有了二級緩存的功能。其內部的執行sql的方法都調用其內部包裝的Executor對象,二級緩存具體如何工作,在後面的緩存章節進行介紹

簡單列出CachingExecutor的構造方法和update的實現

 private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }

SqlSession調用Executor的執行過程

在這裏插入圖片描述

執行堆棧
在這裏插入圖片描述

StatementHandler(聲明處理器)

未完

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