mybatis原始碼解讀---一條sql的旅程

JavaDog發表於2019-03-01

前言:本文從原始的mybatis原始碼開始分析一條sql語句的執行過程,我們常用的mybatis基本都是spring封裝過的,本文不涉及spring封裝部分。

一、mybatis使用步驟

我們先通過一個簡單的例項回顧一下原生mybatis的使用步驟

場景:我們要通過使用者id獲取使用者的詳細資訊,使用mybatis要經過如下四個步驟(sql如下)

select user_id as userId,user_name userName,age from op_user_info where user_id>#{userId}
複製程式碼

1.配置mybatis-config.xml檔案

其中最重要的兩個配置一個是dataSource(資料來源)、mappers(mapper檔案路徑)

<configuration> 
  <dataSource type="POOLED">
    <property name="driver" value="${driver}"/>
    <property name="url" value="${url}"/>
    <property name="username" value="${username}"/>
    <property name="password" value="${password}"/>
  </dataSource>
  <mappers>
     <mapper class="classpath*:mybatis/UserMapper.xml"/>
  </mappers>
   ....
</configuration>  
複製程式碼

2.編寫一個mappr-xx.xml檔案

<mapper namespace="com.alibaba.test.UserDao">
     <select id="getUser" parameterType="com.alibaba.test.UserBO"
            resultType="com.alibaba.test.UserDO">
   select user_id as userId,user_name userName,age from op_user_info where user_id>#{userId}
      </select>
     ......
</mapper>
複製程式碼

3.編寫一個介面Interface

public interface UserDao {
    public UserDO getUser(UserBO UserBO)
    .......
}
複製程式碼

​ <!--注意這裡介面的方法名和mappr-xx.xml中的id一一對應-->

螢幕快照 2019-02-25 下午5.13.11.png

4.開始查詢資料庫

注意我們在查詢資料的時候有兩種選擇

4.1 第一種方式,使用SqlSession的方法直接獲取

SqlSession sqlSession = sqlSessionFactory.openSession();
try {
  UserBO  userBO= new UserBO();
  userBO.setUserId(148736);
  UserDO user = (UserDO) sqlSession.select("com.alibaba.test.UserDao.getUser", userBO);
} finally {
  session.close();
}
複製程式碼

注意mybatis所有的操作增刪改查都是從這句程式碼開始,後面我們分析sql的執行流程時也將從這句程式碼開始。
螢幕快照 2019-02-25 下午4.53.14.png

4.2 第二種方式,通過介面UserDao的實現類獲取

SqlSession session = sqlSessionFactory.openSession();
try {
  UserBO  userBO= new UserBO();
  userBO.setUserId(148736);
  UserDao userDao = session.getMapper(UserDao.class);
  UserDO user    userDao.getUser(userBO);
} finally {
session.close();
}
複製程式碼

分析:第二種方式使用了動態代理的方式獲取了Interface(UserDao)的實現類,最終還是通過第一種方式獲取資料,一直跟蹤程式碼在MapperProxy類中可以找到這段邏輯

public class MapperProxy<T> implements InvocationHandler, Serializable {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }
}
複製程式碼

所以我們得出結論mybatis所有的操作增刪改查都是從這句程式碼開始

螢幕快照 2019-02-25 下午5.40.29.png

二、mybatis初始化

在此之前我們先了解下mybatis初始化階段做了些什麼事情,順便找出我們的主角SqlSession是怎麼產生的

1.mybatis初始化邏輯

1)String resource = "org/mybatis/example/mybatis-config.xml";
2)InputStream inputStream = Resources.getResourceAsStream(resource);
3)SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
4)SqlSession session = sqlSessionFactory.openSession();
複製程式碼

跟蹤第二行程式碼可以看到mybatis將配置檔案mybatis-config.xml中的所有資訊都解析到了工廠類SqlSessionFactory中,SqlSessionFactory將所有的配置資訊儲存在了Configuration類中。SqlSession就是從SqlSessionFactory中建立的。

