转载

Mybatis源码分析一:一条sql语句如何被执行

本篇为原创文章,如需转载,请标明原创地址。

我先写一个简单的例子来执行一条sql语句

mapper.xml

<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>

实体类

@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,并将结果放入到一级缓存中,带二级缓存的执行器会增加一层缓存读写操作,这里先只讨论简单执行器的执行

========================================================================

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框架设计的思想就是鉴于此。

总结:

其实Mybatis执行一条sql,底层还是用的最基本的jdbc操作,只不过将事物,数据源,参数的设置,结果的收集转换都封装了起来,让我们在开发中专注于sql本身,而忽略那些与业务不相关的步骤(结果对象的映射,开启事物,关闭事物等等操作),提高了项目的内聚性。

原文  https://segmentfault.com/a/1190000019274133
正文到此结束
Loading...