mybatis之SQLNode&SqlSource

SqlNode&SqlSource

1.DynamicContext

DynamicContext主要用于记录解析动态SQL语句之后产生的SQL语句片段,可以认为它是一个用于记录动态SQL语句解析结果的容器。DynamicContext中核心字段 含义如下:

 // 参数上下文
  private final ContextMap bindings;
  // 在SqlNode解析动态SQL时,会将解析后的SQL片段添加到该属性中保存,最终拼凑出一条完成的SQL语句
  private final StringJoiner sqlBuilder = new StringJoiner(" ");

ContextMap是DynamicContext中定义的内部类,它实现了HashMap并重写了get()方法,其具体现如下:

static class ContextMap extends HashMap<String, Object> {
    private static final long serialVersionUID = 2977601501966151582L;
    // /将用户传入的参数封装成了MetaObject对象
    private final MetaObject parameterMetaObject;
    private final boolean fallbackParameterObject;

    public ContextMap(MetaObject parameterMetaObject, boolean fallbackParameterObject) {
      this.parameterMetaObject = parameterMetaObject;
      this.fallbackParameterObject = fallbackParameterObject;
    }

    @Override
    public Object get(Object key) {
      String strKey = (String) key;
      // 如果ContextMap中已经包含了该key,则直接返回
      if (super.containsKey(strKey)) {
        return super.get(strKey);
      }

      if (parameterMetaObject == null) {
        return null;
      }

      //是应变参数对且没有在参数元对象像中找到对应strKey对应的Getter方法
      if (fallbackParameterObject && !parameterMetaObject.hasGetter(strKey)) {
        return parameterMetaObject.getOriginalObject();
      } else {
        // issue #61 do not modify the context when reading
        // 从运行时参数中查找对应属性
        return parameterMetaObject.getValue(strKey);
      }
    }
  }

DynamicContext的构造方法会初始bindings集合,注意构造方法的第二个参数parameterObject,它是运行时用户传入的参数 中包含了后续用于替换#{}占位符的实参。DynamicContext构造=造方法的具体实现如下:

public DynamicContext(Configuration configuration, Object parameterObject) {
    // 对于非Map类型的参数,会创建对应的MetaObject对象,并封装成ContextMap对象
    if (parameterObject != null && !(parameterObject instanceof Map)) {
      MetaObject metaObject = configuration.newMetaObject(parameterObject);
      boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
      bindings = new ContextMap(metaObject, existsTypeHandler);
    } else {
      bindings = new ContextMap(null, false);
    }
    // //将PARAMETER_OBJECT_KEY -> parameterObject这一对应关系添加到bindings集合中
    bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
  }

DynamicContext中常用的两个方法是appendSql()方法和getSql()方法:

// 追加SQL片段 
public void appendSql(String sql) {
    sqlBuilder.add(sql);
}
// 获取解析后的SQL语句
public String getSql() {
    return sqlBuilder.toString().trim();
}
2.SqlNode

了解了DynamicContext的功能之后,我们继续介绍SqINode接口实现类如何解析其对应的动态SQL节点。SqINode接口的定义如下:

public interface SqlNode {
  // apply()是SqlNode接口中定义的唯一方法,该方法会根据用户传入的实参,参数解析该SqlNode记录的动态SQL节点,
  // 并调用DynamicContext.appendSql()方法将解析后的SQL片段追加到DynamicContext.sqlBuilder中保存
  // 当SQL节点下的所有SqlNode完成解析后,我们就可以从DynamicContext获取一条动态生成的、完整的SQL语句
  boolean apply(DynamicContext context);
}

SqlNode接口有多个实现类,每个实现类对应一个动态SQL节点 。按照组合模式的角色来划分,SqlNode扮演了抽象组件的角色,MixedSqlNode扮演了树枝节点的角色,TextSqlNode节点扮演了树叶节点的角色等。

**StaticTextSqlNode&MixedSqlNode **

StaticTextSqlNode中使用text字段记录了对应的非动态SQL语句节点,apply()方法直接将text宇段追加到DynamicContext.sqlBuilder字段中。

