Mybatis SqlNode

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 我们在以前的文章中曾经介绍过 OGNL 强大的表达式引擎

网络异常,图片无法展示
|

我们在以前的文章中曾经介绍过 OGNL 强大的表达式引擎

我们知道在 BaseExecutor#query 中

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameter);
  CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
复制代码


我们会通过 MappedStatement 获取解释之后的 Sql

public BoundSql getBoundSql(Object parameterObject) {
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings == null || parameterMappings.isEmpty()) {
    boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
  }
  // check for nested result maps in parameter mappings (issue #30)
  for (ParameterMapping pm : boundSql.getParameterMappings()) {
    String rmId = pm.getResultMapId();
    if (rmId != null) {
      ResultMap rm = configuration.getResultMap(rmId);
      if (rm != null) {
        hasNestedResultMaps |= rm.hasNestedResultMaps();
      }
    }
  }
  return boundSql;
}
复制代码


Mybatis 中存在两种 SqlSource

public SqlSource parseScriptNode() {
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource;
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}
复制代码

一种是动态 Sql 一种是静态 Sql

对于静态 Sql、不需要解释和处理

// StaticSqlSource
@Override
public BoundSql getBoundSql(Object parameterObject) {
  return new BoundSql(configuration, sql, parameterMappings, parameterObject);
}
复制代码


动态 Sql

@Override
public BoundSql getBoundSql(Object parameterObject) {
  DynamicContext context = new DynamicContext(configuration, parameterObject);
  rootSqlNode.apply(context);
  SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
  Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
  SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  context.getBindings().forEach(boundSql::setAdditionalParameter);
  return boundSql;
}
复制代码


DynamicContext


public class DynamicContext {
  public static final String PARAMETER_OBJECT_KEY = "_parameter";
  public static final String DATABASE_ID_KEY = "_databaseId";
  static {
    OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
  }
  private final ContextMap bindings;
  private final StringJoiner sqlBuilder = new StringJoiner(" ");
  private int uniqueNumber = 0;
  ......
复制代码

主要存在两个成员变量 sqlBuilder 的类型为 StringJoiner 、JDK 类不再介绍


主要看看 bindings

static class ContextMap extends HashMap<String, Object> {
  private static final long serialVersionUID = 2977601501966151582L;
  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;
    if (super.containsKey(strKey)) {
      return super.get(strKey);
    }
    if (parameterMetaObject == null) {
      return null;
    }
    if (fallbackParameterObject && !parameterMetaObject.hasGetter(strKey)) {
      return parameterMetaObject.getOriginalObject();
    } else {
      // issue #61 do not modify the context when reading
      return parameterMetaObject.getValue(strKey);
    }
  }
}
复制代码

是 HashMap 、重写了 get 方法。

  • 首先,尝试按照 Map 的规则查找 Key,如果查找成功直接返回;
  • 如果 parameterMetaObject 为 null 直接返回 null
  • 如果 parameterMetaObject 不为 null 、并且 fallbackParameterObject  为 false 或者 parameterMetaObject 有这个属性的 get 方法、那么就调用 getValue 尝试获取值
  • 如果 parameterMetaObject 不为 null、并且 fallbackParameterObject 为 true (其实就是查询的参数的类型存在类型转换器)、并且不存在 get 方法、那么就直接返回这个查询参数


创建 ContextMap 在 DynamicContext 构造函数中

public DynamicContext(Configuration configuration, Object parameterObject) {
  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);
  }
  // 这里实参对应的Key是_parameter
  bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
  bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
复制代码
  • 对于非Map类型的实参,会创建对应的MetaObject对象,并封装成ContextMap对象
  • 对于Map类型的实参,这里会创建一个空的ContextMap对象


SqlNode


创建玩 DynamicContext 之后、就会调用 SqlNode 的 apply 方法

rootSqlNode.apply(context);
复制代码

网络异常,图片无法展示
|

public interface SqlNode {
  boolean apply(DynamicContext context);
}
复制代码

apply 方法会根据用户传入的参数、解析 SqlNode 所表示的动态 SQL 内容并将解释之后的 Sql 片段追加到 Dynamic 中的 StringJoiner 类型的变量上、当 SQL 中全部动态片段都解释完成之后、就可以从 DynamicContext 中 StringJoiner 中获取到一条完整的 SQL


