Mybatis源码解析--SqlSession

Mybatis源码解析--SqlSession

SqlSession介绍

Mybatis框架的主要目的就是简化JDBC操作数据库的繁琐流程(忘了JDBC流程的可以参考这篇),只需要提供sql语句和相关参数即可,不用再对参数进行手动设置,以及手动遍历结果集封装目标对象,不用担心资源的释放等等。而SqlSession这个类是整个Mybatis框架提供用于操作数据库的入口,提供了相关API,开发人员只需要了解该类提供方法就可以对数据库进行操作。下图展示了SqlSession在Mybatis中所起的作用。

Mybatis通过对外暴露SqlSession这个类,使得开发人员不需要了解Mybatis内部具体实现的情况下就能快速操作数据库(门面模式的思想),本篇主要按照官网的Demo来学习SqlSession的创建过程、相关API的学习以及提供StatementId和Mapper代理对象两种接口调用方式的对比。

SqlSession的创建

构建SqlSessionFactory

我们模拟官网的示例写一个demo

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

通过Mybatis提供的Resources工具类将配置文件转换成流对象交给SqlSessionFactoryBuilder,并通过它构建SqlSessionFactory工厂对象。这里使用到了建造者模式和工厂模式,那我们研究一下SqlSessionFactoryBuilder是如何构建SqlSessionFactory。

SqlSessionFactoryBuilder类

SqlSessionFactoryBuilder提供了两大类重载的方法:1) 通过Reader构建 2) 通过InputStream构建

通过Reader构建:通过入参构建一个XMLConfigBuilder对象,该对象的目的就是解析Mybatis配置文件,将配置文件中的静态配置通过解析封装到Configuration这个对象里面。

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
      //对Mybatis配置文件的解析细节全部由该类封装
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      return build(parser.parse());
  }

通过InputStream构建:和上面的处理方式一样,只不过使用InputStream流对象;两者的区别这里就不再提了。

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
      //对Mybatis配置文件的解析细节全部由该类封装
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
  }

最终都通过调用公共方法build(Configuration)创建SqlSessionFactory工厂对象

public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
}

通过上面的源码跟踪,我们看到了它的“庐山真面目”,原来是通过Configuration构建了一个DefaultSqlSessionFactory对象(SqlSessionFactory接口的默认实现类)

创建SqlSessionFactory时序图

SqlSessionFactory提供的API

public interface SqlSessionFactory {
​
  SqlSession openSession();
​
  SqlSession openSession(boolean autoCommit);
​
  SqlSession openSession(Connection connection);
​
  SqlSession openSession(TransactionIsolationLevel level);
​
  SqlSession openSession(ExecutorType execType);
​
  SqlSession openSession(ExecutorType execType, boolean autoCommit);
​
  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
​
  SqlSession openSession(ExecutorType execType, Connection connection);
}

SqlSessionFactory提供了大量创建SqlSession的API,上面已经知道Mybatis实际创建的是DefaultSqlSessionFactory对象,通过查看该类的具体实现,我们可以将上面的API分为两大类:1) 通过DataSource获取  2)通过Connetion获取

通过DataSource获取

这块请对照DefaultSqlSessionFactory的源码一起查看,可以得出下图,左边这些实现方法最终都调用右边这个方法

openSessionFromDataSource方法具体实现


private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
      final Environment environment = configuration.getEnvironment();
      // 事务相关
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 创建Executor  
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
  }

看到这块可能会有点懵,EnviromentTransactionFactoryExecutor这3个对象是干什么的,好像只有TransactionFactory似乎认识,应该和事务有关。其实我们可以先只关心返回值和创建返回值的入参。下面对这3个入参进行分析:

  • configuration:该对象是解析Mybatis配置文件后对应的配置管理类,在创建DefaultSqlSessionFactory时通过构造传入作为其一个属性

  • executor:创建的执行器,sqlSession会将所有的操作最终交给executor去完成,SqlSession仅仅是一个外壳。对外统一暴露,隐藏了内部具体实现。

  • autoCommit:是否自动提交事务

通过DataSource获取

private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
    try {
      boolean autoCommit;
      try {
        // 从connection获取是否自动提交信息  
        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);
      // 通过connection对象创建事务对象  
      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();
    }
  }

这个方法和上面的类似,主要区别在于是否自动提交和事务的创建是通过Connection完成的

SqlSession的内部原理

上面我们已经知道了SqlSession的创建流程并得到了SqlSession对象,该对象是Mybatis对数据库操作的门面类,所有操作都是通过它完成。内部提供了事务相关、资源的关闭、增删改查等API。其中事务我们不在这讨论,只关心增删改查的操作,可以对其分成两大类:StatementId和Mapper接口。

第一类:statementId方式相关接口

  • T selectOne(String statement, Object parameter)

  • List<E> selectList(String statement, Object parameter, RowBounds rowBounds)

  • Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds)

  • Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds)

  • void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler)

  • int insert(String statement, Object parameter)

  • int update(String statement, Object parameter)

  • int delete(String statement, Object parameter)

通过对SqlSession提供API分类后,我们发现有几个频繁出现的参数String statementRowBounds rowBoundsObject parameter,我们先来看一下这些入参信息。

String statement

