Mybatis 源码(五)Mybatis 中的数据读写

数据读写的本质

不管是哪种ORM框架,数据读写其本质都是对JDBC的封装,其目的主要都是简化JDBC的开发流程,进而让开发人员更关注业务。下面是JDBC的核心流程:

  1. 注册 JDBC 驱动(Class.forName("XXX");)
  2. 打开连接(DriverManager.getConnection("url","name","password"))
  3. 根据连接,创建 Statement(conn.prepareStatement(sql))
  4. 设置参数(stmt.setString(1, "wyf");)
  5. 执行查询(stmt.executeQuery();)
  6. 处理结果,结果集映射(resultSet.next())
  7. 关闭资源(finally)

Mybatis也是在对JDBC进行封装,它将注册驱动打开连接交给了数据库连接池来负责,Mybatis支持第三方数据库连接池,也可以使用自带的数据库连接池;创建 Statement执行查询交给了StatementHandler来负责;设置参数交给ParameterHandler来负责;最后两步交给了ResultSetHandler来负责。

Executor内部运作过程

测试方法org.apache.ibatis.binding.BindingTest#shouldFindThreeSpecificPosts

下面是一个简单的数据读写过程,我们在宏观上先来了解一下每一个组件在整个数据读写上的作用:

Executor内部运作过程.png

Mybatis的数据读写主要是通Excuter来协调StatementHandler、ParameterHandler和ResultSetHandler三个组件来实现的:

  • StatementHandler:它的作用是使用数据库的Statement或PrepareStatement执行操作,启承上启下作用;
  • ParameterHandler:对预编译的SQL语句进行参数设置,SQL语句中的的占位符“?”都对应BoundSql.parameterMappings集合中的一个元素,在该对象中记录了对应的参数名称以及该参数的相关属性
  • ResultSetHandler:对数据库返回的结果集(ResultSet)进行封装,返回用户指定的实体类型;

StatementHandler

StatementHandler类图

StatementHandler.png

RoutingStatementHandler

通过StatementType来创建StatementHandler,使用静态代理模式来完成方法的调用,主要起到路由作用。它是Excutor组件真正实例化的组件。

public class RoutingStatementHandler implements StatementHandler {
  /**
   * 静态代理模式
   */
  private final StatementHandler delegate;

  public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    // 根据{@link org.apache.ibatis.mapping.StatementType} 来创建不同的实现类 (策略模式)
    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }
  }

  @Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    return delegate.prepare(connection, transactionTimeout);
  }
...
}

BaseStatementHandler

所有子类的抽象父类,定义了初始化statement的操作顺序,由子类实现具体的实例化不同的statement(模板模式)。

@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
  ErrorContext.instance().sql(boundSql.getSql());
  Statement statement = null;
  try {
    // 实例化Statement(由子类实现)【模板方法+策略模式】
    statement = instantiateStatement(connection);
    // 设置超时时间
    setStatementTimeout(statement, transactionTimeout);
    // 设置获取数据记录条数
    setFetchSize(statement);
    return statement;
  } catch (SQLException e) {
    closeStatement(statement);
    throw e;
  } catch (Exception e) {
    closeStatement(statement);
    throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
  }
}

protected abstract Statement instantiateStatement(Connection connection) throws SQLException;

instantiateStatement()就是一个模板方法,由子类实现。

SimpleStatementHandler

使用JDBCStatement执行模式,不需要做参数处理,源码如下:

@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
  // 实例化Statement
  if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
    return connection.createStatement();
  } else {
    return connection.createStatement(mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
  }
}

@Override
public void parameterize(Statement statement) {
  // N/A
  // 使用Statement是直接执行sql 所以没有参数
}

PreparedStatementHandler

使用JDBCPreparedStatement预编译执行模式。

@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
  // 实例化PreparedStatement
  String sql = boundSql.getSql();
  if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
    String[] keyColumnNames = mappedStatement.getKeyColumns();
    if (keyColumnNames == null) {
      return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
    } else {
      return connection.prepareStatement(sql, keyColumnNames);
    }
  } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
    return connection.prepareStatement(sql);
  } else {
    return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
  }
}