StaticTextSqlNode


用于表示非动态的 SQL 片段、啥都不用干、直接将对应的 Sql 字符串拼接到 context 中保存

public class StaticTextSqlNode implements SqlNode {
  private final String text;
  public StaticTextSqlNode(String text) {
    this.text = text;
  }
  @Override
  public boolean apply(DynamicContext context) {
    context.appendSql(text);
    return true;
  }
}
复制代码


MixedSqlNode


在整个 SqlNode 中充当了树枝节点、主要作用就是组织聚合其他 SqlNode。

public class MixedSqlNode implements SqlNode {
  private final List<SqlNode> contents;
  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }
  @Override
  public boolean apply(DynamicContext context) {
    contents.forEach(node -> node.apply(context));
    return true;
  }
}
复制代码

apply 方法直接遍历集合中的 SqlNode 的 apply 方法。


TextSqlNode


包含了 占位符的动态SQL片段、在apply方法中集合用户给定的参数解释{} 占位符的动态 SQL 片段、在 apply 方法中集合用户给定的参数解释 SQLapply{} 占位符

@Override
public boolean apply(DynamicContext context) {
  GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
  context.appendSql(parser.parse(text));
  return true;
}
复制代码

使用 GenericTokenParser 识别 ${} 占位符、在识别到占位符之后、会通过 BindingTokenParser 将占位符替换为用户传入的参数。

private GenericTokenParser createParser(TokenHandler handler) {
  return new GenericTokenParser("${", "}", handler);
}
复制代码
public String parse(String text) {
  if (text == null || text.isEmpty()) {
    return "";
  }
  // search open token
  int start = text.indexOf(openToken);
  if (start == -1) {
    return text;
  }
  char[] src = text.toCharArray();
  int offset = 0;
  final StringBuilder builder = new StringBuilder();
  StringBuilder expression = null;
  do {
    if (start > 0 && src[start - 1] == '\\') {
      // this open token is escaped. remove the backslash and continue.
      builder.append(src, offset, start - offset - 1).append(openToken);
      offset = start + openToken.length();
    } else {
      // found open token. let's search close token.
      if (expression == null) {
        expression = new StringBuilder();
      } else {
        expression.setLength(0);
      }
      builder.append(src, offset, start - offset);
      offset = start + openToken.length();
      int end = text.indexOf(closeToken, offset);
      while (end > -1) {
        if (end > offset && src[end - 1] == '\\') {
          // this close token is escaped. remove the backslash and continue.
          expression.append(src, offset, end - offset - 1).append(closeToken);
          offset = end + closeToken.length();
          end = text.indexOf(closeToken, offset);
        } else {
          expression.append(src, offset, end - offset);
          break;
        }
      }
      if (end == -1) {
        // close token was not found.
        builder.append(src, start, src.length - start);
        offset = src.length;
      } else {
        builder.append(handler.handleToken(expression.toString()));
        offset = end + closeToken.length();
      }
    }
    start = text.indexOf(openToken, offset);
  } while (start > -1);
  if (offset < src.length) {
    builder.append(src, offset, src.length - offset);
  }
  return builder.toString();
}
复制代码


parse 方法会调用 handleToken 方法

private static class BindingTokenParser implements TokenHandler {
  private DynamicContext context;
  private Pattern injectionFilter;
  public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
    this.context = context;
    this.injectionFilter = injectionFilter;
  }
  @Override
  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);
    }
    Object value = OgnlCache.getValue(content, context.getBindings());
    String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
    checkInjection(srtValue);
    return srtValue;
  }
  private void checkInjection(String value) {
    if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
      throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
    }
  }
}
复制代码


IfSqlNode


对应的是 <if> 标签、在 Mybatis 中、使用 <if> 标签可以通过 test 属性置顶一个表达式、当表达式成立时、<if> 标签内的 SQL 片段才会出现在完整的 SQL 语句中

