从一个简单例子聊 MyBatis(一)

从一个简单例子聊 MyBatis(一)

本文通过一个简单的```select`` 查询语句,通过源码分析聊聊Mybatis的运行机制以及他是如何处理SQL语句的。文末会给出这个例子的源代码。

我是一个栗子

先看一下这个例子,这个例子是最“纯粹”的MyBatis的使用,并没有结合Spring。
首先在单元测试代码里,通过studentService 查询id为1的学生,代码如下

    @Test
    public void testFindStudentById(){
        Student student = studentService.findStudentById(1);
        Assert.assertNotNull(student);
        System.out.println(student);
    }

studentServicefindStudentById的代码如下,通过MyBatisSqlSessionFactory获取sqlSession, 再通过sqlSession传递studentMapper得到接口实现,最后执行查询方法。

    public Student findStudentById(Integer studId){
        SqlSession sqlSession = MyBatisSqlSessionFactory.openSession();
        try {
            StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
            return studentMapper.findStudentById(studId);
        }finally {
            sqlSession.close();
        }
    }

StudentMapper接口的定义如下:

public interface StudentMapper {
   .....
    Student findStudentById(Integer id);
  .....
}

XML映射文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.StudentMapper">
   ....
    <select id="findStudentById" parameterType="int" resultType="Student">
        SELECT STUD_ID AS STUDID,NAME,EMAIL,DOB FROM STUDENTS WHERE STUD_ID=#{ID}
    </select>
    .....
</mapper>

那么MyBatis是如何找到XML的映射文件的呢?
下一节会通过源码分析深入理解MyBatis的执行过程和各个组件的作用。

MyBatis的层次结构

上面的例子中MyBatisSqlSessionFactory主要做了两件事:
1、SqlSessionFactoryBuilder通过读取mybatis-config.xml配置文件的方式生成Configuration组件并且返回SqlSessionFactory对象。
2、通过SqlSessionFactory中的openSession方法获取sqlSession

private static SqlSessionFactory sqlSessionFactory;
    public static SqlSessionFactory getSqlSessionFactory(){
        if(sqlSessionFactory == null){
            InputStream inputStream;
            try{
                inputStream = Resources.getResourceAsStream("mybatis-config.xml");
                //(1)读取配置文件生成Configuration
                sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            }catch (IOException e){
                System.out.println(e.getMessage());
            }
        }
        return sqlSessionFactory;
    }
    public static SqlSession openSession(){
         //(2)返回sqlSession对象
        return getSqlSessionFactory().openSession();
    }

Configuration

进一步跟进上述代码中的build方法。可以看到这里是生成XMLConfigBuilder来解析配置文件。

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
   .....
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
  .... 
  }

在XMLConfigBuilder通过parse函数返回Configuration, 而该函数最终调用parseConfiguration来解析我们写的mybatis-config.xml文件中的各个元素。这里我们抓到了Mybatis的第一个组件Configuration

  public Configuration parse() {
    ....
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }
  
  private void parseConfiguration(XNode root) {
    try {
      propertiesElement(root.evalNode("properties")); //issue #117 read properties first
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      settingsElement(root.evalNode("settings"));
      environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      //对Mapper进行解析
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

我们以上述mapperElement方法为例,看一下MyBatis是如何解析mybatis-config配置文件的<mappers>元素的。
解析的mappers的调用链路比较长,最终我们发现是调用XMLStatementBuilderparseStatementNode方法,这里的id 的值就是<select> 标签中的findStudentById

  public void parseStatementNode() {
    .......
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver);
  }

跟进到addMappedStatement方法中,可以看到该方法对id进行了处理

  public MappedStatement addMappedStatement(
    ....
   
    // 对id加上了接口的全限定名。id就变成了dao.StudentMapper.findStudentById
    // 可以唯一找到namespace为dao.StudentMapper下面id = findStudentById的MappedStatement
    id = applyCurrentNamespace(id, false);
    ....
    MappedStatement statement = statementBuilder.build();
    // 把生成的MappedStatement保存到configuration中
    configuration.addMappedStatement(statement);
    return statement;
  }

进入Configuration中的addMappedStatement方法看到MapperedStatement最终被放到了configuration的map中

 ....
 protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>
 .....
  public void addMappedStatement(MappedStatement ms) {
    mappedStatements.put(ms.getId(), ms);
  }
 ....

经过上面的分析我们看到在Mybatis中,每一个<select><insert><update><delete>标签,都会被解析为一个MappedStatement对象

Configuration
MyBatis所有的配置信息都维持在Configuration对象之中。

Executor

回到MyBatisSqlSessionFactory 中的openSession方法,最终定位到DefaultSqlSessionFactory类中的openSessionFromDataSource方法,在这个方法中,我们看到了创建出了MyBatis的四个核心组件:EnvironmentTransactionFactoryExecutorDefaultSqlSession。这里主要分析后面两个组件,EnvironmentTransactionFactory留到后面有机会在讲。

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //(1) 使用configuration创建Executor
      final Executor executor = configuration.newExecutor(tx, execType, autoCommit);
      // (2) 返回默认的DefaultSqlSession
      return new DefaultSqlSession(configuration, executor);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

进一步进入newExecutor方法, 如果没有在配置文件中指定,MyBatis 默认的ExecutorType 为Simple, cacheEnabled 默认为true。

  public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
    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 {
    //(1) 创建SimpleExecutor
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
    //(2) 创建代理executor,cacheEnabled 默认true
      executor = new CachingExecutor(executor, autoCommit);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

查看的SimpleExecutor的继承结构看到它继承BaseExecutor。


image.png

这里找了MyBatis另一个核心组件BoundSql, 我会在下一篇文章中介绍。下面我们一起分析一下BaseExecutor的query方法。

  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
   // 1.根据具体传入的参数,动态地生成需要执行的SQL语句,用BoundSql对象表示
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 2.为当前的查询创建一个缓存Key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

  @SuppressWarnings("unchecked")
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ....
     //(1)判断缓存是否有值
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
      //(2) 没有的情况下从数据库读取
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } 
    .....
    return list;
  }

queryFromDatabase方法会调用子类的doQuery执行查询。

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
    //(1)调用子类的doQuery执行查询,返回List 结果,然后将查询的结果放入缓存之中  
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

查看子类SimpleExecutor中的doQuery方法。可以看到通过StatementHandler实例,执行SQL语句,这里我们找到了StatementHandler核心组件,该组件将在下一篇文章中介绍。

  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();
      //(1)根据参数创建StatementHandler
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, rowBounds, resultHandler, boundSql);
      //(2)创建java.Sql.Statement对象,传递给StatementHandler对象 
      stmt = prepareStatement(handler, ms.getStatementLog());
      //(3)调用StatementHandler.query()方法,返回List结果集  
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

Executor
MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护

SqlSession

在分析Executor的那一节我们看到opensession返回的是DefaultSqlSession,这里我们找到了MyBatis的了另外一个核心组件SqlSession。查看DefaultSqlSession的接口可以看到包含了所有增删改查的接口。

image.png

定位SqlSession的查询方法,发现最终会调用selectList。进入该方法,可以看到先是通过configuration得到我们之前分析的MappedStatement, 然后交给Executor 做具体的执行。

  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        //(1) 采用之前生成的Configuration获取配置文件对应的Mapper
      MappedStatement ms = configuration.getMappedStatement(statement);
        //(2) 调用MyBatis的Executor执行查询
      List<E> result = executor.<E>query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
      return result;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

SqlSession
SqlSession提供了MyBatis操作数据库的API,包含了增删改查接口,是MyBatis的顶层接口

总结

本篇文章通过代码分析了MyBatis的Configuration、Executor、SqlSession的核心组件,说明了他们的主要功能,下一篇文章会解析其他核心组件,并会分析SQL语句在MyBatis中的执行流程。


PS:
测试项目源代码MyBatisTest
数据库脚本如下

create table STUDENTS(
stud_id int(11) not null auto_increment,
name varchar(50) not null,
email varchar(50) not null,
dob date default null,
primary key(stud_id)
);
insert into STUDENTS(stud_id,name,email,dob) values(1,"student1","student1@email.com","1999-08-08");
insert into STUDENTS(stud_id,name,email,dob) values(2,"student2","student2@email.com","2000-01-01");
select * from STUDENTS;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容