美文网首页Mybatis
MyBatis原理系列(三)-手把手带你了解SqlSession

MyBatis原理系列(三)-手把手带你了解SqlSession

作者: Renaissance_ | 来源:发表于2020-12-21 18:15 被阅读0次

    MyBatis原理系列(一)-手把手带你阅读MyBatis源码
    MyBatis原理系列(二)-手把手带你了解MyBatis的启动流程
    MyBatis原理系列(三)-手把手带你了解SqlSession,SqlSessionFactory,SqlSessionFactoryBuilder的关系
    MyBatis原理系列(四)-手把手带你了解MyBatis的Executor执行器
    MyBatis原理系列(五)-手把手带你了解Statement、StatementHandler、MappedStatement间的关系
    MyBatis原理系列(六)-手把手带你了解BoundSql的创建过程
    MyBatis原理系列(七)-手把手带你了解如何自定义插件
    MyBatis原理系列(八)-手把手带你了解一级缓存和二级缓存
    MyBatis原理系列(九)-手把手带你了解MyBatis事务管理机制

    在上篇文章中,我们讲解了MyBatis的启动流程,以及启动过程中涉及到的组件,在本篇文中,我们继续探索SqlSession,SqlSessionFactory,SqlSessionFactoryBuilder的关系。SqlSession作为MyBatis的核心组件,可以说MyBatis的所有操作都是围绕SqlSession来展开的。对SqlSession理解透彻,才能全面掌握MyBatis。

    1. SqlSession初识

    SqlSession在一开始就介绍过是高级接口,类似于JDBC操作的connection对象,它包装了数据库连接,通过这个接口我们可以实现增删改查,提交/回滚事物,关闭连接,获取代理类等操作。SqlSession是个接口,其默认实现是DefaultSqlSession。SqlSession是线程不安全的,每个线程都会有自己唯一的SqlSession,不同线程间调用同一个SqlSession会出现问题,因此在使用完后需要close掉。


    SqlSession的方法

    2. SqlSession的创建

    SqlSessionFactoryBuilder的build()方法使用建造者模式创建了SqlSessionFactory接口对象,SqlSessionFactory接口的默认实现是DefaultSqlSessionFactory。SqlSessionFactory使用实例工厂模式来创建SqlSession对象。SqlSession,SqlSessionFactory,SqlSessionFactoryBuilder的关系如下(图画得有点丑...):

    类图

    DefaultSqlSessionFactory中openSession是有两种方法一种是openSessionFromDataSource,另一种是openSessionFromConnection。这两种是什么区别呢?从字面意义上将,一种是从数据源中获取SqlSession对象,一种是由已有连接获取SqlSession。SqlSession实际是对数据库连接的一层包装,数据库连接是个珍贵的资源,如果频繁的创建销毁将会影响吞吐量,因此使用数据库连接池化技术就可以复用数据库连接了。因此openSessionFromDataSource会从数据库连接池中获取一个连接,然后包装成一个SqlSession对像。openSessionFromConnection则是直接包装已有的连接并返回SqlSession对像。

    openSessionFromDataSource 主要经历了以下几步:

    1. 从获取configuration中获取Environment对象,Environment包含了数据库配置
    2. 从Environment获取DataSource数据源
    3. 从DataSource数据源中获取Connection连接对象
    4. 从DataSource数据源中获取TransactionFactory事物工厂
    5. 从TransactionFactory中创建事物Transaction对象
    6. 创建Executor对象
    7. 包装configuration和Executor对象成DefaultSqlSession对象
    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);
          final Executor executor = configuration.newExecutor(tx, execType);
          return new DefaultSqlSession(configuration, executor, autoCommit);
        } 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();
        }
      }
    
      private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
        try {
          boolean autoCommit;
          try {
            autoCommit = connection.getAutoCommit();
          } catch (SQLException e) {
            // Failover to true, as most poor drivers
            // or databases won't support transactions
            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);
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    

    3. SqlSession的使用

    SqlSession 获取成功后,我们就可以使用其中的方法了,比如直接使用SqlSession发送sql语句,或者通过mapper映射文件的方式来使用,在上两篇文章中我们都是通过mapper映射文件来使用的,接下来就介绍第一种,直接使用SqlSession发送sql语句。

    public static void main(String[] args){
            try {
                // 1. 读取配置
                InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
                // 2. 获取SqlSessionFactory对象
                SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
                // 3. 获取SqlSession对象
                SqlSession sqlSession = sqlSessionFactory.openSession();
                // 4. 执行sql
                TTestUser user = sqlSession.selectOne("com.example.demo.dao.TTestUserMapper.selectByPrimaryKey", 13L);
                log.info("user = [{}]", JSONUtil.toJsonStr(user));
                // 5. 关闭连接
                sqlSession.close();
                inputStream.close();
            } catch (Exception e){
                log.error("errMsg = [{}]", e.getMessage(), e);
            }
    
        }
    

    其中com.example.demo.dao.TTestUserMapper.selectByPrimaryKey指定了TTestUserMapper中selectByPrimaryKey这个方法,在对应的mapper/TTestUserMapper.xml我们定义了id一致的sql语句

      <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
        select 
        <include refid="Base_Column_List" />
        from t_test_user
        where id = #{id,jdbcType=BIGINT}
      </select>
    

    Mybatis会在一开始加载的时候将每个标签中的sql语句包装成MappedStatement对象,并以类全路径名+方法名为key,MappedStatement为value缓存在内存中。在执行对应的方法时,就会根据这个唯一路径找到TTestUserMapper.xml这条sql语句并且执行返回结果。

    4. SqlSession的执行原理

    4. 1 SqlSession的selectOne的执行原理

    SqlSession的selectOne代码如下,其实是调用selectList()方法获取第一条数据的。其中参数statement就是statement的id,parameter就是参数。

    public <T> T selectOne(String statement, Object parameter) {
            List<T> list = this.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;
            }
        }
    

    RowBounds 对象是分页对象,主要拼接sql中的start,limit条件。并且可以看到两个重要步骤:

    1. 从configuration的成员变量mappedStatements中获取MappedStatement对象。mappedStatements是Map<String, MappedStatement>类型的缓存结构,其中key就是mapper接口全类名+方法名,MappedStatement就是对标签中配置的sql一个包装
    2. 使用executor成员变量来执行查询并且指定结果处理器,并且返回结果。Executor也是mybatis的一个重要的组件。sql的执行都是由Executor对象来操作的。
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
            List var5;
            try {
                MappedStatement ms = this.configuration.getMappedStatement(statement);
                var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
            } catch (Exception var9) {
                throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var9, var9);
            } finally {
                ErrorContext.instance().reset();
            }
    
            return var5;
        }
    

    MappedStatement对象的具体内容和Executor对象的类型,我们将在其它文章中详述。

    4. 2 SqlSession的通过mapper对象使用的执行原理

    在启动流程那篇文章中,我们大致了解了sqlSession.getMapper返回的其实是个代理类MapperProxy,然后调mapper接口的方法其实都是调用MapperProxy的invoke方法,进而调用MapperMethod的execute方法。

    public static void main(String[] args) {
           try {
               // 1. 读取配置
               InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
               // 2. 创建SqlSessionFactory工厂
               SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
               // 3. 获取sqlSession
               SqlSession sqlSession = sqlSessionFactory.openSession();
               // 4. 获取Mapper
               TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
               // 5. 执行接口方法
               TTestUser userInfo = userMapper.selectByPrimaryKey(16L);
               System.out.println("userInfo = " + JSONUtil.toJsonStr(userInfo));
               // 6. 提交事物
               sqlSession.commit();
               // 7. 关闭资源
               sqlSession.close();
               inputStream.close();
           } catch (Exception e){
               log.error(e.getMessage(), e);
           }
       }
    

    MapperMethod的execute方法中使用命令模式进行增删改查操作,其实也是调用了sqlSession的增删改查方法。

    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        switch (command.getType()) {
          case INSERT: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.insert(command.getName(), param));
            break;
          }
          case UPDATE: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.update(command.getName(), param));
            break;
          }
          case DELETE: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.delete(command.getName(), param));
            break;
          }
          case SELECT:
            if (method.returnsVoid() && method.hasResultHandler()) {
              executeWithResultHandler(sqlSession, args);
              result = null;
            } else if (method.returnsMany()) {
              result = executeForMany(sqlSession, args);
            } else if (method.returnsMap()) {
              result = executeForMap(sqlSession, args);
            } else if (method.returnsCursor()) {
              result = executeForCursor(sqlSession, args);
            } else {
              Object param = method.convertArgsToSqlCommandParam(args);
              result = sqlSession.selectOne(command.getName(), param);
              if (method.returnsOptional()
                  && (result == null || !method.getReturnType().equals(result.getClass()))) {
                result = Optional.ofNullable(result);
              }
            }
            break;
          case FLUSH:
            result = sqlSession.flushStatements();
            break;
          default:
            throw new BindingException("Unknown execution method for: " + command.getName());
        }
        if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
          throw new BindingException("Mapper method '" + command.getName()
              + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
        }
        return result;
      }
    

    总结

    在这篇文章中我们详细介绍了SqlSession的作用,创建过程,使用方法,以及执行原理等,对SqlSession已经有了比较全面的了解。其中涉及到的Executor对象,MappedStatement对象,ResultHandler我们将在其它文章中讲解。欢迎在评论区中讨论指正,一起进步。

    相关文章

      网友评论

        本文标题:MyBatis原理系列(三)-手把手带你了解SqlSession

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