public class IfSqlNode implements SqlNode {
  private final ExpressionEvaluator evaluator;
  private final String test;
  private final SqlNode contents;
  public IfSqlNode(SqlNode contents, String test) {
    this.test = test;
    this.contents = contents;
    this.evaluator = new ExpressionEvaluator();
  }
  @Override
  public boolean apply(DynamicContext context) {
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }
}
复制代码

在 IfSqlNode 中的 apply 方法实现中、会依赖 ExpressionEvaluator 工具类解释 test 表达式、只有 test 表达式为 true 、才会调用子 SqlNode 的 apply 方法。ExpressionEvaluator 底层也是依赖 OGNL 实现 test 表达式解释的。


TrimSqlNode


在使用 <trim> 标签的时候、我们可以指定 prefix 和 suffix 属性添加前缀和后缀、也可以指定 prefixesToOverride 和 suffixesToOverride 属性来删除多个前缀和后缀(使用|来分割不同字符串)

public class TrimSqlNode implements SqlNode {
  private final SqlNode contents;
  private final String prefix;
  private final String suffix;
  private final List<String> prefixesToOverride;
  private final List<String> suffixesToOverride;
  private final Configuration configuration;
  public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
    this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
  }
  protected TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, List<String> prefixesToOverride, String suffix, List<String> suffixesToOverride) {
    this.contents = contents;
    this.prefix = prefix;
    this.prefixesToOverride = prefixesToOverride;
    this.suffix = suffix;
    this.suffixesToOverride = suffixesToOverride;
    this.configuration = configuration;
  }
  @Override
  public boolean apply(DynamicContext context) {
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    // 首先执行子SqlNode对象的apply()方法完成对应动态SQL片段的解析
    boolean result = contents.apply(filteredDynamicContext);
    // 使用FilteredDynamicContext.applyAll()方法完成前后缀的处理操作
    filteredDynamicContext.applyAll();
    return result;
  }
  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();
  }
  .......
复制代码


FilteredDynamicContext  可以看作是 DynamicContext 的装饰器、额外添加了处理前缀和后缀的功能

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.appendSql(sqlBuffer.toString());
}
复制代码
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
  if (!prefixApplied) {
    prefixApplied = true;
    if (prefixesToOverride != null) {
      for (String toRemove : prefixesToOverride) {
        if (trimmedUppercaseSql.startsWith(toRemove)) {
          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())) {
          int start = sql.length() - toRemove.trim().length();
          int end = sql.length();
          sql.delete(start, end);
          break;
        }
      }
    }
    if (suffix != null) {
      sql.append(" ");
      sql.append(suffix);
    }
  }
}
复制代码
  • applyPrefix() 方法在处理前缀的时候,首先会遍历 prefixesToOverride 集合,从 SQL 片段的头部逐个尝试进行删除,之后在 SQL 片段的头部插入一个空格以及 prefix 字段指定的前缀字符串。
  • applySuffix() 方法在处理后缀的时候,首先会遍历 suffixesToOverride 集合,从 SQL 片段的尾部逐个尝试进行删除,之后在 SQL 片段的尾部插入一个空格以及 suffix 字段指定的后缀字符串。


WhereSqlNode


public class WhereSqlNode extends TrimSqlNode {
  private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
  public WhereSqlNode(Configuration configuration, SqlNode contents) {
    super(configuration, contents, "WHERE", prefixList, null, null);
  }
}
复制代码

作为 TrimSqlNode 的子类、在 WhereSqlNode 中将 prefix 设置为“WHERE”字符串,prefixesToOverride 集合包含 “OR”“AND”“OR\n”“AND\n”“OR\r”“AND\r” 等字符串,这样就实现了删除 SQL 片段开头多余的 “AND”“OR” 关键字,并添加“WHERE”关键字的效果。


SetSqlNode


public class SetSqlNode extends TrimSqlNode {
  private static final List<String> COMMA = Collections.singletonList(",");
  public SetSqlNode(Configuration configuration,SqlNode contents) {
    super(configuration, contents, "SET", COMMA, null, COMMA);
  }
}
复制代码

在 SetSqlNode 中将 prefix 设置为“SET”关键字,prefixesToOverride 集合和 suffixesToOverride 集合只包含“,”(逗号)字符串,这样就实现了删除 SQL 片段开头和结尾多余的逗号,并添加“SET”关键字的效果。


ForEachSqlNode


