Mybatis源碼分析一:一條sql語句如何被執行

本篇爲原創文章,如需轉載,請標明原創地址。java

我先寫一個簡單的例子來執行一條sql語句

mapper.xmlmysql

<mapper namespace="com.example.demo1.mybatis.ArticleMapper">

    <select id="selectById" resultType="com.example.demo1.mybatis.Article" parameterType="java.lang.Long">
      select
       <include refid="baseColumns"/>
       from article where 1= 1
        and id = #{id}
    </select>

    <sql id="baseColumns">
        id,title
    </sql>

</mapper>

實體類sql

@Data
public class Article {
  private Long id;
  private String title;
}

測試類數據庫

public class MybatisTest {
  public static void main(String[] args) throws IOException {
    // sqlSessionFactory是一個複雜對象,一般建立一個複雜對象會使用建造器來構建,這裏首先建立建造器
    SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();

    // configuration對象對應mybatis的config文件,爲了測試簡便,我這裏直接建立Configuration對象而不經過xml解析得到
    Configuration configuration = new Configuration();
    configuration.setEnvironment(buildEnvironment());

    // 解析一個mapper.xml爲MappedStatement並加入到configuration中
    InputStream inputStream = Resources.getResourceAsStream("mybatis/Article.xml");
    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, "mybatis/Article.xml", configuration.getSqlFragments());
    mapperParser.parse();
    
    SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(configuration);

    // 建立一個sqlSession,這裏使用的是簡單工廠設計模式
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 執行最終的sql,查詢文章id爲1的文章
    Article article = sqlSession.selectOne("com.example.demo1.mybatis.ArticleMapper.selectById",1L);
    // 打印文件的標題
    System.out.println(article.getTitle());
    // sqlSession默認不會自動關閉,咱們須要手動關閉
    sqlSession.close();
  }

  private static Environment buildEnvironment() {
    return new Environment.Builder("test")
            .transactionFactory(getTransactionFactory())
            .dataSource(getDataSource()).build();
  }

  private static DataSource getDataSource() {
    String url = "url";
    String user = "user";
    String password = "password";

    Properties properties = new Properties();
    properties.setProperty("url", url);
    properties.setProperty("username", user);
    properties.setProperty("password", password);
    properties.setProperty("driver", "com.mysql.jdbc.Driver");
    properties.setProperty("driver.encoding", "UTF-8");

    PooledDataSourceFactory factory = new PooledDataSourceFactory();
    factory.setProperties(properties);
    DataSource dataSource = factory.getDataSource();
    return dataSource;
  }

  private static TransactionFactory getTransactionFactory() {
    return new JdbcTransactionFactory();
  }

分析sqlSession.selectOne("com.example.demo1.mybatis.ArticleMapper.selectById",1L);設計模式

public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    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;
    }
  }

經過查看源碼,咱們發現無論是查詢一條數據仍是查詢多條數據都是執行的selectList方法,查詢一條的時候只要取list的第一條數據便可。緩存

public <E> List<E> selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      /*
        根據statement id找到對應的MappedStatement,而statement id對應的就是mapper的namespace+crud操做的id
        在本例中就是com.example.demo1.mybatis.ArticleMapper.selectById
      */
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 委託執行器來執行查詢操做
      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();
    }
  }

使用執行器來執行查詢操做,簡單的執行器只會執行sql,並將結果放入到一級緩存中,帶二級緩存的執行器會增長一層緩存讀寫操做,這裏先只討論簡單執行器的執行mybatis

========================================================================app

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //獲得綁定sql
    BoundSql boundSql = ms.getBoundSql(parameter);
    //建立緩存Key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    //查詢
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }
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
        StatementHandler的做用主要有如下幾個:
          1.從sqlSource獲取最終須要執行的sql
          2.建立jdbc的statement對象
          3.給statement對象賦值
          4.執行statement.execute方法執行賦值的sql
          5.經過resultHandler對resultSet結果集進行處理收集,得到最終的結果
          6.返回最終的結果
      */
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 經過jdbc鏈接建立一個全新的prepareStatement,並對其進行賦值,對應上面步驟的2,3
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 執行賦值後的sql並對結果進行處理收集,對應上面步驟的4,5
      return handler.<E>query(stmt, resultHandler);
    } finally {
      // 執行statement.close方法
      closeStatement(stmt);
    }
  }

建立statementHandler對象框架

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    /* 建立一個路由statementHandler
       根據statementType進行路由,根據jdbc的基本知識咱們知道經常使用的statementType有三種:
        1.STATEMENT 硬編碼的語句,有sql注入風險
        2.PREPARED  預編譯sql的語句,通常狀況下都使用這個
        3.CALLABLE  執行存儲過程的語句
       一般咱們使用的都是preparedStatement
     */
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    /* 
    經過上一個步驟statementHandler已經被建立好,preparedStatement也已被初始化
    咱們獲得了一個sql爲select id ,title from article where id = ? 的preparedStatement
    想想,若是咱們的sql爲 select id,title from article 返回多條記錄,咱們要分頁的話怎麼辦?
    一種方法是咱們在mapper.xml中在sql語句尾部手動添加limit *,*來進行分頁(以mysql舉例)
    另外一種方法咱們能夠經過插件的方式來實現,像經常使用的PageHelper插件就是基於此來實現的分頁功能。
    使用插件的好處是將分頁功能和sql語句分離,達到去耦合的目的。這樣咱們切換數據庫的時候sql語句並不須要改動。
    * */
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }
/**
   * 這個方法沒有什麼好分析的,就是執行sql語句,並對結果resultSet進行處理。
   * 默認的結果集處理器就是DefaultResultSetHandler,其處理方案就是遍歷resultSet集合,
   * 將全部的數據追加到List<Article>集合中去。
   */
  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.<E> handleResultSets(ps);
  }

關於MappedStament

該對象是mapper.xml在對象中的體現,是整個mybatis框架中最爲核心的對象,咱們也能夠沒必要經過xml文件來構建該對象,能夠直接經過編碼方式構建,像最經常使用的簡單的增刪改查操做徹底能夠手動構建mappedStatement對象並加入到mybatis容器中,這樣咱們就不須要在xml文件中手寫CRUD操做了,mybatis-plus框架設計的思想就是鑑於此。ide

總結:

其實Mybatis執行一條sql,底層仍是用的最基本的jdbc操做,只不過將事物,數據源,參數的設置,結果的收集轉換都封裝了起來,讓咱們在開發中專一於sql自己,而忽略那些與業務不相關的步驟(結果對象的映射,開啓事物,關閉事物等等操做),提升了項目的內聚性。

相關文章
相關標籤/搜索