MixedSqlNode中使用contents字段记录其子节点对应的SqINode对象集合,其apply()方法会循环调用contents集合中所有SqINode对象的apply()方法。

**TextSqlNode **

TextSqlNode表示的是包含 占 位 符 的 动 态 S Q L 节 点 。 T e x t S q ! N o d e . i s D y n a m i c ( ) 方 法 在 前 面 己 经 分 析 过 了 , 这 里 不 再 重 复 。 T e x t S q l N o d e . a p p l y ( ) 方 法 会 使 用 G e n e r i c T o k e n P a r s e r 解 析 {}占位符的动态SQL节点。TextSq!Node.isDynamic()方法在前面己经分析过了,这里不再重复。TextSqlNode.apply()方法会使用GenericTokenParser解析 SQLTextSq!Node.isDynamic()TextSqlNode.apply()使GenericTokenParser{}占位符,并直接替换成用户给定的实际参数值,具体实现如下:

 @Override
  public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    // 将解析后的 SQL 片段添加到 DynamicContext
    context.appendSql(parser.parse(text));
    return true;
  }

  private GenericTokenParser createParser(TokenHandler handler) {
    //解析的是${}占位符
    return new GenericTokenParser("${", "}", handler);
  }

BindingTokenParser是TextSqlNode中定义的内部类,继承了TokenHandler接口,它的主要功能是根据DynamicContext.bindings集合中的信息解析SQL语句节点中的${}占位符。BindingTokenParser.context字段指向了对应的DynamicContext对象,BindingTokenParse.handleToken()方法的实现如下:

 public String handleToken(String content) {
      // 获取用户提供的实参
      Object parameter = context.getBindings().get("_parameter");
      if (parameter == null) {
        context.getBindings().put("value", null);
      } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
        context.getBindings().put("value", parameter);
      }
      // 通过OGNL解析content的值
      Object value = OgnlCache.getValue(content, context.getBindings());
      String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
      // 检测合理性
      checkInjection(srtValue);
      return srtValue;
    }

**IfSqlNode **

IfSqlNode对应的动态SQL节点是节点,其中定义的字段含义如下:

 // 用于解析节点test表达式的值
  private final ExpressionEvaluator evaluator;
  // 记录了test的表达式
  private final String test;
  // 记录了节点的子节点
  private final SqlNode contents;

IfSqlNode.apply()方法首先会通过ExpressionEvaluator.evaluateBoolean()方法检测其test表达式是否为true,然后根据test表达式的结果,决定是否执行其子节点的apply()方法:

@Override
  public boolean apply(DynamicContext context) {
    // 检测test属性中的表达式
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      // 如果表达式为true,调用子节点的apply()方法
      contents.apply(context);
      return true;
    }
    return false;
  }

   //ExpressionEvaluator.evaluateBoolean()方法
   public boolean evaluateBoolean(String expression, Object parameterObject) {
    // 首先通过OGNL解析表达式的值
    Object value = OgnlCache.getValue(expression, parameterObject);
    // 处理Boolean类型
    if (value instanceof Boolean) {
      return (Boolean) value;
    }
    // 处理Number类型
    if (value instanceof Number) {
      return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
    }
    return value != null;
   }

**TrimSqlNode&WhereSqlNode&SetSqlNode **

TrimSqlNode会根据子节点的解析结果,添加或删除相应的前缀或后缀。TrimSqlNode中字段的含义如下:

  // 的子节点
  private final SqlNode contents;
  // 前缀字符串
  private final String prefix;
  // 后缀字符串
  private final String suffix;
  // 如果节点包裹的SQL语句是空语句,删除指定的前缀,如 where
  private final List<String> prefixesToOverride;
  // 如果节点包裹的SQL语句是空语句,删除指定的后缀,如 逗号
  private final List<String> suffixesToOverride;