在动态 SQL 语句中、我们可以使用  标签对一个集合进行迭代。我们可以通过 index 属性指定元素的下标索引(迭代 Map 集合的话、就是 key 值)、使用 item 属性置顶变量作为集合元素(迭代 Map 集合的话、就是 Value 值)。另外我们还可以通过 open 和 close 属性在迭代开始前和结束后添加相应的字符串、也允许使用 separator 属性自定义分隔符。

public class ForEachSqlNode implements SqlNode {
  public static final String ITEM_PREFIX = "__frch_";
  private final ExpressionEvaluator evaluator;
  private final String collectionExpression;
  private final SqlNode contents;
  private final String open;
  private final String close;
  private final String separator;
  private final String item;
  private final String index;
  private final Configuration configuration;
  public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
    this.evaluator = new ExpressionEvaluator();
    this.collectionExpression = collectionExpression;
    this.contents = contents;
    this.open = open;
    this.close = close;
    this.separator = separator;
    this.index = index;
    this.item = item;
    this.configuration = configuration;
  }
  ........
复制代码


ChooseSqlNode


在有的业务场景中,可能会碰到非常多的分支判断,在 Java 中,我们可以通过 switch...case...default 的方式来编写这段代码;在 MyBatis 的动态 SQL 语句中,我们可以使用 、 和  三个标签来实现类似的效果。

标签会被 MyBatis 解析成 ChooseSqlNode 对象, 标签会被解析成 IfSqlNode 对象, 标签会被解析成 MixedSqlNode 对象。

public class ChooseSqlNode implements SqlNode {
  private final SqlNode defaultSqlNode;
  private final List<SqlNode> ifSqlNodes;
  public ChooseSqlNode(List<SqlNode> ifSqlNodes, SqlNode defaultSqlNode) {
    this.ifSqlNodes = ifSqlNodes;
    this.defaultSqlNode = defaultSqlNode;
  }
  @Override
  public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : ifSqlNodes) {
      if (sqlNode.apply(context)) {
        return true;
      }
    }
    if (defaultSqlNode != null) {
      defaultSqlNode.apply(context);
      return true;
    }
    return false;
  }
}
复制代码


VarDeclSqlNode


这个不太常用、

VarDeclSqlNode 抽象了  标签,其核心功能是将一个 OGNL 表达式的值绑定到一个指定的变量名上,并记录到 DynamicContext 上下文中。

VarDeclSqlNode 中的 name 字段维护了  标签中 name 属性的值,expression 字段记录了  标签中 value 属性的值(一般是一个 OGNL 表达式)。

在 apply() 方法中,VarDeclSqlNode 首先会通过 OGNL 工具类解析 expression 这个表达式的值,然后将解析结果与 name 字段的值一起绑定到 DynamicContext 上下文中,这样后面就可以通过 name 字段值获取这个表达式的值了。

public class VarDeclSqlNode implements SqlNode {
  private final String name;
  private final String expression;
  public VarDeclSqlNode(String var, String exp) {
    name = var;
    expression = exp;
  }
  @Override
  public boolean apply(DynamicContext context) {
    final Object value = OgnlCache.getValue(expression, context.getBindings());
    context.bind(name, value);
    return true;
  }
}


目录
相关文章
|
4月前
|
缓存 Java 数据库连接
Mybatis
Mybatis
35 0
|
2月前
|
算法 Java 数据库连接
mybatis plus 主键策略
mybatis plus 主键策略
31 2
|
4月前
|
Java 关系型数据库 数据库连接
Mybatis-plus
Mybatis-plus
|
1月前
|
SQL Java 数据库连接
Mybatis02(一)
Mybatis02(一)
20 0
|
4月前
|
Java 数据库连接 数据库
mybatis的@MappedTypes
mybatis的@MappedTypes
85 1
|
4月前
|
SQL 缓存 Java
|
9月前
|
SQL 安全 Java
Mybatis中# 和 $ 的使用详解
Mybatis中# 和 $ 的使用详解
145 0
|
SQL Java 数据库连接
|
SQL XML 存储
|
SQL Java 数据库连接
跟我一起学mybatis
跟我一起学mybatis
209 0
跟我一起学mybatis