從原始碼聊聊mybatis一次查詢都經歷了些什麼

SimpleYoung發表於2019-03-26

原文地址
mybatis是一種非常流行的ORM框架,可以通過一些靈活簡單的配置,大大提升我們運算元據庫的效率,當然,我覺得它如此受歡迎的原因更主要的是,它的原始碼設計的非常簡單。接下來我們就來聊聊使用mybatis做一次資料庫查詢操作背後都經歷了什麼。

首先我們先上一段非常簡單的程式碼,這是原始的JDBC方式的資料庫操作。

// 1. 建立資料來源
DataSource dataSource = getDataSource();
// 2. 建立資料庫連線
try (Connection conn = dataSource.getConnection()) {
    try {
        conn.setAutoCommit(false);
        // 3. 建立Statement
        PreparedStatement stat = conn.prepareStatement("select * from std_addr where id=?");
        stat.setLong(1, 123456L);
        // 4. 執行Statement,獲取結果集
        ResultSet resultSet = stat.executeQuery();
        // 5. 處理結果集,這一步往往是非常複雜的
        processResultSet(resultSet);
        // 6.1 成功提交,對於查詢操作,步驟6是不需要的
        conn.commit();
    } catch (Throwable throwable) {
    	// 6.2 失敗回滾
        conn.rollback();
    }
}
複製程式碼

下面這段是mybatis連線資料庫以及做同樣的查詢操作的程式碼。

DataSource dataSource = getDataSource();
TransactionFactory txFactory = new JdbcTransactionFactory();
Environment env = new Environment("test", txFactory, dataSource);
Configuration conf = new Configuration(env);
conf.setMapUnderscoreToCamelCase(true);
conf.addMapper(AddressMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(conf);
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
    AddressMapper mapper = sqlSession.getMapper(AddressMapper.class);
    Address addr = mapper.getById(123456L);
}
複製程式碼

這是mybatis的Mapper,也非常簡單

@Mapper
public interface AddressMapper {
    String TABLE = "std_addr";

    @Select("select * from " + TABLE + " where id=#{id}")
    Address getById(long id);
}
複製程式碼

從上面的程式碼可以看出,通過mybatis查詢資料庫需要以下幾個步驟:

  1. 準備執行環境Environment,即建立資料來源和事務工廠
  2. 建立核心配置物件Configuration,此物件包含mybatis的配置資訊(xml或者註解方式配置)
  3. 建立SqlSessionFactory,用於建立資料庫會話SqlSession
  4. 建立SqlSession進行資料庫操作

下面我們從原始碼逐步分析mybatis在一次select查詢中這幾個步驟的詳細情況。

準備執行環境Environment

Environment有兩個核心屬性,dataSource和transactionFactory,下面是原始碼

public final class Environment {
  private final String id;
  private final TransactionFactory transactionFactory;
  private final DataSource dataSource;
}
複製程式碼

其中,dataSource用來獲取資料庫連線,transactionFactory用來建立事務。
我們詳細看一下mybatis的JdbcTransactionFactory的原始碼,這裡可以通過資料來源或者資料庫連線來建立JdbcTransaction。

public class JdbcTransactionFactory implements TransactionFactory {
  public Transaction newTransaction(Connection conn) {
    return new JdbcTransaction(conn);
  }

  public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
    return new JdbcTransaction(ds, level, autoCommit);
  }
}
複製程式碼

我把JdbcTransaction的原始碼精簡了一下,大概是這個樣子的。這裡實際上就是把JDBC的DataSource或者一個Connection託管給了mybatis的Transaction物件,由Transaction來管理事務的提交與回滾。

public class JdbcTransaction implements Transaction {
  protected Connection connection;
  protected DataSource dataSource;
  protected TransactionIsolationLevel level;
  protected boolean autoCommmit;

  public Connection getConnection() throws SQLException {
    if (connection == null) {
      connection = dataSource.getConnection();
      if (level != null) {
        connection.setTransactionIsolation(level.getLevel());
      }
      if (connection.getAutoCommit() != autoCommmit) {
        connection.setAutoCommit(autoCommmit);
      }
    }
    return connection;
  }

  public void commit() throws SQLException {
    if (connection != null && !connection.getAutoCommit()) {
      connection.commit();
    }
  }

