1. 程式人生 > >MyBatis原始碼分析---呼叫SqlSession增刪改查到底執行了什麼步驟

MyBatis原始碼分析---呼叫SqlSession增刪改查到底執行了什麼步驟

SqlSession建立流程:

sqlSession可謂是MyBatis中最為核心的物件,相當於與資料庫的一次會話,是程式和資料庫的橋樑。

SqlSession物件由SqlSessionFactory工廠物件建立

SqlSessionFactory工廠物件提供了一大堆過載方法用來建立SqlSession物件

public interface SqlSessionFactory {

  SqlSession openSession();
  //是否自動提交
  SqlSession openSession(boolean autoCommit);
  //設定事物隔離級別
  SqlSession openSession(TransactionIsolationLevel level);
  //ExecutorType 列舉型別 用於定義執行sql的方式 決定了使用的Executor執行器
  SqlSession openSession(ExecutorType execType);
  SqlSession openSession(ExecutorType execType, boolean autoCommit);
  Configuration getConfiguration();
}

無參構造:

無參構造是平日裡最常用的一個方法,不自動提交事物,使用預設的執行sql命令方式(ExecutorType.SIMPLE)

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

sqlSessionFactory的預設實現類DefaultSqlSessionFactory轉而呼叫openSessionFromDataSource()方法

再次獲取配置檔案中設定的執行sql命令方式,不設定事物隔離級別,設定不自動提交。

再來看看openSessionFromDataSource方法做了什麼

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      //從配置中獲取環境物件
      final Environment environment = configuration.getEnvironment();
      //獲取環境配置中的事物工廠物件 主要用於建立事物物件
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      //事物物件包含了本次訪問的connection物件以及對事物的管理 
      //sqlsession操作事物時實際就是在操作這個物件
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //建立核心執行器物件(預設情況下使用的是SimpleExecutor類)
      //SqlSession物件進行增刪改查實際上全是在依賴這個執行器物件
      final Executor executor = configuration.newExecutor(tx, execType);
      //sqlsession依賴的物件全部建立完成  返回sqlSession物件
      return new DefaultSqlSession(configuration, executor, autoCommit);
    }catch...
  }

Executor執行器物件的建立流程:

ConfigUration物件下的newExecutor方法

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      //由於我們沒有傳入ExecutorType並且沒有在配置檔案中配置
      //預設使用了simpleExecutor作為實現
      executor = new SimpleExecutor(this, transaction);
    }
    //如果在全域性配置開啟了cacheEnabled(二級快取)
    //則再對執行器物件進行包裝:使用了裝飾器模式,當執行器執行查詢操作時會先從二級快取中查詢
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

SqlSession執行流程:

大家都知道,在呼叫SqlSession.getMapper()時,返回的是該介面的代理類

在執行代理類所有方法時,實際上是執行了MapperProxy類下的invoke()方法

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //如果呼叫了Object類的方法 直接呼叫返回結果
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      //是否是預設方法 (jdk1.8之後可以在介面中定義default方法)
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    //MapperMethod:包含了要執行方法的所有資訊 方法的唯一標識
    //執行的方法名 方法的型別:select/insert/delete/update 方法返回值型別
    //方法依賴的引數個數 方法引數是否使用了@Param註解    ......
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //執行方法
    return mapperMethod.execute(sqlSession, args);
  }

現在已經拿到了執行方法的所有資訊,和傳遞過來的引數

呼叫了MapperMethod物件的execute()方法

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      //無論是insert/update/delte執行的都是一樣的流程 最終都會執行sqlsession中的update方法
      case INSERT/UPDATE/DELETE: {
      //將傳入方法的引數進行轉換
      //引數只有一個並且沒有註解的話直接返回
      //多個引數就封裝為map物件
      Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        //返回值為List/set/Array
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        //返回值為Map
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        //呼叫儲存過程返回遊標
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        //返回值為單個物件
        } else {
          //封裝引數 同上
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
        }
        break;
    }
    return result;
  }