2.Configuration類

Configuration中存放了mybatis-config.xml中的所有配置資訊

螢幕快照 2019-02-25 下午7.28.33.png
其中

1)Environment environment封裝了資料來源資訊

螢幕快照 2019-02-25 下午7.19.16.png

2) protected final Map mappedStatements 封裝了mapepr-xx.xml中的資訊。

其中Map的key為mapepr-xx.xml中中的namespace+id,MappedStatement封裝了

….

螢幕快照 2019-02-25 下午7.19.22.png

三、mybatis中幾個重要的類

  • SqlSession 作為MyBatis工作的主要頂層API,表示和資料庫互動的會話,完成必要資料庫增刪改查功能

  • Executor MyBatis執行器,是MyBatis 排程的核心,負責SQL語句的生成和查詢快取的維護

  • StatementHandler 封裝了JDBC Statement操作,負責對JDBC statement 的操作,如設定引數、將Statement結果集轉換成List集合。

  • ParameterHandler 負責對使用者傳遞的引數轉換成JDBC Statement 所需要的引數,

  • ResultSetHandler 負責將JDBC返回的ResultSet結果集物件轉換成List型別的集合;

  • TypeHandler 負責java資料型別和jdbc資料型別之間的對映和轉換

  • MappedStatement MappedStatement維護了一條節點的封裝,

  • SqlSource 負責根據使用者傳遞的parameterObject,動態地生成SQL語句,將資訊封裝到BoundSql物件中,並返回

  • BoundSql 表示動態生成的SQL語句以及相應的引數資訊

  • Configuration MyBatis所有的配置資訊都維持在Configuration物件之中。

四、資料查詢的執行流程

1.通過第一篇文章的分析我們知道所有的執行流程從這句程式碼開始,其中"com.alibaba.test.UserDao.getUser"是

Configuration類中 Map mappedStatements的key值。

 UserDO user = (UserDO) sqlSession.select("com.alibaba.test.UserDao.getUser", userBO);
複製程式碼

2.進入select方法發現首先根據statementId從 Map mappedStatements中獲取到了封裝的MappedStatement,然後將資料查詢操作委託給了Executor executor。

public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    executor.query(ms, wrapCollection(parameter), rowBounds, handler);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}
複製程式碼

3.跟蹤executor的query方法。說道Executor,mybatis有三種,他們的區別如下

SimpleExecutor:每執行一次update或select,就開啟一個Statement物件,用完立刻關閉Statement物件。(可以是Statement或PrepareStatement物件)

ReuseExecutor:執行update或select,以sql作為key查詢Statement物件,存在就使用,不存在就建立,用完後,不關閉Statement物件,而是放置於Map內,供下一次使用。(可以是Statement或PrepareStatement物件),程式碼如下

BatchExecutor:執行update(沒有select,JDBC批處理不支援select),將所有sql都新增到批處理中(addBatch()),等待統一執行(executeBatch()),它快取了多個Statement物件,每個Statement物件都是addBatch()完畢後,等待逐一執行executeBatch()批處理的;

我們進入SimpleExecutor的query方法,在這個方法中通過ms.getBoundSql(parameter)生成了具體執行的sql。並將結果存在了BoundSql中。併為當前的查詢建立一個快取Key ,至此我們得到了一個可以正常執行的完整sql。

 @Override
 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
   // 1.根據具體傳入的引數,動態地生成需要執行的SQL語句,用BoundSql物件表示    
   BoundSql boundSql = ms.getBoundSql(parameter);
    // 2.為當前的查詢建立一個快取Key  
   CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
   return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
複製程式碼

4.繼續跟蹤query(ms, parameter, rowBounds, resultHandler, key, boundSql);

這段程式碼中根據上一步獲取的CacheKey從快取中獲取結果,如果快取結果為空則呼叫

queryFromDatabase(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 {
  ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  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();
    }
    // issue #601
    deferredLoads.clear();
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}
複製程式碼