  public void rollback() throws SQLException {
    if (connection != null && !connection.getAutoCommit()) {
      connection.rollback();
    }
  }
}
複製程式碼

到這裡,執行環境Environment已經準備完畢,我們可以從Environment中獲取DataSource或者建立一個新的Transaction,從而建立一個資料庫連線。

建立核心配置物件Configuration

Configuration類非常複雜,包含很多配置資訊,我們優先關注以下核心屬性

public class Configuration {
  protected Environment environment;
  protected boolean cacheEnabled = true;
  protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
  // 儲存著所有Mapper的動態代理物件
  protected final MapperRegistry mapperRegistry;
  // 儲存著所有型別處理器,處理Java型別和JDBC型別的轉換
  protected final TypeHandlerRegistry typeHandlerRegistry;
  // 儲存配置的Statement資訊,可以是XML或註解
  protected final Map<String, MappedStatement> mappedStatements;
  // 儲存二級快取資訊
  protected final Map<String, Cache> caches;
  // 儲存配置的ResultMap資訊
  protected final Map<String, ResultMap> resultMaps;
}
複製程式碼
  1. 從SqlSessionFactory的build方法可以看出,mybatis提供了兩種解析配置資訊的方式,分別是XMLConfigBuilder和MapperAnnotationBuilder。解析配置的過程,其實就是填充上述Configuration核心屬性的過程。
// 根據XML構建
InputStream xmlInputStream = Resources.getResourceAsStream("xxx.xml");
SqlSessionFactory xmlSqlSessionFactory = new SqlSessionFactoryBuilder().build(xmlInputStream);
// 根據註解構建
Configuration configuration = new Configuration(environment);
configuration.addMapper(AddressMapper.class);
SqlSessionFactory annoSqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
複製程式碼
  1. TypeHandlerRegistry處理Java型別和JDBC型別的對映關係,從TypeHandler的介面定義可以看出,主要是用來為PreparedStatement設定引數和從結果集中獲取結果的
public interface TypeHandlerRegistry<T> {
  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
  T getResult(ResultSet rs, String columnName) throws SQLException;
  T getResult(ResultSet rs, int columnIndex) throws SQLException;
  T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
複製程式碼

總而言之,Configuration物件包含了mybatis的Statement、ResultMap、Cache等核心配置,這些配置資訊是後續執行SQL操作的關鍵。

建立SqlSessionFactory

我們提供new SqlSessionFactoryBuilder().build(conf)構建了一個DefaultSqlSessionFactory,這是預設的SqlSessionFactory

public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}
複製程式碼

DefaultSqlSessionFactory的核心方法有兩個,程式碼精簡過後是下面這個樣子的。其實都是一個套路,通過資料來源或者連線建立一個事務(上面提到的TransactionFactory建立事務的兩種方式),然後建立執行器Executor,最終組合成一個DefaultSqlSession,代表著一次資料庫會話,相當於一個JDBC的連線週期。

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  final Environment environment = configuration.getEnvironment();
  final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
  Transaction tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
  final Executor executor = configuration.newExecutor(tx, execType);
  return new DefaultSqlSession(configuration, executor, autoCommit);
}

private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
  boolean autoCommit;
  try {
    autoCommit = connection.getAutoCommit();
  } catch (SQLException e) {
    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);
}
複製程式碼

下面這段程式碼是Configuration物件建立執行器Executor的過程,預設的情況下會建立SimpleExecutor,然後在包裝一層用於二級快取的CachingExecutor,很明顯Executor的設計是一個典型的裝飾者模式。

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  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);
  }
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}
複製程式碼

建立SqlSession進行資料庫操作

進行一次資料庫查詢操作的步驟如下:

1. 通過DefaultSqlSessionFactory建立一個DefaultSqlSession物件
SqlSession sqlSession = sqlSessionFactory.openSession(true);
複製程式碼
2. 建立獲取一個Mapper的代理物件
AddressMapper mapper = sqlSession.getMapper(AddressMapper.class);
複製程式碼

DefaultSqlSession的getMapper方法引數是我們定義的Mapper介面的Class物件,最終是從Configuration物件的mapperRegistry登錄檔中獲取這個Mapper的代理物件。
下面是MapperRegistry的getMapper方法的核心程式碼,可見這裡是通過MapperProxyFactory建立代理

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  return mapperProxyFactory.newInstance(sqlSession);
}
複製程式碼

