MyBatis原理——用户交互接口SqlSession

概述

正如其名,SqlSession代表着一次SQL会话,是MyBatis提供给用户的顶层接口,用户通过它来访问数据库。源码对该类的注释如下:

/**
 * The primary Java interface for working with MyBatis.
 * Through this interface you can execute commands, get mappers and manage transactions.
 *
 * @author Clinton Begin
 */
public interface SqlSession extends Closeable {

正如注释所说,SqlSession主要有3个功能:

  1. 执行命令:这正是SqlSession最主要的用处:执行SQL语句。
  2. 获取Mapper:这是SqlSession提供的另一种交互方式,也就是现阶段最常用的通过Mapper接口来执行命令的方式。随后说。
  3. 管理事务。

在这里插入图片描述

接下来将从源码出发,分析SqlSession是如何实现这3个功能的。基于MyBatis 3.5.3版本。

SqlSession提供的交互方式

现阶段MyBatis中,SqlSession主要提供了两种交互方式:

1. 基于Statement ID

在这里插入图片描述

这是MyBatis最传统的交互方式,首先来解释下这个Statement ID是什么。

这个Statement ID本质是一个字符串,相当于是MyBatis给SQL语句定义的唯一主键,由mapper的命名空间namespace+SQL中的id组成,比如对于如下StudentMapper.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="cn.novalue.project.test.mapper.StudentMapper">
    <select id="getOne" parameterType="int" resultType="Student">
        select * from `student` where id = #{id}
    </select>
</mapper>

这条SQL的Statement ID就是cn.novalue.project.test.mapper.StudentMapper.getOne

SqlSession提供了增删改查很多重载方法,来满足用户的种种需求。但其实这些方法都是类似的:都需要提供一个String类型的Statement ID,以及SQL参数(若有参数的话)。所以可以对它有个初步的认识:SqlSession正是通过Statement ID定位到了xml文件中的sql标签(若有参数则设置参数),然后执行sql的。

2. 基于Mapper接口

基于Statement ID虽然简单易懂,但是其实际操作却并不优雅,想象一下每次要写一长串字符串。。。它不符合面向对象语言的概念和面向接口编程的编程习惯。由于面向接口的编程是面向对象的大趋势,所以这种交互方式应运而生。但其实这种方式也是依托于第1种的,只不过MyBatis把Statement ID相关的操作封装了起来。

习惯上,一般一个xml配置文件,对应一个真实数据库表,而这个配置文件,又是以<mapper>为root结点(如上边的StudentMapper.xml)。所以为了实现基于Mapper接口交互,MyBatis 将配置文件中的每一个<mapper> 节点抽象为一个 Mapper 接口,而这个接口中声明的方法和跟<mapper> 节点中的<select|update|delete|insert>节点项一一对应,即<select|update|delete|insert>节点的id值为Mapper 接口中的方法名称,parameterType 值表示Mapper 对应方法的入参类型,而resultMap 值则对应了Mapper 接口表示的返回值类型或者返回结果集的元素类型。
在这里插入图片描述
根据MyBatis 的配置规范配置好后,通过SqlSession.getMapper(XXXMapper.class)方法,MyBatis 会根据相应的接口声明的方法信息,通过动态代理机制生成一个Mapper 实例,我们使用Mapper 接口的某一个方法时,MyBatis 会根据这个方法的方法名和参数类型,确定Statement ID,底层还是通过SqlSession.select("statementId",parameterObject);或者SqlSession.update("statementId",parameterObject);等等来实现对数据库的操作。

正是有了这种对应关系,所以现在我们可以对Statement ID有一个全新的认识:Mapper全限定类名+方法名,因为xml文件中的namespace是Mapper接口的全限定类名,sql标签的id是Mapper接口中的方法名。这也就是为什么Mybatis的Mapper接口中不允许有重载方法,因为那样Statement ID就重复了,而它是作为唯一主键存在的。

SqlSession执行分析

首先看下SqlSession的继承关系(SqlSessionTemplate其实是org.mybatis.spring包下的):
在这里插入图片描述

Mybatis对SqlSession功能上的实现,其实仅有一个子类:DefaultSqlSession(虽然还有个SqlSessionManager以及针对Spring环境下的SqlSessionTemplate,但它们其实还是委托DefaultSqlSession的,这两个子类后边再说)。所以只要来看这个类即可:

public class DefaultSqlSession implements SqlSession {

  private final Configuration configuration;
  private final Executor executor;

DefaultSqlSession主要有两个字段,这两个字段又涉及MyBatis两个核心组件:

  1. Configuration:MyBatis的大仓库,保存了几乎所有MyBatis要用的东西,人手一份。
  2. Executor:MyBatis真正的执行器,SqlSession几乎所有功能,其实就是委托它实现的。

这里以select操作为例,分析下SqlSession的源码。

DefaultSqlSession的所有select相关重载方法,最后都会来到public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds)。这个方法的源码如下:

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      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();
    }
  }

异常简单,仅仅是从大仓库Configuration中获取到一个MappedStatement,然后委托Executor去执行。这里简单说下:MappedStatement是MyBatis对SQL语句的封装,对应于xml文件中的一个<select|update|delete|insert>节点,或者说Mapper接口中的一个方法。

其它的数据库操作方法类似,几乎都是直接委托Executor来执行,所以DefaultSqlSession的源码还是很好理解的。

创建SqlSession的方式

