原始碼解析MyBatis Sharding-Jdbc SQL語句執行流程詳解(文末有流程圖)

中介軟體興趣圈發表於2019-11-19

本文將詳細介紹Mybatis SQL語句執行的全流程,本文與上篇具有一定的關聯性,建議先閱讀該系列中的前面3篇文章,重點掌握Mybatis Mapper類的初始化過程,因為在Mybatis中,Mapper是執行SQL語句的入口,類似下面這段程式碼:

@Service
public UserService implements IUserService {
 	@Autowired
    private UserMapper userMapper;
    public User findUser(Integer id) {
        return userMapper.find(id);
    }
}
複製程式碼

開始進入本文的主題,以原始碼為手段,分析Mybatis執行SQL語句的流行,並且使用了資料庫分庫分表中介軟體sharding-jdbc,其版本為sharding-jdbc1.4.1。

為了方便大家對本文的原始碼分析,先給出Mybatis層面核心類的方法呼叫序列圖。

1、SQL執行序列圖

在這裡插入圖片描述

2、原始碼解析SQL執行流程

接下來從從原始碼的角度對其進行剖析。

溫馨提示:在本文的末尾,還會給出一張詳細的Mybatis Shardingjdbc語句執行流程圖。(請勿錯過哦)。

2.1 MapperProxy#invoker

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);   // @1
    return mapperMethod.execute(sqlSession, args);                                     // @2
  }
複製程式碼

程式碼@1:建立並快取MapperMethod物件。

程式碼@2:呼叫MapperMethod物件的execute方法,即mapperInterface中定義的每一個方法最終會對應一個MapperMethod。

2.2 MapperMethod#execute

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    if (SqlCommandType.INSERT == command.getType()) { 
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
    } else if (SqlCommandType.SELECT == command.getType()) {
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
    } else {
      throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }
複製程式碼

該方法主要是根據SQL型別,insert、update、select等操作,執行對應的邏輯,本文我們以查詢語句,進行跟蹤,進入executeForMany(sqlSession, args)方法。

2.3 MapperMethod#executeForMany

private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
    } else {
      result = sqlSession.<E>selectList(command.getName(), param);
    }
    // issue #510 Collections & arrays support
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
      if (method.getReturnType().isArray()) {
        return convertToArray(result);
      } else {
        return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
      }
    }
    return result;
  }
複製程式碼

該方法也比較簡單,最終通過SqlSession呼叫selectList方法。

2.4 DefaultSqlSession#selectList

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);   // @1
      List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);   // @2
      return result;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
複製程式碼

程式碼@1:根據資源名稱獲取對應的MappedStatement物件,此時的statement為資源名稱,例如com.demo.UserMapper.findUser。至於MappedStatement物件的生成在上一節初始化時已詳細介紹過,此處不再重複介紹。

程式碼@2:呼叫Executor的query方法。這裡說明一下,其實一開始會進入到CachingExecutor#query方法,由於CachingExecutor的Executor delegate屬性預設是SimpleExecutor,故最終還是會進入到SimpleExecutor#query中。

接下來我們進入到SimpleExecutor的父類BaseExecutor的query方法中。

2.5 BaseExecutor#query

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {   // @1
    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;                                            // @2
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);                   // @3
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      deferredLoads.clear(); // issue #601
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {                         // @4
        clearLocalCache(); // issue #482
      }
    }
    return list;
  }
複製程式碼

程式碼@1:首先介紹一下該方法的入參,這些類都是Mybatis的重要類:

  • MappedStatement ms 對映語句,一個MappedStatemnet物件代表一個Mapper中的一個方法,是對映的最基本物件。
  • Object parameter SQL語句的引數列表。
  • RowBounds rowBounds 行邊界物件,其實就是分頁引數limit與size。
  • ResultHandler resultHandler 結果處理Handler。
  • CacheKey key Mybatis快取Key
  • BoundSql boundSql SQL與引數繫結資訊,從該物件可以獲取在對映檔案中的SQL語句。