其實在呼叫介面代理類的方法後,到頭來還是呼叫了sqlSession去執行增刪改查方法.

sqlSession的增刪改查方法都需要sql對映檔案下增刪改查標籤的唯一標識,以及依賴的引數

只不過在這中間,MyBatis將方法解析了出來,程式設計師只需要照常呼叫介面內的方法

由mybatis告訴sqlsession該執行update還是select,並且將引數封裝一下。

查詢單個物件:

sqlSession的selectOne()方法:

  @Override
  /**
   *    statement : 所執行方法在sql對映檔案中的namespace + id
   *    parameter : 由MyBatix進行封裝後的引數(匹配更方便)
   */
  public <T> T selectOne(String statement, Object parameter) {
    // 其實無論查詢一個還是查詢多個  最後都進入了selectList()方法
    List<T> list = this.<T>selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    //如果查詢出來的結果不止一條 丟擲異常
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null;
    }
  }

sqlSession的selectList()方法:

  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      //根據方法的唯一標識(全類名加方法名) 從配置中獲取節點資訊並封裝為MappedStatement物件
      //MappedStatement相當於對mapper.xml中一個select/insert/update/delete節點的封裝
      //包含了該節點的所有資訊(sql文字,屬性)
      //還包含了一個sqlSource物件 用於根據引數動態生成sql語句並繫結到BoundSql物件中
      MappedStatement ms = configuration.getMappedStatement(statement);
      //wrapCollection(parameter)判斷引數是否是collection型別或者陣列  如果是 轉換為map
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

在sqlSession執行的selectList()方法中主要匹配到了mapper.xml檔案中對應的select節點並封裝為mappedStatement物件

已經有了當前節點的全部資訊以及引數

executor物件以及可以開始工作了

  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //通過mapperStatement物件獲取最終執行的sql語句(執行了一系列條件判斷 動態生成了最終sql)
    BoundSql boundSql = ms.getBoundSql(parameter);
    //建立用作於一級快取Map物件的key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    //呼叫過載方法
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }
​
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    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 {
        //快取中未找到,從資料庫中進行查詢
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache();
      }
    }
    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 {
      //現在已經確定快取中沒有要查詢的資料 必須從資料庫進行查詢
      //並且動態生成後的sql語句和引數都已經準備好了
      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;
  }

 在doQuery()方法中,建立了一個StatementHandler物件

這個物件用來建立statement物件\設定引數\以及操作statement進行增刪改查

StatementHandler是一個介面 使用時的實現類由標籤中的StatementType屬性來決定

StatementType屬性用來決定執行sql的方式,它有三個取值

1):STATEMENT:直接操作sql,不進行預編譯,設定引數:${}       實現類:SimpleStatementHandler

2):PREPARED:預處理,引數,進行預編譯,  設定引數:#{}       實現類:PrepareStatementHandler

3):CALLABLE:執行儲存過程                                                               實現類:CallableStatementHandler

  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 物件建立時還會建立MyBatis的最後兩大核心元件:
      //ParameterHandler(用於給statement繫結引數)和ResultSetHandler(處理結果集)
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      //在executor物件中呼叫prepareStatement(handler, ms.getStatementLog())
      //分別執行了三個操作:
      //1):從繫結的事物物件獲取connection
      //2):呼叫statementHandler物件的prepare(Coneection conn)並獲取Statement物件
      //3):呼叫statementHandler物件的parameterize(Statement statement)方法
      //為statement設定引數(如果是SimplePrapareHandler則不需要 方法內是空實現)
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

  初始化好的statement物件已經準備就緒了 轉而呼叫statementHandler的query方法

 以下是prepareStatementHandler類對query的實現

  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    //執行sql
    ps.execute();
    //處理resultSet,封裝結果集 並返回封裝好的List<E>集合
    //執行完畢
    return resultSetHandler.<E> handleResultSets(ps);
  }

在進行增刪改的方法時其實與查詢方法的步驟大同小異,流程基本相似