该参数用来标识一个SQL语句,一般我们会在Mapper的xml配置文件或者是在Mapper接口方法上使用注解定义SQL,这两种方式定义的SQL最终都会被Mybatis框架解析封装到某个类(MappedStatement)中交给”大总管“Configuration进行管理,并且每个SQL都会有一个唯一标识,所以这个statement参数就是那个唯一标识用来获取对应SQL的作用。

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.keminapera.mybatis.mapper.StudentMapper">
  <insert id="insertStudent" parameterType="com.keminapera.pojo.Student">
    insert into student (n_id, c_name, n_age, c_school) values (#{id}, #{name}, #{age}, #{school})
  </insert>
</mapper>

xml方式通过解析生成的唯一标识是由<mapper>标签的namespace属性和某个<insert><update><select><delete>标签的id属性组合而成,一般namespace对应Mapper接口的全限定类名,id是对应Mapper接口内的方法名.(虽然StatementId方式可以随意指定,只要唯一即可)

public interface StudentMapper {
  @Insert({"INSERT INTO student",
    "(n_id, c_name, n_age, c_school)",
    "VALUES",
    "(#{id}, #{name}, #{age}, #{school})"})
  int insertStudent(Student student);
}

注解方式默认会将Mapper接口的全限定名+对应方法名作为唯一标识

RowBounds rowBounds

该类是Mybatis框架提供用来分页的,注意该类只能进行逻辑上分页,本质还是将数据库所有结果加载到内存,然后再进行截取的方式,推荐使用流式查询分页插件

public class RowBounds {

  public static final int NO_ROW_OFFSET = 0;
  public static final int NO_ROW_LIMIT = Integer.MAX_VALUE;
  public static final RowBounds DEFAULT = new RowBounds();

  private final int offset;
  private final int limit;

  public RowBounds() {
    this.offset = NO_ROW_OFFSET;
    this.limit = NO_ROW_LIMIT;
  }

  public RowBounds(int offset, int limit) {
    this.offset = offset;
    this.limit = limit;
  }
}

内部两个属性,offset指定从哪个下标开始读取,limit指定读取多少条数据

Object parameter

该参数是用于给sql进行传参.一般查询会有查询条件,添加需要指定新对象,修改需要更新后的对象,删除需要指定记录的标识,这些数据是要替换掉前面sql中的占位符,最终才能交给DBMS执行.

上面已经理清了这些入参的含义,下面我们主要看一下这些方法的内部具体实现

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  }

selectMap方法

public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {
    final List<? extends V> list = selectList(statement, parameter, rowBounds);
    final DefaultMapResultHandler<K, V> mapResultHandler = new DefaultMapResultHandler<>(mapKey,
            configuration.getObjectFactory(), configuration.getObjectWrapperFactory(), configuration.getReflectorFactory());
    final DefaultResultContext<V> context = new DefaultResultContext<>();
    for (V o : list) {
      context.nextResultObject(o);
      mapResultHandler.handleResult(context);
    }
    return mapResultHandler.getMappedResults();
  }

selectCursor方法

public <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds) {
      MappedStatement ms = configuration.getMappedStatement(statement);
      Cursor<T> cursor = executor.queryCursor(ms, wrapCollection(parameter), rowBounds);
      registerCursor(cursor);
      return cursor;
  }

insert方法

public int insert(String statement, Object parameter) {
    return update(statement, parameter);
  }

注意: 这块insert操作调用update操作

public int delete(String statement, Object parameter) {
  return update(statement, parameter);
}

注意: 这块delete操作调用update操作

public int update(String statement, Object parameter) {
  dirty = true;
  MappedStatement ms = configuration.getMappedStatement(statement);
  return executor.update(ms, wrapCollection(parameter));
}

和上面select操作一样,都是先获取MappedStatement,最后转交给executor去完成

第二类:mapper代理对象方式相关接口(推荐)

  • T getMapper(Class<T> type)

该方法会返回一个Mapper接口的代理对象,代理对象的生成过程请参考这篇博客

两种方式的使用示例

StatementId方式:通过一串字符

  @Test
  public void insertStudentWithStatementId() {
    try(SqlSession sqlSession = sessionFactory.openSession(true)){
      int count = sqlSession.insert("com.keminapera.mybatis.mapper.StudentMapper.insertStudent", getStudent());
    }
  }

mapper接口方式:通过指定的mapper接口类对象

@Test
  public void insertStudentWithMapper() {
    try(SqlSession sqlSession = sessionFactory.openSession(true)){
      StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
      studentMapper.insertStudent(getStudent());
    }
  }

两种方式的比较

通过上面示例的比较,很明显使用mapper代理的方式更面向对象,更容易理解,只需要从SqlSession中获取对应的Mapper对象,然后调用内部方法即可。相比第一种,需要传入一个字符串字面量,而且要和SQL标识保持一致,容易出错

作用域和生命周期

SqlSessionFactoryBuilder:通过上面源码的分析,我们知道在该类中对Mybatis的配置文件进行了解析,最后得到一个configuration对象,通过configuration对象创建了一个DefaultSqlSessionFactory返回。配置文件的解析伴随着io读取操作,以及里面标签的解析,所以很重;再说,一般我们直接一次性将Mybatis配置好的,要是没有修改,没有必要进行多次解析。官网推荐最好是方法的局部变量,不保存引用。

SqlSessionFactory:该对象可以反复使用,主要作用是创建sqlsession对象,该对象作用域是整个应用范围

SqlSession:该对象线程不安全,主要作用就是执行数据库相关操作

小结

SqlSession是整个Mybatis对外暴露的门面类,通过它进行数据库操作,为了简化创建SqlSession的过程框架提供了几个辅助类,整体之间的关系如下:

建造好SqlSession对象后发现该对象内部什么都没有干,最终全部转交给Executor对象完成,调用关系图如下:

涉及的设计模式:门面模式、建造者模式、工厂模式

注意事项:SqlSession对象不是线程安全的,所以每次操作都要获取一个新的SqlSession对象

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值