程式碼@2:首先從快取中獲取,Mybatis支援一級快取(SqlSession)與二級快取(多個SqlSession共享)。

程式碼@3:從資料庫查詢結果,然後進入到doQuery方法,執行真正的查詢動作。

程式碼@4:如果一級快取是語句級別的,則語句執行完畢後,刪除快取。

2.6 SimpleExecutor#doQuery

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 handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);   // @1
      stmt = prepareStatement(handler, ms.getStatementLog());                                                                                                                   // @2
      return handler.<E>query(stmt, resultHandler);                                                                                                                                        // @3
    } finally {
      closeStatement(stmt);
    }
  }
複製程式碼

程式碼@1:建立StatementHandler,這裡會加入Mybatis的外掛擴充套件機制(將在下篇詳細介紹),如圖所示:

在這裡插入圖片描述
程式碼@2:建立Statement物件,注意,這裡就是JDBC協議的java.sql.Statement物件了。

程式碼@3:使用Statment物件執行SQL語句。

接下來詳細介紹Statement物件的建立過程與執行過程,即分佈詳細跟蹤程式碼@2與程式碼@3。

3、Statement物件建立流程

3.1 java.sql.Connection物件建立

3.1.1 SimpleExecutor#prepareStatement

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

建立Statement物件,分成三步: 程式碼@1:建立java.sql.Connection物件。

程式碼@2:使用Connection物件建立Statment物件。

程式碼@3:對Statement進行額外處理,特別是PrepareStatement的引數設定(ParameterHandler)。

3.1.2 SimpleExecutor#getConnection

getConnection方法,根據上面流程圖所示,先是進入到org.mybatis.spring.transaction.SpringManagedTransaction,再通過spring-jdbc框架,利用DataSourceUtils獲取連線,其程式碼如下:

public static Connection doGetConnection(DataSource dataSource) throws SQLException {  
		Assert.notNull(dataSource, "No DataSource specified");
		ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); 
		if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
			conHolder.requested();
			if (!conHolder.hasConnection()) {
				conHolder.setConnection(dataSource.getConnection());
			}
			return conHolder.getConnection();
		}
		// Else we either got no holder or an empty thread-bound holder here.

		logger.debug("Fetching JDBC Connection from DataSource");
		Connection con = dataSource.getConnection();      // @1

        // 這裡省略與事務處理相關的程式碼
		return con;
	}
複製程式碼

程式碼@1:通過DataSource獲取connection,那此處的DataSource是“誰”呢?看一下我們工程的配置:

在這裡插入圖片描述
在這裡插入圖片描述

故最終dataSouce.getConnection獲取的連線,是從SpringShardingDataSource中獲取連線。

com.dangdang.ddframe.rdb.sharding.jdbc.ShardingDataSource#getConnection
public ShardingConnection getConnection() throws SQLException {
        MetricsContext.init(shardingProperties);
        return new ShardingConnection(shardingContext);
}
複製程式碼

返回的結果如下:

在這裡插入圖片描述
備註:這裡只是返回了一個ShardingConnection物件,該物件包含了分庫分表上下文,但此時並沒有執行具體的分庫操作(切換資料來源)。

Connection的獲取流程清楚後,我們繼續來看一下Statemnet物件的建立。

3.2 java.sql.Statement物件建立

stmt = prepareStatement(handler, ms.getStatementLog());            
複製程式碼

上面語句的呼叫鏈:RoutingStatementHandler -》BaseStatementHandler

3.2.1 BaseStatementHandler#prepare

public Statement prepare(Connection connection) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
      statement = instantiateStatement(connection);    // @1
      setStatementTimeout(statement);                         // @2
      setFetchSize(statement);                                      // @3
      return statement;
    } catch (SQLException e) {
      closeStatement(statement);
      throw e;
    } catch (Exception e) {
      closeStatement(statement);
      throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
  }
複製程式碼

程式碼@1:根據Connection物件(本文中是ShardingConnection)來建立Statement物件,其預設實現類:PreparedStatementHandler#instantiateStatement方法。