TrimSqlNode的构造函数中,会调用 parseOverrides()方法对参数prefixesToOverride和参数sufftxesToOverride进行解析,井初始化prefixesToOverride和suffixesToOverride 具体实现如下:

 private static List<String> parseOverrides(String overrides) {
    if (overrides != null) {
      // 按照|进行分割
      final StringTokenizer parser = new StringTokenizer(overrides, "|", false);
      final List<String> list = new ArrayList<>(parser.countTokens());
      while (parser.hasMoreTokens()) {
        // 转换为大写,并添加到集合中
        list.add(parser.nextToken().toUpperCase(Locale.ENGLISH));
      }
      return list;
    }
    return Collections.emptyList();
  }

了解了TrimSq!Node字段的初始化之后,再来看TrimSqlNode.apply()方法的实现。该方法首先解析子节点,然后根据子节点的解析结果处理前缀和后缀,其具体实现如下:

 @Override
  public boolean apply(DynamicContext context) {
    // 创建FilteredDynamicContext对象封装了DynamicContext
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    // 调用子节点的apply()方法
    boolean result = contents.apply(filteredDynamicContext);
    // 用FilteredDynamicContext.applyAll()方法处理前级和后缀
    filteredDynamicContext.applyAll();
    return result;
  }

处理前缀和后缀的主要逻辑是在FilteredDynamicContext中实现的,它继承了DynamicContext,同时也是DynamicContext的代理类。FilteredDynamicContext除了将对应方法调用委托给其中封装的DynamicContext对象,还提供了处理前缀和后缀的applyAll()方法。FilteredDynamicContext中各个字段的含义如下:

 // DynamicContext对象
    private DynamicContext delegate;
    // 是否已经处理过前级和后缀,初始值都为false
    private boolean prefixApplied;
    private boolean suffixApplied;
    // 用于记录子节点解析后的结果
    private StringBuilder sqlBuffer;

FilteredDynamicContext.applyAll()方法具体实现如下:

public void applyAll() {
      sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
      // 获取子节点解析后的结果,并全部转换为大写
      String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
      if (trimmedUppercaseSql.length() > 0) {
        // 处理前缀
        applyPrefix(sqlBuffer, trimmedUppercaseSql);
        // 处理后缀
        applySuffix(sqlBuffer, trimmedUppercaseSql);
      }
      // 将解析后的结果添加到delegate中
      delegate.appendSql(sqlBuffer.toString());
    }

FilteredDynamicContext.applyPrefix()方法和applySuffix()方法主要负责处理前缀和后缀,具体实现如下:

  private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
      // 检测是否已经处理过前缀
      if (!prefixApplied) {
        // 标记已处理过前缀
        prefixApplied = true;
        if (prefixesToOverride != null) {
          for (String toRemove : prefixesToOverride) {
            if (trimmedUppercaseSql.startsWith(toRemove)) {
              // 如果以prefixesToOverride中某项开头,则将该项从SQL语句开头删除掉
              sql.delete(0, toRemove.trim().length());
              break;
            }
          }
        }
        // 添加前缀
        if (prefix != null) {
          sql.insert(0, " ");
          sql.insert(0, prefix);
        }
      }
    }

    private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
      // 检测是否已经处理过后缀
      if (!suffixApplied) {
        // 标记已处理过后缀
        suffixApplied = true;
        if (suffixesToOverride != null) {
          for (String toRemove : suffixesToOverride) {
            if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
              // 如果以suffixesToOverride中某项结尾,则将该项从SQL语句结尾删除掉
              int start = sql.length() - toRemove.trim().length();
              int end = sql.length();
              sql.delete(start, end);
              break;
            }
          }
        }
        // 添加后缀
        if (suffix != null) {
          sql.append(" ");
          sql.append(suffix);
        }
      }
    }

WhereSqlNode和SetSqlNode都继承了TrimSqlNode ,其中WhereSqlNode指定了prefix字段为WHERE,prefixesToOverride集合中的项为AND和OR,suffix字段和suffixesToOverride集合为null,也就是说,节点解析后的SQL语句片段如果以AND或OR开头,则将开头处的AND或OR删除,之后再将WHERE关键字添加到SQL片段开始位置,从而得到该节点最终生成的SQL片段。

