MyBatis(技术NeiMu):核心处理层(StatementHandler)

回顾

StatementHandler

StatementHandler接口是MyBatis的核心接口之一,它完成了MyBatis中最核心的工作,也就是如何去与使用原生JDBC数据库进行交互,是Executor接口的基础,Executor接口也是核心的接口之一

StatementHandler的功能

  • 创建对应的Statement对象
  • 为SQL绑定实参
  • 执行各种SQL语句
  • 批量执行SQL语句
  • 将结果集映射到结果对象

说白了其实就是组合前面我们研究过的各种模块功能,比如创建Statement对象需要的SQL、实参与占位符的映射是交由前面DynamicSqlSourc或者RawSqlSource;将结果集映射到结果对象,就是上一篇学到的DefaultResultSetHandler负责处理
在这里插入图片描述
在这里插入图片描述
其是一个接口,来看一下其各种方法

  • batch:批量执行SQL
  • getBoundSql:获取绑定的SQL
  • getParameterHandler:获取参数处理器
  • prepare:一些Statement的初始化,比如超时时间、fetchSize
  • update:执行更改语句,也就是增删改
  • query:执行查询语句

在这里插入图片描述
可以看到其实现类有两个

  • BaseStatementHandler:抽象类,提供了一些StatementHandler的共性实现,其实就是提供了参数绑定的方法,其有3个子类,对应具体执行的SQL类型

    • PreparedStatementHandler:用于执行预处理SQL的
    • CallableStatementHandler:用于执行过过程函数的
    • SimpleStatementHandler:用于执行不需要预处理的SQL的
  • RoutingStatementHandler:采用了装饰者模式,用来装饰增强上面PreparedStatementHandler、CallableStatementHandler和SimpleStatementHandler的,虽然从代码上没看到任何增强作用,里面的方法都是直接调用组装的statementHandler方法的

RoutingStatementHandler

在这里插入图片描述
成员属性只有一个delegate,就是装饰的StatementHandler,在构造方法上,会根据MappedStatement的StatementType来装饰对应的StatementHandler;前面已经研究过,MappedStatement对应的就是一个SQL节点,也就是select、insert、delete、update节点
在这里插入图片描述
然后其实现StatementHandler接口的方法都是直接调用装饰的StatementHandler的,并没有看到一些增强作用,个人觉得可能是MyBatis作者觉得这样便于以后扩展吧,不过有人认为这个RoutingStatementHandler是一个代理类,也有人觉得RoutingStatementHandler使用的是策略模式,关于RoutingStatementHandler究竟作用是什么,后面我们在讨论

BaseStatementHandler

BaseStatementHandler是一个实现了StatementHandler接口的抽象类,实现了绑定参数、生成主键和prepare初始化Statement等方法,其他具体的细节都是交由子类去实现的

在这里插入图片描述
成员属性有如下

  • configuration:MyBatis的配置

  • objectFactory:创建实例

  • typeHandlerRegistry:类型转换器注册中心

  • ResultSetHandler:结果集映射处理器

  • ParameterHandler:用于给SQL绑定实参的

  • executor:执行SQL的executor对象

  • mappedStatement:需要执行的SQL节点的解析对象,里面不仅有SQL,还有对应的ResultMap

  • rowBounds:分页

  • BoundSql:绑定的SQL

构造方法

protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    //从MappedStatement中获取Configuration,个人感觉这个Configuration好乱呀
    this.configuration = mappedStatement.getConfiguration();
    this.executor = executor;
    this.mappedStatement = mappedStatement;
    this.rowBounds = rowBounds;
	//从Configuration中获取类型转换器注册中心
    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    //获取ObjectFactory,用来创建实例
    this.objectFactory = configuration.getObjectFactory();
	//使用MappedStatement获取绑定的SQL
    if (boundSql == null) { // issue #435, get the key before calculating the statement
      generateKeys(parameterObject);
      boundSql = mappedStatement.getBoundSql(parameterObject);
    }
		
    this.boundSql = boundSql;
	//创建ParameterHandler和ResultSetHandler
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
  }

BaseStatementHandler比较关键的其实就是prepare方法,用来初始化Statement的

@Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
        //创建对应的Statement,延迟到子类去实现
      statement = instantiateStatement(connection);
        //初始化Statement超时时间
      setStatementTimeout(statement, transactionTimeout);
        //初始化Statement的fetSuze
      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);
    }
  }

没了,所以BaseStaementHandler仅仅提供初始化Statement的prepare方法,而具体创建对应类型的Statement的instantiateStatement方法则是延迟到子类去实现,这也是模板方法的一个体现

下面需要认识一下BaseStatementHandler的两个重要对象

  • ParameterHandler:用于参数绑定的
  • ResultSetHandler:前面已经认识过了,用来完成结果集映射的
