MyBatis 执行SQL的原理

在开发中使用mybatis,是将Mapper接口生成的代理类存入到spring IOC容器中。然后,通过注解获取代理对象,调用对应接口方法,间接调用到 mapper xml中的SQL。

如果想了解mybatis是如何通过接口实现SQL操作可以看一下这个:https://www.toutiao.com/article/7206541912168268343


一般接口都会有入参,和返回对象。那么mybatis是如何进行封装的呢?

mapper xml 文件中的语句 经常会使用一些标签 if 、foreach、set 、where 等,是如何实现的呢?


Mybatis 参数的解析

参数解析这一步骤,通常是在执行SQL语句之前就要完成的,因此,需要查看SQL执行之前的代码。问题是从哪里开始查看源码呢?既然不知道具体是哪里进行了参数解析,那就从 调用代理对象开始 。


代理对象调用方法执行 MapperProxy.invoke 方法,MapperProxy.invoke方法中调用 MapperProxy.cachedInvoker 方法,并且该方法中创建一个 MapperMethod 实例对象。

MapperMethod对象中有两个成员常量,SqlCommand 和 MethodSignature对象。后面会用到。

最终MapperProxy.cachedInvoker 方法返回一个 PlainMethodInvoker 对象,接着PlainMethodInvoker对象调用自己的 invoke 方法(注意:这就是一个invoke方法,跟代理没关系,不要被迷惑了)

调用方法后,执行 MapperMethod对象的 execute 方法。该方法中

method.convertArgsToSqlCommandParam(args),就是我们要找的参数解析方法

这块代码可以不看。

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);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        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;
  }


convertArgsToSqlCommandParam 方法,MethodSignature 对象是创建MapperMethod时一个成员常量。

ParamNameResolver 是 MethodSignature中的一个成员常量。

ParamNameResolver 对象中 ParamNameResolver 方法。该方法获取 带有@param注解的参数。

ParamNameResolver是用来解析 @param 类型的

public ParamNameResolver(Configuration config, Method method) {
  //默认使用 实际参数名
  this.useActualParamName = config.isUseActualParamName();
  final Class<?>[] paramTypes = method.getParameterTypes();
  final Annotation[][] paramAnnotations = method.getParameterAnnotations();
  final SortedMap map = new TreeMap<>();
  int paramCount = paramAnnotations.length;
  // get names from @Param annotations    获取@Param注解的参数
  for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
    if (isSpecialParameter(paramTypes[paramIndex])) {
      // skip special parameters
      continue;
    }
    String name = null;
    for (Annotation annotation : paramAnnotations[paramIndex]) {
      if (annotation instanceof Param) {
        hasParamAnnotation = true;
        name = ((Param) annotation).value();
        break;
      }
    }
    if (name == null) {
      // @Param was not specified.
      if (useActualParamName) {
        name = getActualParamName(method, paramIndex);
      }
      if (name == null) {
        // use the parameter index as the name ("0", "1", ...)
        // gcode issue #71
        name = String.valueOf(map.size());
      }
    }
    map.put(paramIndex, name);
  }
  names = Collections.unmodifiableSortedMap(map);
}



用来解析实体参数 (param1, param2, ...)

public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
      return null;
    } 
  //如果只有一个参数,并且没有使用@Param注解,就直接返回第一个参数
  else if (!hasParamAnnotation && paramCount == 1) {
      Object value = args[names.firstKey()];
      return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
    } 
  //有多个参数,则封装成一个map,key为参数参数名称,
  //参数使用@Param注解,名称就是注解中的值,否则key为arg0、arg1这种类型,
  //同时,一定含有key为param1、param2的参数,值就是传入的值
  else {
      final Map param = new ParamMap<>();
      int i = 0;
      for (Map.Entry entry : names.entrySet()) {
        //没有@Param注解,key为arg0、arg1这种类型
        param.put(entry.getValue(), args[entry.getKey()]);
        // add generic param names (param1, param2, ...)
        final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
        // ensure not to overwrite parameter named with @Param
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
  }

小结:

ParamNameResolver 对象解析参数,两种方法:

ParamNameResolver方法:解析形参,判断是否使用了@Param注解。


getNamedParams方法:封装实参,如果只有一个,并且没有使用@Param注解,就直接返回第一个参数值,否则封装成map。 多个参数,key为参数参数名称,使用@Param注解,key值就是注解中的值,否则key为arg0、arg1这种类型,并且,一定含有key为param1、param2。。。的参数,作为值传入。


上面的这一部分主要是将参数解析。



Mybatis 解析好的参与SQL整合

参数解析完成后,执行SqlSession中的方法。

以selectList方法为例,首先获取MappedStatement对象,给对象包含SQL。

wrapCollection(parameter) 方法对解析后的参数进行了再次封装。

对集合、数组、列表进行了二次封装,其他类型原样返回。

执行 executor.query(ms, wrapCollection(parameter), rowBounds, handler) 代码后,进入BaseExecutor类中query方法。

BoundSql boundSql = ms.getBoundSql(parameter);

通过 mappedStatement 对象,获取 BoundSql对象。

然后,根据mappedStatement 成员变量 sqlSource 调用 getBoundSql 。

BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

  1. BoundSql语句的解析主要是通过对#{}字符的解析,将其替换成?。
  2. #{}中的key属性以及相应的参数映射,比如javaType、jdbcType等信息均保存至BoundSql的parameterMappings属性中。
  3. BoundSql类中的成员:

ParameterObject :传入的参数。

一个参数对象时:为该参数的类型;

多个参数对象时:为ParamMap对象。

ParameterMapping:#{name}形式的引用变量,变量会在解析Mapper.xml文件中的语句时,就被替换成占位符“?”。ParameterMapping类记录对应的变量信息

additionalParameters:使用Criteria对象的sql。

MetaObject:Mybatis在sql参数设置和结果集映射里经常使用到这个对象。


DynamicSqlSource或者RowSqlSource,前者表示动态SQL,后者表示静态SQL


动态SQL:行该sql相关操作的时候才根据传入的参数进行解析。(mybatis 标签 if 、where 等)

以StaticSqlSource为例:


getBoundSql直接返回BoundSql对象。


代码返回到:BaseExecutor 类query方法中。

query 方法调用 queryFromDatabase 方法,queryFromDatabase 再调用

doQuery(ms, parameter, rowBounds, resultHandler, boundSql)方法。


doQuery 方法中 prepareStatement 就是给预编译SQL进行赋值的。

@Override
public  List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    //预编译sql,并且给参数赋值
    stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

prepareStatement方法中 handler.parameterize(stmt) 就是设置参数值。

调用PreparedStatementHandler 类的 parameterize方法。

parameterize方法中的 parameterHandler.setParameters((PreparedStatement) statement) 调用 DefaultParameterHandler setParameters 方法。

typeHandler.setParameter(ps, i + 1, value, jdbcType);

千辛万苦终于是把解析的参数与SQL语句进行融合了。


本篇篇幅有点长,后期拆分一下。

展开阅读全文

页面更新:2024-04-30

标签:常量   注解   变量   语句   接口   原理   对象   成员   参数   类型   方法

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top