SetSqlNode指定了prefix字段为SET,suffixesToOverride集合中的项只有“,”,suffix字段和prefixesToOverride集合为null,也就是说节点解析后的SQL语句片段如果以“,”结尾,则将结尾处的“,”删除掉,之后再将SET关键字添加到SQL片段的开始位置,从而得到该节点最终生成的SQL片段。 WhereSqlNode和SetSqlNode实现比较简单,代码不在此分析:

ForEachSqlNode

在动态SQL语句中构建IN条件语句的时候,通常需要对一个集合进行迭代,MyBatis提供了标签实现该功能,在使用标签迭代集合时,不仅可以使用集合的元素和索引值,还可以在循环开始之前或结束之后添加指定的字符串,也允许在迭代过程中添加指定的分隔符。

标签对应SqINode实现ForeachSqlNode, ForeachSqlNode 中各个字段含义和功能具体实现如下:

// 用于判断循环的终止条件
  private final ExpressionEvaluator evaluator;
  // 迭代的集合表达式
  private final String collectionExpression;
  // 记录了节点的子节点
  private final SqlNode contents;
  // 循环开始时添加的字符串
  private final String open;
  // 循环结束时添加的字符串
  private final String close;
  // 分隔符
  private final String separator;
  // index代表的是当前迭代的次数,item的值是本次迭代的元素。若迭代集合是Map,则index是键,item是值
  private final String item;
  private final String index;
  //配置对象
  private final Configuration configuration;

在开始介绍ForEachSqlNode的实现之前,先来分析其中定义的两个内部类,分别是PrefixedContext和FilteredDynamicContext,它们都继承了DynamicContext,同时也都是DynamicContext的代理类。首先来看 PrefixContext各个字段的含义:

// 底层封装的DynamicContext对象
    private final DynamicContext delegate;
    // 指定的前缀
    private final String prefix;
    // 是否处理过前缀
    private boolean prefixApplied;

PrefixedContext中其他方法都是通过调用delegate的对应方法实现的,不再赘述。

FilteredDynamicContext负责处理#{}占位符 ,但它并未完全解析#{}占位符,其中各个字段的含义如下:

 // 底层封装的DynamicContext对象
    private final DynamicContext delegate;
    // 对应集合项在集合中索引的位置
    private final int index;
    // 对应集合项的index
    private final String itemIndex;
    // 对应集合项的item
    private final String item;

FilteredDynamicContext.appendSql()方法会将#{item}占位符转换成#{__frch_item_1}的格式,其中__frch_是固定的前缀, item与处理前的占位符一样,未发生改变,1则是FilteredDynamicContext产生的单调递增值;还会将#{itemIndex}占位符转换成#{__frch_itemIndex_1}的格式,其中各个部分的含义同上。该方法的具体实现如下:

 @Override
    public void appendSql(String sql) {
      // 创建GenericTokenParser解析器
      GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
        // 对item进行处理
        String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
        if (itemIndex != null && newContent.equals(content)) {
          // 对itemIndex处理
          newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
        }
        return "#{" + newContent + "}";
      });
      // 将解析后的SQL语句片段追加到delegate中保存
      delegate.appendSql(parser.parse(sql));
    }

现在回到对ForEachSqlNode.apply()方法的分析,该方法的主要步骤如下:

(1)解析集合表达式,获取对应的实际参数

(2)在循环开始之前,添加 open 字段指定的字符串。具体方法的applyOpen()代码如下:

 private void applyOpen(DynamicContext context) {
    if (open != null) {
      context.appendSql(open);
    }
  }

(3)开始遍历集合,根据遍历的位置是否指定分隔符。用PrefixedContext封装DynamicContext。

(4)调用applylndex()方法将index添加到DynamicContext.bindings集合中,供后续解析使用,applyIndex()方法的实现如下:

private void applyIndex(DynamicContext context, Object o, int i) {
    if (index != null) {
      // key为index, value是集合元素
      context.bind(index, o);
      // 为index添加前缀和后缀形成新的key
      context.bind(itemizeItem(index, i), o);
    }
  }
private static String itemizeItem(String item, int i) {
    // 添加__frch_前缀和i后缀
    return ITEM_PREFIX + item + "_" + i;
  }