然後是MapperProxyFactory的newInstance方法,看上去是不是相當熟悉。很明顯,這是一段JDK動態代理的程式碼,這裡會返回Mapper介面的一個代理類例項。

public T newInstance(SqlSession sqlSession) {
  final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
複製程式碼
3. 呼叫代理物件的查詢方法
Address byId = mapper.getById(110114);
複製程式碼

這裡實際上是呼叫到Mapper對應的MapperProxy,下面是MapperProxy的invoke方法的一部分。可見,這裡針對我們呼叫的Mapper的抽象方法,建立了一個對應的代理方法MapperMethod。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  return mapperMethod.execute(sqlSession, args);
}
複製程式碼

我精簡了MapperMethod的execute方法的程式碼,如下所示。其實最終動態代理為我們呼叫了SqlSession的select方法。

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
  case SELECT:
    Object param = method.convertArgsToSqlCommandParam(args);
    result = sqlSession.selectOne(command.getName(), param);
    break;
  }
  return result;
}
複製程式碼
4. 接下來的關注點在SqlSession

SqlSession的selectOne方法最終是呼叫的selectList,這個方法也非常簡單,入參statement其實就是我們定義的Mapper中被呼叫的方法的全名,本例中就是x.x.AddressMapper.getById,通過statement獲取對應的MappedStatement,然後交由executor執行query操作。

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  MappedStatement ms = configuration.getMappedStatement(statement);
  return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}
複製程式碼

前面我們提到過預設的執行器是SimpleExecutor再裝飾一層CachingExecutor,下面看看CachingExecutor的query程式碼,在這個方法之前會先根據SQL和引數等資訊建立一個快取的CacheKey。下面這段程式碼也非常明瞭,如果配置了Mapper級別的二級快取(預設是沒有配置的),則優先從快取中獲取,否則將呼叫被裝飾者也就是SimpleExecutor(其實是BaseExecutor)的query方法。

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
  throws SQLException {
  Cache cache = ms.getCache();
  // cache不為空,表示當前Mapper配置了二級快取
  if (cache != null) {
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      List<E> list = (List<E>) tcm.getObject(cache, key);
      // 快取未命中,查庫
      if (list == null) {
        list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list);
      }
      return list;
    }
  }
  return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
複製程式碼

BaseExecutor的query方法的核心程式碼如下所示,這裡有個一級快取,是開啟的,預設的作用域是SqlSession級別的。如果一級快取未命中,則呼叫queryFromDatabase方法從資料庫中查詢。

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  List<E> 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);
  }
  return list;
}
複製程式碼

然後將呼叫子類SimpleExecutor的doQuery方法,核心程式碼如下。

public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  Configuration configuration = ms.getConfiguration();
  StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
  stmt = prepareStatement(handler, ms.getStatementLog());
  return handler.<E>query(stmt, resultHandler);
}
複製程式碼

通過原始碼發現Configuration建立的是一個RoutingStatementHandler,然後根據MappedStatement的statementType屬性建立一個具體的StatementHandler(三種STATEMENT、PREPARED或者CALLABLE)。終於出現了一些熟悉的東西了,這不就是JDBC的三種Statement嗎。我們選擇其中的PreparedStatementHandler來看一看原始碼,這裡就很清晰了,就是呼叫了JDBC的PreparedStatement的execute方法,然後將結果交由ResultHandler處理。

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

從上面doQuery的程式碼可以看出,執行的Statement是由prepareStatement方法建立的,可以看出這裡是呼叫了StatementHandler的prepare方法建立Statement,實際上是通過MappedStatement的SQL、引數等資訊,建立了一個預編譯的PrepareStatement。

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;
}
複製程式碼

最終,這個PrepareStatement的執行結果ResultSet,會交由DefaultResultSetHandler來處理,然後根據配置中的型別、Results、返回值等資訊,生成對應的實體物件。

到這裡我們就分析完了mybatis做一次查詢操作所經歷的全部流程。當然,這裡面還有一些細節沒有提到,比如說二級快取、引數和結果集的解析等,這些具體的內容可能會在後續的mybatis原始碼解析文章中詳細描述。

相關文章