美文网首页MyBatis
从一个简单例子聊 MyBatis(一)

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

作者: 樂浩beyond | 来源:发表于2018-06-20 17:18 被阅读141次

    从一个简单例子聊 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;
    

    相关文章

      网友评论

        本文标题:从一个简单例子聊 MyBatis(一)

        本文链接:https://www.haomeiwen.com/subject/oamxyftx.html