(5)调用 applyltem()方法将item添加到DynamicContext.bindings集合中,供后续解析使用,applyltem()方法实现如下:

  private void applyItem(DynamicContext context, Object o, int i) {
    if (item != null) {
      // key为item, value是集合元素
      context.bind(item, o);
      // 为item添加前缀和后缀形成新的key
      context.bind(itemizeItem(item, i), o);
    }
  }

(6)转换子节点中的#{}占位符,此步骤会将PrefixedContext封装成FilteredDynamicContext,在追加子节点转换结果时,就会使用前面介绍的FilteredDynamicContext.apply()方法将#{}占位符转换成#{__frch_…}的格式。返回步骤3继续循环。

(7)循环结束后,调用DynarnicContext.appendSql()方法添加close指定的字符串。

ForEachSqlNode.apply()方法的具体代码如下所示:
@Override
  public boolean apply(DynamicContext context) {
    Map<String, Object> bindings = context.getBindings();
    // 步骤一:解析集合表达式,获取对应的实际参数
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
    if (!iterable.iterator().hasNext()) {
      return true;
    }
    boolean first = true;
    // 步骤二:在循环开始之前,添加 open 字段指定的字符串
    applyOpen(context);
    int i = 0;
    for (Object o : iterable) {
      DynamicContext oldContext = context;
      // 步骤三:创建PrefixedContext,并让context指向该PrefixedContext对象
      if (first || separator == null) {
        context = new PrefixedContext(context, "");
      } else {
        context = new PrefixedContext(context, separator);
      }
      // uniqueNumber从0开始,每次递增1,用于转换生成新的#{}占位符名称
      int uniqueNumber = context.getUniqueNumber();
      // Issue #709
      // 如果集合是Map类型,将集合key和value添加到DynamicContext.bindings集合中保存
      if (o instanceof Map.Entry) {
        @SuppressWarnings("unchecked")
        Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
        // 步骤4
        applyIndex(context, mapEntry.getKey(), uniqueNumber);
        // 步骤5
        applyItem(context, mapEntry.getValue(), uniqueNumber);
      } else {
        // 将集合索引和元素添加到DynamicContext.bindings集合中保存
        // 步骤4
        applyIndex(context, i, uniqueNumber);
        // 步骤5
        applyItem(context, o, uniqueNumber);
      }
      // 步骤6:调用子节点的apply()方法进行处理,这里使用的FilteredDynamicContext对象
      contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
      if (first) {
        first = !((PrefixedContext) context).isPrefixApplied();
      }
      context = oldContext;
      i++;
    }
    // 步骤7:循环结束后,调用DynarnicContext.appendSql()方法添加close指定的字符串
    applyClose(context);
    context.getBindings().remove(item);
    context.getBindings().remove(index);
    return true;
  }

ChooseSqlNode

如果在编写动态SQL语句时需要类似Java中的Switch语句的功能,可以考虑使用三个标签组合。MyBatis会将标签解析成ChooseSqINode,将标签解析成lfSqlNode,将标签解析成MixedSqINode。

ChooseSqlNode中各字段含义如下:

// 节点对应的SqlNode
  private final SqlNode defaultSqlNode;
  // 节点对应的IfSqlNode集合
  private final List<SqlNode> ifSqlNodes;

ChooseSqlNode.apply()方法的逻辑比较简单,首先遍历ifSqlNodes集合并调用其中SqlNode对象的apply()方法,然后根据前面的处理结果决定是否调用defaultSqlNode.apply()方法。

@Override
  public boolean apply(DynamicContext context) {
    // 遍历ifSqlNodes集合并调用其中的SqlNode对象的apply()方法
    for (SqlNode sqlNode : ifSqlNodes) {
      if (sqlNode.apply(context)) {
        return true;
      }
    }
    // 调用defaultSqlNode.apply()方法
    if (defaultSqlNode != null) {
      defaultSqlNode.apply(context);
      return true;
    }
    return false;
  }

**VarDeclSqlNode **

