MyBatis四大组件Executor、StatementHandler、ParameterHandler、ResultSetHandler 详解(2)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: MyBatis四大组件Executor、StatementHandler、ParameterHandler、ResultSetHandler 详解

四、ParameterHandler

1. ParameterHandler 接口方法

ParameterHandler 接口相对比较简单,只有两个方法

public interface ParameterHandler {
  Object getParameterObject();
  void setParameters(PreparedStatement ps) throws SQLException;
}

所以看得出来,ParameterHandler 其实就是两个功能,一个是提供入参对象,另一个就是把入参给sql注入进去


2. 实现类核心方法

ParameterHandler 接口只有一个默认实现类 DefaultParameterHandler

13c4210f42cb4c67a5633f30f618161e.png

我们来看一下,要构建个参数处理器需要些什么内容,来看一下该类的构造方法:

  public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    // 由 MyBatis解析xml得出的某个方法对应的sql的信息,包含sql语句 ,参数和结果集的映射
    this.mappedStatement = mappedStatement;
    // MyBatis总配置对象,包含mybatis的所有设置信息
    this.configuration = mappedStatement.getConfiguration();
    // 类型处理器注册表, 类型处理器是用来处理Java对象和字段类型之间映射,负责将Java类型和JDBC类型相互转换
    this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
    // 方法的入参拼出的对象,实际内部为HashMap
    this.parameterObject = parameterObject;
    // 完整的 sql语句,但参数部分尚未装填,由 ? 代替
    this.boundSql = boundSql;
  }

再来看下这所谓的处理器到底是怎么把入参设置进sql内的呢?我们来看看其核心方法 setParameters ,该方法作用是为预处理语句设置参数

  @Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    // 从boundSql中获取参数映射列表,即方法入参对象 和 sql 中预留的参数位置的映射信息
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        // 检查参数模式是否不是OUT , OUT模式一般用在存储过程,拿参数去接返回值
        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;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          // 获取类型处理器,主要是根据方法入参类型确定的
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          // 获取jdbc类型,由用户在xml中预留参数时指定,如 #{orderdesc , JDBCTYPE = VARCHAR}
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            // 如果值为null且JDBC类型也为null,则将JDBC类型设置为null值的默认JDBC类型,
            // ORACLE 出现此情况可能或报错: Error setting null for parameter #XXX with JdbcType OTHER
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            // 使用类型处理程序为预处理语句设置参数值
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

通过对该方法的分析,其实不难看出来,就是把对方法的入参拆成基础的数据类型,然后替换掉sql里的对应的 " ?" 部分。但是具体是怎么做的呢。我们还得看其调用的一个关键类 TypeHandler 以及这里的 typeHandler.setParameter 方法。


3. TypeHandler 类型处理器

TypeHandler 是 MyBatis 框架的一部分,它是一个接口,用于将 Java 类型和数据库类型之间进行转换。在 MyBatis 中,通过 TypeHandler 将 Java 对象转换为 JDBC 可以处理的数据类型,同时也将查询结果从数据库中的数据类型转换为 Java 类型。

public interface TypeHandler<T> {
  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
  T getResult(ResultSet rs, String columnName) throws SQLException;
  T getResult(ResultSet rs, int columnIndex) throws SQLException;
  T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}

可以看出,它不仅有设置参数的能力,而且还能返回结果,即把sql的返回结果,使用对应的java类型展示出来。而且我们先来看一看它的基类 BaseTypeHandler 及核心方法 setParameter

  @Override
  // 将 Java 对象转换为 JDBC 可以处理的数据类型,并设置到 PreparedStatement 对象中
  // 把sql里第i个问号注入参数parameter,且要将parameter对象转换为指定的Jdbc类型
  public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
    if (parameter == null) {
      if (jdbcType == null) {
        throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
      }
      try {
        // 为第i个问号注入null,第二个参数则为jdbc类型的唯一代码,比如FLOAT = 6,DATE = 91等
        ps.setNull(i, jdbcType.TYPE_CODE);
      } catch (SQLException e) {
        throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
              + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
              + "Cause: " + e, e);
      }
    } else {
      try {
        // 如果参数不为空,则需要真正塞值进去,此方法抽象类里没有实现,交由各子类实现
        setNonNullParameter(ps, i, parameter, jdbcType);
      } catch (Exception e) {
        throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
              + "Try setting a different JdbcType for this parameter or a different configuration property. "
              + "Cause: " + e, e);
      }
    }
  }