程式碼@2:為Statement設定超時時間。

程式碼@3:設定fetchSize。

3.2.2 PreparedStatementHandler#instantiateStatement

protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
        return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
      } else {
        return connection.prepareStatement(sql, keyColumnNames);
      }
    } else if (mappedStatement.getResultSetType() != null) {
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    } else {
      return connection.prepareStatement(sql);
    }
  }
複製程式碼

其實Statement物件的建立,就比較簡單了,既然Connection是ShardingConnection,那就看一下其對應的prepareStatement方法即可。

3.2.2 ShardingConnection#prepareStatement

public PreparedStatement prepareStatement(final String sql) throws SQLException {   // sql,為配置在mybatis xml檔案中的sql語句
        return new ShardingPreparedStatement(this, sql);
}
ShardingPreparedStatement(final ShardingConnection shardingConnection, 
            final String sql, final int resultSetType, final int resultSetConcurrency, final int resultSetHoldability) {
        super(shardingConnection, resultSetType, resultSetConcurrency, resultSetHoldability);
        preparedSQLRouter = shardingConnection.getShardingContext().getSqlRouteEngine().prepareSQL(sql);
}
複製程式碼

在構建ShardingPreparedStatement物件的時候,會根據SQL語句建立解析SQL路由的解析器物件,但此時並不會執行相關的路由計算,PreparedStatement物件建立完成後,就開始進入SQL執行流程中。

4、SQL執行流程

接下來我們繼續看SimpleExecutor#doQuery方法的第3步,執行SQL語句:

handler.<E>query(stmt, resultHandler)。
複製程式碼

首先會進入RoutingStatementHandler這個類中,進行Mybatis層面的路由(主要是根據Statement型別)

在這裡插入圖片描述
然後進入到PreparedStatementHandler#query中。

4.1 PreparedStatementHandler#query

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

程式碼@1:呼叫PreparedStatement的execute方法,由於本例是使用了Sharding-jdbc分庫分表,此時呼叫的具體實現為:ShardingPreparedStatement。

程式碼@2:處理結果。

我們接下來分別來跟進execute與結果處理方法。

4.2 ShardingPreparedStatement#execute

public boolean execute() throws SQLException {
    try {
        return new PreparedStatementExecutor(getShardingConnection().getShardingContext().getExecutorEngine(), routeSQL()).execute(); // @1
    } finally {
        clearRouteContext();
    }
}
複製程式碼

這裡奧妙無窮,其關鍵點如下: 1)創造PreparedStatementExecutor物件,其兩個核心引數:

  • ExecutorEngine executorEngine:shardingjdbc執行引擎。
  • Collection< PreparedStatementExecutorWrapper> preparedStatemenWrappers 一個集合,每一個集合是PreparedStatement的包裝類,這個集合如何而來?

2)preparedStatemenWrappers是通過routeSQL方法產生的。

3)最終呼叫PreparedStatementExecutor方法的execute來執行。

接下來分別看一下routeSQL與execute方法。

4.3 ShardingPreparedStatement#routeSQL

private List<PreparedStatementExecutorWrapper> routeSQL() throws SQLException {
        List<PreparedStatementExecutorWrapper> result = new ArrayList<>();
        SQLRouteResult sqlRouteResult = preparedSQLRouter.route(getParameters());   // @1
        MergeContext mergeContext = sqlRouteResult.getMergeContext();                      
        setMergeContext(mergeContext);
        setGeneratedKeyContext(sqlRouteResult.getGeneratedKeyContext());
        for (SQLExecutionUnit each : sqlRouteResult.getExecutionUnits()) {                      // @2          
            PreparedStatement preparedStatement = (PreparedStatement) getStatement(getShardingConnection().getConnection(each.getDataSource(), sqlRouteResult.getSqlStatementType()), each.getSql());     // @3
            replayMethodsInvocation(preparedStatement);
            getParameters().replayMethodsInvocation(preparedStatement);
            result.add(wrap(preparedStatement, each));
        }
        return result;
}
複製程式碼