MyBatis到处使用工厂模式创建对象,SqlSession也是如此。与此对应的顶层接口是SqlSessionFactory,并且针对DefaultSqlSession,也有其对应实现:DefaultSqlSessionFactory。该类实现了工厂接口中的众多重载方法,但还是按照惯例,最终落实到一个或几个具体方法上。这里就是openSessionFromDataSourceopenSessionFromConnection,也就是从数据源和数据库连接中创建。源码如下:

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      // Configuration中取Environment
      final Environment environment = configuration.getEnvironment();
      // Environment中取配置的事务工厂
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      // 新建事务
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 新建执行器
      final Executor executor = configuration.newExecutor(tx, execType);
      // 创建DefaultSqlSession
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      // 处理异常
      // ...
    } finally {
    }
  }
    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) {
    } finally {
    }
  }

无非还是从大仓库Configuration中取东西,然后组合、配置,最后返回DefaultSqlSession,还是比较简单的。

而工厂又进一步是由SqlSessionFactoryBuilder构建生成的,其读入MyBatis的配置文件(也即mybatis-config.xml),并返回一个DefaultSqlSessionFactory

由于DefaultSqlSession的具体实现都委托给Executor,而Executor内部的一级缓存直接是HashMap实现的,所以正如DefaultSqlSession源码注释所说:它不是线程安全的。所以SqlSessionManager应运而生。

SqlSessionManager

SqlSessionManager是为了解决DefaultSqlSession线程不安全问题而来的。首先来看下类定义:

public class SqlSessionManager implements SqlSessionFactory, SqlSession {

  private final SqlSessionFactory sqlSessionFactory;
  private final SqlSession sqlSessionProxy;

  private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();
  // 私有构造方法
  private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {
    this.sqlSessionFactory = sqlSessionFactory;
    this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[]{SqlSession.class},
        new SqlSessionInterceptor());
  }
  // 创建对象的工厂方法
  public static SqlSessionManager newInstance(Reader reader) {
    return new SqlSessionManager(new SqlSessionFactoryBuilder().build(reader, null, null));
  }
  // 自有方法
  public void startManagedSession() {
    this.localSqlSession.set(openSession());
  }
  // SqlSessionFactory接口的方法
  @Override
  public SqlSession openSession() {
    return sqlSessionFactory.openSession();
  }
  // SqlSession接口定义的方法
  @Override
  public <T> T selectOne(String statement) {
    return sqlSessionProxy.selectOne(statement);
  }

SqlSessionManager中方法很多,但是大都是重载的,这里仅挑几个代表,其它类似。这里来总结下它的几个特点:

  1. 同时实现了SqlSessionFactorySqlSession接口(自己生自己?)
  2. 分别持有SqlSessionFactorySqlSession类型的属性字段,且对应接口的方法全是直接委托这两个字段执行。
  3. 持有一个 ThreadLocal<SqlSession> 类型的字段localSqlSession,也就是说会保存一个线程绑定的SqlSession
  4. 构造方法是私有的,仅对外提供静态的工厂方法。且入参也只是简单地新建一个DefaultSqlSessionFactory而已(new SqlSessionFactoryBuilder().build(reader, null, null)方法最终新建一个DefaultSqlSessionFactory)。
  5. 对字段sqlSessionProxy的赋值应用了JDK的动态代理,涉及类SqlSessionInterceptor
  6. 除了实现这两个接口的方法,其自身还有一些重载方法:startManagedSession,一旦调用了这个方法,就给自身的localSqlSession中放入一个SqlSession(其实也就是DefaultSqlSession)。

综上可以看到,SqlSessionManager也基本都是委托自身的两个字段去做事,sqlSessionFactory就是简单的DefaultSqlSessionFactory,没什么说的,只不过sqlSessionProxy是以动态代理的方式创建的,那么对它的调用,最终都会走到对应的InvocationHandler#invoke方法中,所以关于线程安全的秘密只能在localSqlSession和类SqlSessionInterceptor中,下面就来看下这个内部类定义:

  private class SqlSessionInterceptor implements InvocationHandler {
    public SqlSessionInterceptor() {
        // Prevent Synthetic Access
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // 从ThreadLocal中获取SqlSession
      final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();
      // 如果有线程绑定的SqlSession,就用它来执行方法
      if (sqlSession != null) {
        try {
          return method.invoke(sqlSession, args);
        } catch (Throwable t) {
          throw ExceptionUtil.unwrapThrowable(t);
        }
        // 如果没有线程绑定的SqlSession
      } else {
      	// 则从工厂中新建一个
        try (SqlSession autoSqlSession = openSession()) {
          try {
            final Object result = method.invoke(autoSqlSession, args);
            autoSqlSession.commit();
            return result;
          } catch (Throwable t) {
            autoSqlSession.rollback();
            throw ExceptionUtil.unwrapThrowable(t);
          }
        }
      }
    }
  }

这个类的内容也算简单明了。当sqlSessionProxy在执行有关SqlSession接口方法时就会来到这里,首先从自身内部字段localSqlSession中尝试获取SqlSession,如果有,就用线程绑定的;如果没有,再从工厂新建。实际干活的还是DefaultSqlSession。这也就是为什么它能够实现线程安全的SqlSession

而也只有调用了startManagedSession的重载方法,才会往localSqlSession中放SqlSession。所以startManagedSession方法相当于SqlSessionManager的一个开关:

  • 若没有调用该重载方法,它实际上就是普通的DefaultSqlSession,每次用 每次新建。
  • 调用该重载方法后,SqlSessionManager就会给当前线程绑定一个DefaultSqlSession,以后都使用线程绑定的这个,从而实现线程安全。

SqlSessionTemplate

其实知道了SqlSessionManager是如何实现线程安全的,那么也就知道了在Spring环境下的SqlSessionTemplate的实现原理:同样的动态代理,给线程绑定一个SqlSession。只不过在Spring框架下,有一个事务同步管理器TransactionSynchronizationManager,专门保存当前线程相关的资源,这里由它负责保存线程绑定的SqlSession而已。

参考文章:《深入理解mybatis原理》 MyBatis的架构设计以及实例分析

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值