5.我們繼續跟蹤queryFromDatabase。這個方法先執行查詢返回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 {
      //4. 執行查詢,返回List 結果,然後    將查詢的結果放入快取之中  
    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;
}
複製程式碼

6.根據 doQuery(ms, parameter, rowBounds, resultHandler, boundSql);方法。以上都是抽象類BaseExecutor中的方法。至此進入預設實現類SimpleExecutor。

SimpleExecutor.doQuery原始碼

@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
     // 根據既有的引數,建立StatementHandler物件來執行查詢操作  
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
     //建立java.Sql.Statement物件,傳遞給StatementHandler物件  
    stmt = prepareStatement(handler, ms.getStatementLog());
      //呼叫StatementHandler.query()方法,返回List結果集  
    return handler.<E>query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}
複製程式碼

該函式的作用如下:

6.1 根據既有的引數,建立StatementHandler物件來執行查詢操作,在這段程式碼中我們發現了 interceptorChain.pluginAll(statementHandler) ,我們經常見到的mybatis攔截器就是在這個地方開始生效的。

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
  statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
  return statementHandler;
}
複製程式碼

6.2 呼叫prepareStatement方法建立java.Sql.Statement物件,並對建立的Statement物件設定引數,即設定SQL 語句中 ? 設定為指定的引數 ,最後傳遞給StatementHandler物件

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;
  Connection connection = getConnection(statementLog);
  stmt = handler.prepare(connection, transaction.getTimeout());
  handler.parameterize(stmt);
  return stmt;
}
複製程式碼

6.3 呼叫StatementHandler.query()方法

StatementHandler物件負責設定Statement物件中的查詢引數、處理JDBC返回的resultSet,將resultSet加工為List

6.3.1進入PreparedStatementHandler的query方法,該函式中終於看到了我們熟悉的程式碼。進行了最終的資料庫查詢操作。並將結果交給了ResultSetHandler處理

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  PreparedStatement ps = (PreparedStatement) statement;
  ps.execute();
 return resultSetHandler.<E> handleResultSets(ps);
複製程式碼

6.3.2 跟蹤ResultSetHandler的實現類DefaultResultSetHandler。找到了handleResultSets方法。

ResultSetHandler的handleResultSets(Statement) 方法會將Statement語句執行後生成的resultSet 結果集轉換成List 結果集

@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
  ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

  final List<Object> multipleResults = new ArrayList<Object>();

  int resultSetCount = 0;
  ResultSetWrapper rsw = getFirstResultSet(stmt);

  List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  int resultMapCount = resultMaps.size();
  validateResultMapsCount(rsw, resultMapCount);
  while (rsw != null && resultMapCount > resultSetCount) {
    ResultMap resultMap = resultMaps.get(resultSetCount);
    handleResultSet(rsw, resultMap, multipleResults, null);
    rsw = getNextResultSet(stmt);
    cleanUpAfterHandlingResultSet();
    resultSetCount++;
  }

  String[] resultSets = mappedStatement.getResultSets();
  if (resultSets != null) {
    while (rsw != null && resultSetCount < resultSets.length) {
      ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
      if (parentMapping != null) {
        String nestedResultMapId = parentMapping.getNestedResultMapId();
        ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
        handleResultSet(rsw, resultMap, null, parentMapping);
      }
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }
  }

  return collapseSingleResultList(multipleResults);
}
複製程式碼

五、總結

sql執行流程總結如下:

1.呼叫SqlSession的select方法,傳入StatementId(Mappr-xx.xml的namespace+id)和查詢條件引數

2.呼叫Configuration的getMappedStatement方法獲取StatementId對應的MappedStatement,並呼叫Executor的query方法

3.呼叫Executor的query方法,根據入參獲取具體的sql,封裝到BoundSql中

4.建立StatementHandler物件執行查詢操作

5.ResultSetHandler將查詢結果轉化為需要的格式

螢幕快照 2019-02-28 上午10.31.05.png

mybatis原始碼解讀---一條sql的旅程


相關文章