ParameterHandler

前面解析SQL节点时,对于DynamicSqlSource和RawSqlSource都是可能包含问号占位符,而每个问号占位符都是对应两个SqlSource里面的ParameterMappings集合里面的一个元素, 并且是按顺序对应的,但此时仅仅存在映射关系,还没有具体绑定!

具体绑定参数其实就是指对应符号位去注入参数!像以前使用原生JDBC的时候,使用PrepareStatement时,也是要对应占位符来进行注入参数才能够执行SQL,而具体绑定参数的动作是交由ParameterHandler来负责执行的

在这里插入图片描述
可以看到,ParameterHandler是一个接口,里面有两个方法

  • getParameterObject:获取注入的Java实参对象
  • setParameters:给对应的PrepareStatement注入参数

在这里插入图片描述
而MyBatis对于ParameterHandler仅仅只有DefaultParameterHandler实现类

DefaultParameterHandler

在这里插入图片描述
成员属性有如下

  • TypeHandlerRegistry:类型转换注册中心
  • MappedStatement:对应的解析SQL节点生成的MappedStatement
  • ParameterObject:注入的实参
  • Configuration:配置

比较重要的方法是setParameters,也就是DefaultParameterHandler是如何给预处理SQL绑定参数的

@Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
      //获取ParameterMappings
      //前面提到过ParameterMapping里面就是#{}占位符里面的内容!
      //对应一个#{}占位符
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
      //判空
    if (parameterMappings != null) {
        //遍历ParameterMappings集合
      for (int i = 0; i < parameterMappings.size(); i++) {
          //当前的ParameterMapping
        ParameterMapping parameterMapping = parameterMappings.get(i);
          //如果不是输出参数
          //存储过程才会有输出参数
        if (parameterMapping.getMode() != ParameterMode.OUT) {
            //value用于记录当前ParameterMapping对应的实参
          Object value;
            //获取${}里面的token
            //需要使用token来获取实参的
            //从传进来的java对象中,根据token取出对象里面的对应的属性作为实参
          String propertyName = parameterMapping.getProperty();
            //判断有没有额外的参数
          if (boundSql.hasAdditionalParameter(propertyName)) { 
            value = boundSql.getAdditionalParameter(propertyName);
          } 
            //如果传进来的参数对象为空,那就直接为null
            else if (parameterObject == null) {
            value = null;
          } 
            //判断类型转换注册中心有没有该参数对象的类型转换
            //如果有,其实就代表是基础类型了,不需要进行映射解析了
            //比如Integer、Long、Double那些
            else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } 
            //如果参数对象不为空,并且不是基本类型、或者对应的包装类
            //代表是一个封装起来的对象,就需要进行解析
            else {
                //获取参数对象的MetaObject对象
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
                //从MetaObject对象中获取token对应的成员属性
                //MetaObject获取token对应的成员属性是使用get方法来完成的
                //因为参数对象必须要有get方法
            value = metaObject.getValue(propertyName);
          }
            //获取${}占位符里面的type属性对应的TypeHandler
            //TypeHandler用来类型转换,Java转为JDBC
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
            //获取${}占位符里面的jdbc属性对应的JdbcType
          JdbcType jdbcType = parameterMapping.getJdbcType();
            //但一般我们都不会去设置这两个属性,一般都是${xxx}完事
            //因此需要完成自动映射
          if (value == null && jdbcType == null) {
              //从配置文件中获取jdbcType为Null时的JDBC类型
              //是一个OTHER类型
              //OTHER类型表明jdbcType直接特定于数据库对应的类型
              //就是数据库中是什么类型,jdbcType就是什么类型
              //是JDBC提供的一种类型
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
              //使用TypeHandler来给PrepareStatement对应占位符位置注入实参
              //因为PrepareStatement实参与占位符绑定的位置是从1开始,因此要加1
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

现在大概清楚了DefaultParameterHandler是如何工作的

  • 获取前面解析SQL的#{}占位符时的ParameterMappings集合
  • 遍历ParameterMappings集合
  • 判断传进来的参数对象是否为空,如果为空,那么实参也为Null
  • 判断传进来的参数对象是不是已经注册在类型转换中心了,如果已经注册,代表参数对象是基础类型,也就是基本数据类型、包装类、String和日期等类型,参数对象本身就是实参,不需要继续解析其成员属性
  • 如果参数对象不为空,而且不是基础类型,那么代表参数对象是一个自定义的引用类型,真正的实参是参数对象内部的成员属性,将参数对象转换为MetaObject
  • 取出#{}占位符里面的token,参数对象的MetaObject通过token来调用参数对象的get方法,从而获取占位符对应的实参
  • 然后会根据ParameterMapping对象里面的JdbcType和TypeHandler
  • JdbcType就是确认的JDBC类型,如果前面#{}没有配置的话,会为OTHER类型
  • 使用TypeHandler来给PrepareStatement按照当前位置去注入实参

认识完ParameterHandler之后,具体看BaseStatementHandler的子类

SimpleStatementHandler

在这里插入图片描述
SimpleStatementHandler是用来执行一些普通不需要注入参数的SQL的,也就是使用Statement来完成数据库操作,所以SQL中是不能有占位符的,也就是不可以预处理

创建Statement

BaseStatementHandler已经提供了prepare模板方法去初始化对应的Statement了,然后延迟了创建Statment的具体方法到子类去实现,也就是instantiateStatement方法

在这里插入图片描述
可以看到,SimpleStatementHandler只是简单地使用Connection去创建了Statement出来

执行查询

在这里插入图片描述
可以看到,执行查询的方法就是获取SQL了之后,就直接调用Statement的execute方法,然后交由ResultSetHandler去处理结果集映射

执行修改
@Override
  public int update(Statement statement) throws SQLException {
      //获取SQL
    String sql = boundSql.getSql();
      //获取注入的实参
      //因为
    Object parameterObject = boundSql.getParameterObject();
      //获取主键生成器
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    int rows;
      //使用主键生成器
    if (keyGenerator instanceof Jdbc3KeyGenerator) {
        //执行SQL
      statement.execute(sql, Statement.RETURN_GENERATED_KEYS);
        //MySQL返回的添加结果行数
      rows = statement.getUpdateCount();
        //将生成的主键注入进实参中,这里是数据库生成的主键
      keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
    } else if (keyGenerator instanceof SelectKeyGenerator) {
        //执行SQL
      statement.execute(sql);
        //MySQL返回添加的结果行数
      rows = statement.getUpdateCount();
        //将生成的主键注入进实参中,这里是SlectKey节点中的SQL生成的主键
      keyGenerator.processAfter(executor, mappedStatement, statement, parameterObject);
    } else {
        //如果没有主键生成器,直接执行
      statement.execute(sql);
      rows = statement.getUpdateCount();
    }
      //返回MySQL的结果
    return rows;
  }

可以看到,SimpleStatement除了执行SQL之外,还要去处理主键的生成,执行完SQL后,将生成的主键注入进实参中!从而获取到主键

  • 使用Statement执行插入SQL
  • 使用KeyGenerator生成主键,调用后处理方法,将主键注入进实参中
PrepareStatementHandler

在这里插入图片描述
PrepareStatementHandler与SimpleStatement的较大不同之处在于PrepareStatementHandler是基于PrepareStatement接口完成数据库交互的;而SimpleStatement是基于Statement接口来完成交互的;说白了就是一个可以预处理,另外一个不能预处理

创建Statement

同样,先去看看其是如何创建Statement的

protected Statement instantiateStatement(Connection connection) throws SQLException {
    //获取绑定的SQL
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
          //创建PrepareStatement
        return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
      } else {
          //创建PrepareStatement
        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);
    }
  }
  • 获取绑定的SQL

  • 使用绑定的SQL去创建PrepareStatement

  • 而前面的SimpleStatementHandler,则是创建Statement,然后只有在执行SQL的时候才回去绑定SQL

执行查询

在这里插入图片描述
可以看到,关于查询的方法也是直接调用PrepareStatement的execute方法去执行,然后使用ResultSetHandler来进行处理结果集映射而已

执行修改
public int update(Statement statement) throws SQLException {
    //执行SQL
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    //获取修改的行数
    int rows = ps.getUpdateCount();
    //获取实参对象
    Object parameterObject = boundSql.getParameterObject();
    //给实参对象去注入主键
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    //返回修改的行数
    return rows;
  }

可以看到,执行修改的动作跟SimpleStatement其实是很类似的

  • 执行SQL
  • 获取修改的行数
  • 获取实参对象
  • 获取主键的生成器
  • 往实参对象注入生成的主键
  • 返回修改的行数

可能有人觉得,PrepareStatement不是还要绑定参数的吗?!

其实PrepareStatementHandler提供了绑定参数的方法,但PrepareStatementHandler自身并没有去绑定,而是由上层的Executor接口进行绑定!

绑定实参

在这里插入图片描述
可以看到,绑定实参就是调用ParameterHandler去给PrepareStatement去绑定实参,具体的细节上面已经提到过

CallableStatementHandler

在这里插入图片描述
CallableStatementHandler是用来执行存储过程的,底层依赖CallableStatement去完成数据库交互的功能

具体的细节就不看了,比较少用函数过程

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值