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

原文地址
mybatis是一種很是流行的ORM框架,能夠經過一些靈活簡單的配置,大大提高咱們操做數據庫的效率,固然,我以爲它如此受歡迎的緣由更主要的是,它的源碼設計的很是簡單。接下來咱們就來聊聊使用mybatis作一次數據庫查詢操做背後都經歷了什麼。java

首先咱們先上一段很是簡單的代碼,這是原始的JDBC方式的數據庫操做。sql

// 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查詢數據庫須要如下幾個步驟:mybatis

  1. 準備運行環境Environment,即建立數據源和事務工廠
  2. 建立核心配置對象Configuration,此對象包含mybatis的配置信息(xml或者註解方式配置)
  3. 建立SqlSessionFactory,用於建立數據庫會話SqlSession
  4. 建立SqlSession進行數據庫操做

下面咱們從源碼逐步分析mybatis在一次select查詢中這幾個步驟的詳細狀況。app

準備運行環境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。ui

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來管理事務的提交與回滾。this

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,從而建立一個數據庫鏈接。spa

建立核心配置對象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源碼解析文章中詳細描述。

相關文章
相關標籤/搜索