Mybatis源码学习记录(SqlSession篇)

前言

本文衔接上文(Mapper接口篇),继续分析Mapper相关的源码

进入源码

上文中我们分析到了MapperMethod的execute方法

MapperMethod.java

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);
        }
        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;
  }

根据上文示例,我们的代码会执行到executeForMany(sqlSession, args);这个分支,下面分析这个方法做了什么工作

private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    // 1. 首先将传入的Object[] args转为上文分析的合适的参数
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
      // 1.1. 该调用方法有行限制参数RowBounds
      RowBounds rowBounds = method.extractRowBounds(args);
      result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
    } else {
      // 1.2. 无行限制参数RowBounds
      result = sqlSession.<E>selectList(command.getName(), param);
    }
    // 对于返回值为Collections和Array的支持,因为SqlSession接口定义的返回值全部都是List<T>,如果我们的Mapper接口中定义了Array类型的返回值,就需要进行转换
    // issue #510 Collections & arrays support
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
      if (method.getReturnType().isArray()) {
        return convertToArray(result);
      } else {
        return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
      }
    }
    return result;
  }
  1. method.convertArgsToSqlCommandParam(args);

这里在真正调用SqlSession的相关方法前会执行MethodSignature的convertArgsToSqlCommandParam(Object[] args)方法

public Object convertArgsToSqlCommandParam(Object[] args) {
      return paramNameResolver.getNamedParams(args);
}

就是调用了内部持有的paramNameResolver对象的getNamedParams方法,将从代理Mapper对象方法调用拿到的Object[] args参数转为合适的名称参数对象以便接下来的sqlSession相关方法执行使用

接下来,我们将目光转向Mybatis中大名鼎鼎的Sqlsession

SqlSession.java

Mybatis暴露的顶层Api接口,用于操作Sql的执行,以屏蔽掉底层JDBC操作数据库的繁琐,让我们可以直接调用其申明的各种方便的方法如,查询/新增/更新/删除/提交事务/回滚事务/获取Mapper代理对象等

下面看下SqlSession的接口定义

public interface SqlSession extends Closeable {

    // 查询单个结果
    <T> T selectOne(String statement);
    <T> T selectOne(String statement, Object parameter);
    
    // 查询List<E>结果,可指定RowBounds参数
    <E> List<E> selectList(String statement);
    <E> List<E> selectList(String statement, Object parameter);
    <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);
    
    // 返回Map的查询方式
    <K, V> Map<K, V> selectMap(String statement, String mapKey);
    <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey);
    <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds);
    
    // 返回懒处理迭代器Cursor游标的查询,注意这种方式的查询非常适合与数百万数据项的查询(不会直接加载进内存中)
    <T> Cursor<T> selectCursor(String statement);
    <T> Cursor<T> selectCursor(String statement, Object parameter);
    <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds);
    
    // 指定resultHandler参数的查询
    void select(String statement, Object parameter, ResultHandler handler);
    void select(String statement, ResultHandler handler);
    void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler);
    
    // insert相关
    int insert(String statement);
    int insert(String statement, Object parameter);
    
    // update相关
    int update(String statement);
    int update(String statement, Object parameter);
    
    // delete相关
    int delete(String statement);
    int delete(String statement, Object parameter);
    
    // 事务相关,force参数代表是否强制提交/回滚事务
    void commit();
    void commit(boolean force);
    void rollback();
    void rollback(boolean force);
    
    // 检索当前Mybatis配置对象
    Configuration getConfiguration();

    // 检索一个绑定到当前SqlSession对象的type类型的Mapper代理对象
    <T> T getMapper(Class<T> type);

    // 检错内部持有的数据库连接对象
    Connection getConnection();
}

接口终归是接口,活还是得有人来干,下面看看SqlSession的实现类有哪些

DefaultSqlSession.java

Mybatis 默认的SqlSession实现类,实现了接口中定义的所有功能

接口定义

public class DefaultSqlSession implements SqlSession {...}

可以看到这里DefaultSqlSession仅实现了SqlSession接口

内部属性定义

  // 重要属性,持有Mybatis Configuration实例
  private final Configuration configuration;
  // 持有执行器Executor,后面会详细分析
  private final Executor executor;

  // 是否自动提交事务
  private final boolean autoCommit;
  // 是否已过期,执行update操作就会置为true
  private boolean dirty;
  // 当查询数以百万计的数据时用到
  private List<Cursor<?>> cursorList;

每个SqlSession实例内部都持有一个Mybatis的配置实例以及一个执行器Executor实例,且在其构造函数中就需要初始化这两个属性了

构造函数

public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
    this.configuration = configuration;
    this.executor = executor;
    this.dirty = false;
    this.autoCommit = autoCommit;
  }

  public DefaultSqlSession(Configuration configuration, Executor executor) {
    this(configuration, executor, false);
  }

接下来我们继续上面的查询例子sqlSession.<E>selectList(command.getName(), param);

这里的command.getName的值是"com.wilson.mapper.UserOrderMapper.completedOrdersByUserPhone"这个字符串,而param则是一个Map对象,其值为{{"phone", "15800000000"}, {"param1", "15800000000"}}

ok,接下来我们直接看DefaultSqlSession的selectList方法做了什么

  @Override
  public <E> List<E> selectList(String statement, Object parameter) {
    // 内部调用重载的带有RowBounds参数的方法,传入默认的RowBounds,即offset为0,limit为Integer的最大值
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }

  @Override
  public <E> List<E> selectList(String statement, Object 
  parameter, RowBounds rowBounds) {
    try {
      // 1.  
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 2. 
      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();
    }
  }

重点:

  1. 首先从configuration实例中获取到MappedStatement实例
  2. 调用执行器executorquery方法,并传入1. 中得到的MappedStatement实例作为参数

以上两步我们分开分析,首先看第一步如何从配置对象configuration中获取到MappedStatement实例

根据statementId获取MappedStatement实例

// Configuration.java
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");

public MappedStatement getMappedStatement(String id) {
  // 根据传入的statementId获取
  return this.getMappedStatement(id, true);
}

public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
  if (validateIncompleteStatements) {
    buildAllStatements();
  }
  // 就是从mappedStatements取出已经存入的MappedStatement实例
  return mappedStatements.get(id);
}

总结

至此本文内容分析结束,SqlSession真正的SQL查询交给了Executor执行器,前文不是说过SqlSession是Mybatis提供的顶层API嘛,通过SqlSession就可以操作数据库的各种SQL操作了,这里怎么又出来一个执行器呢?实际上,Mybatis的SqlSession非常聪明,将各种SQL操作的脏活、累活委托给了Executor执行器干,自己则坐享其成...

下文我们将分析Mybatis的Executor篇

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357

推荐阅读更多精彩内容