而 BaseTypeHandler 在mybatis3.5.6 内置了四十三种子类,即有四十三种类型处理器,基本覆盖了当前数据库的各大字段类型,当然同时也支持用户自定义 TypeHandler。

a86afbcf2ec844778d4f0e6537a7e04a.png

我们选择最典型的几种


Double

  public void setNonNullParameter(PreparedStatement ps, int i, Double parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setDouble(i, parameter);
  }

Date

  public void setNonNullParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setTimestamp(i, new Timestamp(parameter.getTime()));
  }

String

  public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setString(i, parameter);
  }

需要注意的是,上面的ps, 已经是来自各数据库的驱动的实现类了,因此可以根据数据库对该字段不同的命名,自行来处理。


4. 实例演示

我们写下这样的代码,Dao层以及SQL文件

    // 实体类,构造方法
    public User(int id, String username, String password) {
        this.id = id;
        this.username = username;
        this.password = password;
    }
    public boolean addUser(User user) {
        User user1 = new User(1 ,"zhangsan","123456");
        User user2 = new User(2 ,"lisi","123456");
        User user3 = new User(3 ,"wangwu","123456");
        List<User> list = new ArrayList<>();
        list.add(user1);
        list.add(user2);
        list.add(user3);
        return userMapper.addUser(list);
    }
  boolean addUser(@Param("list") List<User> users);
    <insert id="addUser" parameterType="com.zhanfu.springboot.demo.entity.User" >
        insert  into user (id,username,password) values
        <foreach collection="list" item="item">
            (#{item.id}, #{item.username}, #{item.password})
        </foreach>
    </insert>

4.1 解析出参数映射关系——ParameterMappings

需要注意的是,因为我们的示例中,含有foreach标签,所以是一段动态sql。而动态sql的逻辑和样式,是在有入参之后才能确定的,所以动态sql解析映射关系是在dao方法真正被调用的时候才开始。而静态sql,会在程序运行时,就构造出ParameterMappings,并存储在StaticSqlSource对象内


像上面这样的Dao方法和Sql,当我们执行Dao方法时,会先把动态条件进行判断好,比如此处列表有三条数据,意味着foreach 就会重复三次,最后解析出来的原生sql 就是


insert into user(id,username,password) values

(#{__frch_item_0.id}, #{__frch_item_0.username},#{__frch_item_0.password})

(#{__frch_item_1.id}, #{__frch_item_1.username},#{__frch_item_1.password})

(#{__frch_item_2.id}, #{__frch_item_2.username},#{__frch_item_2.password})


在通过对这段sql 的 #{} 里面的内容如"__frch_item_0.id",进行分析,结合入参的同名字段的适配,最终会生成一个长度为 9 的映射关系列表,如下图:

3559bcc69ecf4dd8b5033bd3bf199534.png

这样我们就得到了一个ParameterMappings,注意,这里只是映射关系,就是明确了有9个java对象,对应Sql的九个位置,而真正的参数填充还没开始


4.2 遍历填充参数

现在,我们聚焦到DefaultParameterHandler.setParameter() 方法,关注mybatis是怎么填充参数的。

79c0141df7214206afe671f5fcd5adfc.png

57ca96070bb3481bb4a97d35b1ccda03.png

596766dbd52844549d44494e590805d6.png


最终参数的设置,会交由 ClientPreparedStatement 完成,而 ClientPreparedStatement 则是mysql的驱动层了。


五、ResultSetHandler

1. 接口方法

ResultHandler 的接口就是用来处理结果集的,根绝不同的sql分类有三种方法,第一种是最常用的

public interface ResultSetHandler {
   // 处理 ResultSet 的结果,将其转换成一个 List<java对象> 并返回 ———— 最常用
  <E> List<E> handleResultSets(Statement stmt) throws SQLException;
  // 处理Cursor游标的的结果
  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
  // 处理存储过程等带回调参数的情况
  void handleOutputParameters(CallableStatement cs) throws SQLException;
}

2. 实现功能讲解

MyBatis中提供了一个默认的ResultSetHandler实现

70084e6ef3a14b5cab9accf3d6d71421.png

这个类比较大,我们直接看其核心方法,正是将结果集一行行遍历,然后针对指定的出参java类型,使用构造函数构造完后,往里面填入映射的值。

//  public class DefaultResultSetHandler implements ResultSetHandler 
  private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    ResultSet resultSet = rsw.getResultSet();
    skipRows(resultSet, rowBounds);
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
      ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
      // 循环从结果集中获取数据,注意此时的 rowValue 已经是业务对象了
      Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
      // 储存该业务对象
      storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }
  }
  private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException {
    if (parentMapping != null) {
      linkToParents(rs, parentMapping, rowValue);
    } else {
    // 调用 resultHandler 存储本行结果
      callResultHandler(resultHandler, resultContext, rowValue);
    }
  }

3. ResultSetHandler 与 ResultHandler 的联系

ResultHandler 其实是一个储存用的接口,它有两个实现类

public interface ResultHandler<T> {
  void handleResult(ResultContext<? extends T> resultContext);
}
  1. DefaultResultHandler:默认的ResultHandler实现类。它将查询结果存储在一个List中
  2. MapResultHandler:将查询结果转换为Map类型的ResultHandler实现类。它将每条记录转换为一个Map,其中Map的key是列名,value是列的值

这两个类的逻辑非常简单,两个类分别维护了一个List 和 Map,将入参的resultContext内的值解析,存入各自的集合里即可。


需要注意的是,ResultHandler 的入参resultContext仅代表一行数据,真正的返回值可能是多行的,所以 ResultHandler 其实是在for循环中,一行行解析和转换的,而负责处理多行的结果处理器是ResultSetHandler,ResultSet在数据量较大时,会占用较大的内存,而ResultHandler可以将查询结果逐条处理,避免了占用大量内存的问题


所以,ResultSetHandler主要用于将查询结果转换成Java对象;而ResultHandler主要用于对查询结果以某种形式展现。它们的使用场景是不同的

六、总结

ParameterHandler 负责翻译,把java对象的值,翻译进sql的指定位置;

ResultSetHandler 则是一个讲解员,把SQL的结果集按框架搭建出来,再汇报给上级;

StatementHandler则是一个部门经理,它不仅管理着前两者,还能创建statement(即存储着sql的对象),并指挥翻译把入参翻译进statement,然后调用驱动执行statement,最后的结果指挥讲解员把结果以特定格式展示出来;

Executor则是个公司老板,位置更高,不再执行那些基础的工作,而是负责招聘部门经理(创建StatementHandler),并一键通知经理做事,而他自己的主要职责则是与其他公司搞关系(获取数据库连接、事务的提交回滚),调度仓储(缓存)

fdd26eae170745b78fe8d50f3f8c769a.png



目录
相关文章
|
7月前
|
存储 前端开发 Java
MyBatis 四大核心组件之 ResultSetHandler 源码解析
MyBatis 四大核心组件之 ResultSetHandler 源码解析
|
7月前
|
SQL 前端开发 Java
MyBatis 四大核心组件之 ParameterHandler 源码解析
MyBatis 四大核心组件之 ParameterHandler 源码解析
|
7月前
|
SQL 前端开发 Java
MyBatis 四大核心组件之 StatementHandler 源码解析
MyBatis 四大核心组件之 StatementHandler 源码解析
|
SQL Java 数据库连接
MyBatis的基本概念和核心组件
MyBatis的基本概念和核心组件
|
安全 Java 数据库连接
Rpamis-security-基于Mybatis-Plugin的一站式加解密脱敏安全组件
项目是一个基于Mybatis插件开发的安全组件,旨在提供更优于市面上组件的脱敏、加解密落库等企业数据安全解决方案。组件提供注解式编程方式,开发者只需要对需要处理的字段或方法加上对应注解,无需关心安全相关需求,由组件全自动完成脱敏、加解密等功能
191 7
|
7月前
|
SQL 缓存 Java
MyBatis 四大核心组件之 Executor 源码解析
MyBatis 四大核心组件之 Executor 源码解析
|
2月前
|
Java 数据库连接 Maven
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
150 2
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
|
2月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
79 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
2月前
|
前端开发 Java Apache
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
本文详细讲解了如何整合Apache Shiro与Spring Boot项目,包括数据库准备、项目配置、实体类、Mapper、Service、Controller的创建和配置,以及Shiro的配置和使用。
556 1
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
|
2月前
|
SQL Java 数据库连接
mybatis使用二:springboot 整合 mybatis,创建开发环境
这篇文章介绍了如何在SpringBoot项目中整合Mybatis和MybatisGenerator,包括添加依赖、配置数据源、修改启动主类、编写Java代码,以及使用Postman进行接口测试。
27 0
mybatis使用二:springboot 整合 mybatis,创建开发环境