程式碼@1:根據SQL引數進行路由計算,本文暫不關注其具體實現細節,這些將在具體分析Sharding-jdbc時具體詳解,在這裡就直觀看一下其結果:

程式碼@2、@3:對分庫分表的結果進行遍歷,然後使用底層Datasource來建立Connection,建立PreparedStatement 物件。

routeSQL就暫時講到這,從這裡我們得知,會在這裡根據路由結果,使用底層的具體資料來源建立對應的Connection與PreparedStatement 物件。

4.4 PreparedStatementExecutor#execute

public boolean execute() {
    Context context = MetricsContext.start("ShardingPreparedStatement-execute");
    eventPostman.postExecutionEvents();
    final boolean isExceptionThrown = ExecutorExceptionHandler.isExceptionThrown();
    final Map<String, Object> dataMap = ExecutorDataMap.getDataMap();
    try {
        if (1 == preparedStatementExecutorWrappers.size()) {     // @1
            PreparedStatementExecutorWrapper preparedStatementExecutorWrapper = preparedStatementExecutorWrappers.iterator().next();
            return executeInternal(preparedStatementExecutorWrapper, isExceptionThrown, dataMap);
        }
        List<Boolean> result = executorEngine.execute(preparedStatementExecutorWrappers, new ExecuteUnit<PreparedStatementExecutorWrapper, Boolean>() {    // @2
        
            @Override
            public Boolean execute(final PreparedStatementExecutorWrapper input) throws Exception {
                synchronized (input.getPreparedStatement().getConnection()) {
                    return executeInternal(input, isExceptionThrown, dataMap);
                }
            }
        });
        return (null == result || result.isEmpty()) ? false : result.get(0);
    } finally {
        MetricsContext.stop(context);
    }
 }
複製程式碼

程式碼@1:如果計算出來的路由資訊為1個,則同步執行。

程式碼@2:如果計算出來的路由資訊有多個,則使用執行緒池非同步執行。

那還有一個問題,通過PreparedStatement#execute方法執行後,如何返回結果呢?特別是非同步執行的。

在上文其實已經談到:

4.4 DefaultResultSetHandler#handleResultSets

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);         // @1
    //省略部分程式碼,完整程式碼可以檢視DefaultResultSetHandler方法。
    return collapseSingleResultList(multipleResults);
  }

private ResultSetWrapper getFirstResultSet(Statement stmt) throws SQLException {
    ResultSet rs = stmt.getResultSet();              // @2
    while (rs == null) {
      // move forward to get the first resultset in case the driver
      // doesn't return the resultset as the first result (HSQLDB 2.1)
      if (stmt.getMoreResults()) {
        rs = stmt.getResultSet();
      } else {
        if (stmt.getUpdateCount() == -1) {
          // no more results. Must be no resultset
          break;
        }
      }
    }
    return rs != null ? new ResultSetWrapper(rs, configuration) : null;
  }
複製程式碼

我們看一下其關鍵程式碼如下: 程式碼@1:呼叫Statement#getResultSet()方法,如果使用shardingJdbc,則會呼叫ShardingStatement#getResultSet(),並會處理分庫分表結果集的合併,在這裡就不詳細進行介紹,該部分會在shardingjdbc專欄詳細分析。

程式碼@2:jdbc statement中獲取結果集的通用寫法,這裡也不過多的介紹。

mybatis shardingjdbc SQL執行流程就介紹到這裡了,為了方便大家對上述流程的理解,最後給出SQL執行的流程圖:

在這裡插入圖片描述

Mybatis Sharding-Jdbc的SQL執行流程就介紹到這裡了,從圖中也能清晰看到Mybatis的拆件機制,將在下文詳細介紹。


作者介紹:《RocketMQ技術內幕》作者,維護公眾號:中介軟體興趣圈,目前主要發表了原始碼閱讀java集合、JUC(java併發包)、Netty、ElasticJob、Mycat、Dubbo、RocketMQ、mybaits等系列原始碼。

在這裡插入圖片描述

相關文章