@Override
public void parameterize(Statement statement) throws SQLException {
  // 参数处理
  parameterHandler.setParameters((PreparedStatement) statement);
}

CallableStatementHandler

使用JDBCCallableStatement执行模式,用来调用存储过程。现在很少用。

ParameterHandler

主要作用是给PreparedStatement设置参数,源码如下:

@Override
public void setParameters(PreparedStatement ps) {
  ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
  // 获取参数映射关系
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings != null) {
    // 循环获取处理参数
    for (int i = 0; i < parameterMappings.size(); i++) {
      // 获取对应索引位的参数
      ParameterMapping parameterMapping = parameterMappings.get(i);
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        // 获取参数名称
        String propertyName = parameterMapping.getProperty();
        // 判断是否是附加参数
        if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
          value = boundSql.getAdditionalParameter(propertyName);
        }
        // 判断是否是没有参数
        else if (parameterObject == null) {
          value = null;
        }
        // 判断参数是否有相应的 TypeHandler
        else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          // 以上都不是,通过反射获取value值
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        // 获取参数的类型处理器
        TypeHandler typeHandler = parameterMapping.getTypeHandler();
        JdbcType jdbcType = parameterMapping.getJdbcType();
        if (value == null && jdbcType == null) {
          jdbcType = configuration.getJdbcTypeForNull();
        }
        try {
          // 根据TypeHandler设置参数
          typeHandler.setParameter(ps, i + 1, value, jdbcType);
        } catch (TypeException | SQLException e) {
          throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
        }
      }
    }
  }
}
  1. 获取参数映射关系
  2. 获取参数名称
  3. 根据参数名称获取参数值
  4. 获取参数的类型处理器
  5. 根据TypeHandler设置参数值

通过上面的流程可以发现,真正设置参数是由TypeHandler来实现的。

TypeHandler

Mybatis基本上提供了我们所需要用到的所有TypeHandler,当然我们也可以自己实现。TypeHandler的主要作用是:

  1. 设置PreparedStatement参数值
  2. 获取查询结果值
public interface TypeHandler<T> {

  /**
   * 给{@link PreparedStatement}设置参数值
   *
   * @param ps        {@link PreparedStatement}
   * @param i         参数的索引位
   * @param parameter 参数值
   * @param jdbcType  参数类型
   * @throws SQLException
   */
  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  /**
   * 根据列名获取结果值
   *
   * @param columnName Colunm name, when configuration <code>useColumnLabel</code> is <code>false</code>
   */
  T getResult(ResultSet rs, String columnName) throws SQLException;

  /**
   * 根据索引位获取结果值
   */
  T getResult(ResultSet rs, int columnIndex) throws SQLException;

  /**
   * 获取存储过程结果值
   */
  T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}

TypeHandler的本质就是对JDBC中stmt.setString(1, "wyf");resultSet.getString("name")的封装,JDBC完整代码可以查看JDBC 面试要点

ResultSetHandler

ResultSetHandler主要作用是:对数据库返回的结果集(ResultSet)进行封装,通过通过ResultMap配置和反射完成自动映射,返回用户指定的实体类型;核心思路如下:

  1. 根据RowBounds做分页处理
  2. 根据ResultMap配置的返回值类型和constructor配置信息实例化目标类
  3. 根据ResultMap配置的映射关系,获取到TypeHandler,进而从ResultSet中获取值
  4. 根据ResultMap配置的映射关系,获取到目标类的属性名称,然后通过反射给目标类赋值

源码太多了,这里就不发了,有兴趣就在下面的源码上看注释吧,下面的流程图会画出方法的调用栈。

Mybatis自带的RowBounds分页是逻辑分页,数据量大了有可能会内存溢出,所以不建议使用Mybatis默认分页。

数据读取流程图

mybatis数据读写流程1.png
mybatis数据读写流程2.png

总结

Mybatis的整个数据读取流程其实就是对JDBC的一个标准实现。

Mybatis 源码中文注释

https://github.com/xiaolyuh/mybatis

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