VarDeclSqlNode表示的是动态SQL语句中的节点,该节点可以从OGNL表达式中创建一个变量并将其记录到上下文中 。在VarDeclSqlNode通过name字段记录节点的name 属性值,expression宇段记录节点的value属性值。VarDeclSqlNode.apply()方法的实现也比较简单,具体实现如下:

 @Override
  public boolean apply(DynamicContext context) {
    // 解析OGNL表达式的值
    final Object value = OgnlCache.getValue(expression, context.getBindings());
    // 将name和表达式的值存入DynamicContext.bindings集合中
    context.bind(name, value);
    return true;
  }
3.SqlSourceBuilder

在经过SqlNode.apply()方法的解析之后,SQL语句会被传递到SqlSourceBuilder进行进一步的解析。SqlSourceBuilder主要完成了两方面的操作,一方面是解析SQL语句中的#{}占位符中定义的属性,格式类似于#{__frch_item_0, javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler},另一方面是将SQL语句中的#{}占位符替换成?占位符。SqlSourceBuilder是BaseBuilder子类之一,其核心逻辑位于 parse()方法中,具体实现如下:

 /**
   *
   * @param originalSql 经过SqlNode.apply()方法处理后的SQL语句
   * @param parameterType 用户传入的实参类型
   * @param additionalParameters 记录了形参与实参的对应关系,实际就是DynamicContext。bindings集合
   * @return
   */
  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    // 创建ParameterMappingTokenHandler对象,它是解析#{}占位符中的参数属性和替换占位符的核心
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    // 使用GenericTokenParser和ParameterMappingTokenHandler配合解析#{}占位符
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql = parser.parse(originalSql);
    // 创建StaticSqlSource,其中封装了占位符被替换成?的SQL语句以及参数对应的ParameterMappings集合
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

ParameterMappingTokenHandler也继承了BaseBuilder,其中各字段含义如下:

// 用于记录解析到的parameterMappings集合
    private List<ParameterMapping> parameterMappings = new ArrayList<>();
    // 参数类型
    private Class<?> parameterType;
    // DynamicContext.bindings集合对应的MeteObject对象
    private MetaObject metaParameters;

ParameterMapping中记录了#{}占位符的参数属性,其各字段的含义如下:

// 传入参数的name
  private String property;
  // 输入参数还是输出参数
  private ParameterMode mode;
  // 参数的java类型
  private Class<?> javaType = Object.class;
  // 参数的jdbc类型
  private JdbcType jdbcType;
  // 浮点数的精度
  private Integer numericScale;
  // 参数对应的TypeHandler
  private TypeHandler<?> typeHandler;
  // 参数对应ResultMap的id
  private String resultMapId;
  // 参数的jdbcTypeName属性
  private String jdbcTypeName;

ParameterMappingTokenHandler.handleToken()方法的实现会调用buildParameterMapping()方法解析参数属性,并将解析得到的ParameterMapping对象添加到parameterMappings 集合中,具体实现如下:

@Override
    public String handleToken(String content) {
      // 创建一个ParameterMapping对象,并添加到parameterMappings集合中保存
      parameterMappings.add(buildParameterMapping(content));
      // 返回?占位符
      return "?";
    }

    private ParameterMapping buildParameterMapping(String content) {
      // 解析参数的属性,并形成Map
      Map<String, String> propertiesMap = parseParameterMapping(content);
      // 获取参数名称
      String property = propertiesMap.get("property");
      Class<?> propertyType;
      // 确定Java类型
      if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
        propertyType = metaParameters.getGetterType(property);
      } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
        propertyType = parameterType;
      } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
        propertyType = java.sql.ResultSet.class;
      } else if (property == null || Map.class.isAssignableFrom(parameterType)) {
        propertyType = Object.class;
      } else {
        MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
        if (metaClass.hasGetter(property)) {
          propertyType = metaClass.getGetterType(property);
        } else {
          propertyType = Object.class;
        }
      }
      // 创建ParameterMapping的建造者,设置ParameterMapping相关配置
      ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
      Class<?> javaType = propertyType;
      String typeHandlerAlias = null;
      for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
        String name = entry.getKey();
        String value = entry.getValue();
        // 处理javaType、jdbcType、mode...等相关属性
        if ("javaType".equals(name)) {
          javaType = resolveClass(value);
          builder.javaType(javaType);
        } else if ("jdbcType".equals(name)) {
          builder.jdbcType(resolveJdbcType(value));
        } else if ("mode".equals(name)) {
          builder.mode(resolveParameterMode(value));
        } else if ("numericScale".equals(name)) {
          builder.numericScale(Integer.valueOf(value));
        } else if ("resultMap".equals(name)) {
          builder.resultMapId(value);
        } else if ("typeHandler".equals(name)) {
          typeHandlerAlias = value;
        } else if ("jdbcTypeName".equals(name)) {
          builder.jdbcTypeName(value);
        } else if ("property".equals(name)) {
          // Do Nothing
        } else if ("expression".equals(name)) {
          throw new BuilderException("Expression based parameters are not supported yet");
        } else {
          throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}.  Valid properties are " + PARAMETER_PROPERTIES);
        }
      }
      // 获取TypeHandler对象
      if (typeHandlerAlias != null) {
        builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
      }
      // 创建ParameterMapping对象,如果没有指定TypeHandler,则会根据JavaType和jdbcType从TypeHandlerRegistry中获取对应的TypeHandler对象
      return builder.build();
    }

之后SqlSourceBuilder会将上述SQL语句以及parameterMappings集合封装成StaticSqlSource对象StaticSqlSource.getBoundSql()方法的实现比较简单,它直接创建井返回BoundSql对象,该BoundSql对象也就是DynamicSqlSource返回的BoundSql对象。

@Override
  public BoundSql getBoundSql(Object parameterObject) {
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }
 

BoundSql核心字段的含义如下:

// 该字段记录了SQL语句,可能含有?占位符
  private final String sql;
  // SQL中的参数属性集合,ParameterMapping的集合
  private final List<ParameterMapping> parameterMappings;
  // 客户端执行SQL时传入的实际参数
  private final Object parameterObject;
  // 空的HashMap集合,之后会复制DynamicContext.bindings集合中的内容
  private final Map<String, Object> additionalParameters;
  // additionalParameters集合对应的MetaObject对象
  private final MetaObject metaParameters;
4.DynamicSqlSource

DynamicSq!Source 负责解析动态 SQL 语句,也是最常用的 SqI Source 实现之一。 SqIN ode 中使

用了组合模式 成了 个树状结构, DynamicSq!Source 中使用 rootSq!Node 宇段( Sq!Node

型)记录了待解析的 Sq!Node 树的根节点。

DynamicSq!Source.getBoundSql()方法的具体实现如下:

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    // 创建DynamicContext对象,parameterObject是用户传入的实参
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 通过调用rootSqlNode.apply()方法调用整个树形结构中全部SqlNode.apply()方法
    rootSqlNode.apply(context);
    // 创建SqlSourceBuilder,解析参数属性,并将SQL语句中的#{}占位符替换成?占位符
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    // 创建BoundSql对象 ,并将DynamicContext.bindings中的参数信息复制到其additionalParameters集合中保存
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }
5.RawSqlSource

RawSqlSource在构造方法中首先会调用getSql()方法,其中通过调用SqINode.apply()方法完成SQL语句的拼装和初步处理之后会使用Sq!SourceBuilder完成占位符的替换和ParameterMapping集合的创建,井返回StaticSqlSource对象。

下面简单介绍一下RawSqlSource的实现:

public class RawSqlSource implements SqlSource {

  // StaticSqlSource对象
  private final SqlSource sqlSource;

  public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    // 调用getSql()方法,完成SQL的拼装和初步解析
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
  }

  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    // 通过SqlSourceBuilder完成占位符的解析和替换操作
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    // 返回StaticSqlSource
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
  }

  private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    DynamicContext context = new DynamicContext(configuration, null);
    rootSqlNode.apply(context);
    return context.getSql();
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    return sqlSource.getBoundSql(parameterObject);
  }

}

你可能感兴趣的:(Mybatis)