平时我们基于 MyBaits 框架进行编写的 Mapper.xml 中每一个 insert/update/delete/select
标签里面的每一行 SQL(包括 include 标签被替换成 SQL ) 文本被抽象为 SqlNode。
#{}
占位符,不包含任何动态 SQL 语句(包含 ${}
占位符 )${}
占位符;insert/update/delete/select
标签的 SQL 文本不止一行,则把所有的 SqlNode 组装在一起的 SqlNode。
SqlNode 接口只定义了一个 boolean apply(DynamicContext context)
方法,通过 DynamicContext 对象把各个 SqlNode 组装成一条完整的 SQL 语句。
DynamicContext 就像上图串串的竹签,而 SqlNode 就是竹签上一块块肉肉,一个竹签上的所有肉肉就是 MixedSqlNode,通过竹签把肉肉串在一起,就组成了美味的烧烤——SQL!!烧烤怎么少了佐料,就如 SQL 语句怎么少了参数呢?参数保存在 DynamicContext 中 bindings 字段中。通过 getSql() 方法获取 StringJoiner 拼接 SQL 语句。
由于不包含任何动态 SQL 所以不依赖实参来拼接 SQL 语句
public class StaticTextSqlNodeDemo { public static void main(String[] args) { Configuration configuration = new Configuration(); SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user "); DynamicContext dynamicContext = new DynamicContext(configuration, null); staticTextSqlNode.apply(dynamicContext); String sql = dynamicContext.getSql(); System.out.println(sql); } }
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; } }
StaticTextSqlNode 源码非常简单就是把 SQL 语句通过 DynamicContext 的 appendSql() 方法拼接在之前的 SQL 语句后面。
由于 SQL 语句中含有
${}
占位符,要解析占位符所以需要参数。
public class TextSqlNodeDemo { public static void main(String[] args) { Configuration configuration = new Configuration(); Map<String, Object> paraMap = new HashMap<>(); // 把注释放放开并把下面put 方法注解之后会发现解析 ${} 占位符的值为空字符串 // Map<String, Object> paraMap = null; paraMap.put("user", "user"); // paraMap.put("user", "'user'"); SqlNode textSqlNode = new TextSqlNode("SELECT * FROM ${user}"); DynamicContext dynamicContext = new DynamicContext(configuration, paraMap); textSqlNode.apply(dynamicContext); String sql = dynamicContext.getSql(); System.out.println(sql); } }
@Override public boolean apply(DynamicContext context) { // 通过 createParse 获取 GenericTokenParser 对象(主要是解决 ${} 占位符)。 // 如果发现 ${} 占位符则通过 BindingTokenParser 的 handleToken(String) 方法返回值替换 ${} 占位符 GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter)); context.appendSql(parser.parse(text)); return true; } @Override public String handleToken(String content) { // 通过 DynamicContext 获取实参 Object parameter = context.getBindings().get("_parameter"); if (parameter == null) { context.getBindings().put("value", null); } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) { // SimpleTypeRegistry 中 SIMPLE_TYPE_SET 包含的类则存在 DynamicContext 参数中 context.getBindings().put("value", parameter); } // 通过 OGNL 从实参中获取 ${} 占位符的值 Object value = OgnlCache.getValue(content, context.getBindings()); String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null" checkInjection(srtValue); return srtValue; }
if/when 子标签里面的 SQL 语句抽象,只要 if 标签里面的 test 表达式为 true 时才拼接 if 标签里面的 SQL 语句。
public class IfSqlNodeDemo { public static void main(String[] args) { Configuration configuration = new Configuration(); // 实参对象 Map<String, Object> paraMap = new HashMap<>(); paraMap.put("user", "user"); SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user"); // 构建 IfSqlNode 对象,传入 if 标签里面的 SQL 抽象和 test 表达式 SqlNode ifSqlNode = new IfSqlNode(staticTextSqlNode, "user != null"); DynamicContext dynamicContext = new DynamicContext(configuration, paraMap); // 通过 DynamicContext 拼接 SQL ifSqlNode.apply(dynamicContext); // 获取 SQL 语句 String sql = dynamicContext.getSql(); // 控制台输出 System.out.println(sql); } }
@Override public boolean apply(DynamicContext context) { // 通过 OGNL 判断 test 表达式是否成立,表达式里面涉及的属性值通过 // DynamicContext 传入的实参获取。如果成立折拼接 SQL 语句 if (evaluator.evaluateBoolean(test, context.getBindings())) { contents.apply(context); return true; } return false; }
choose 子标签里面的 SQL 语句抽象,当 when 标签里面的 test 表达式成立时才会拼接里面的 SQL 语句,否则取 otherwise 标签里面的 SQL 语句。类似于 Java 里面的 if… else if…else 语句,只执行一个分支逻辑。
public class ChooseSqlNodeDemo { public static void main(String[] args) { Configuration configuration = new Configuration(); // 实参对象 Map<String, Object> paraMap = new HashMap<>(); paraMap.put("name", "文海"); SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user WHERE 1 = 1"); // 构建 IfSqlNode 对象,传入 if 标签里面的 SQL 抽象和 test 表达式 SqlNode ifSqlNode = new IfSqlNode(new StaticTextSqlNode(" AND name = #{name}"), "name != null"); SqlNode defaultSqlNode = new StaticTextSqlNode(" AND name = 'wenhai'"); DynamicContext dynamicContext = new DynamicContext(configuration, paraMap); // 通过 DynamicContext 拼接 SQL staticTextSqlNode.apply(dynamicContext); // 通过 DynamicContext 拼接 SQL ChooseSqlNode chooseSqlNode = new ChooseSqlNode(Collections.singletonList(ifSqlNode), defaultSqlNode); chooseSqlNode.apply(dynamicContext); // 获取 SQL 语句 String sql = dynamicContext.getSql(); // 控制台输出 System.out.println(sql); } }
// 通过构造函数传入 when 标签 SQL 抽象和 otherwise 标签的 SQL 抽象 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; } } // 前面的 when 标签里面的表达式都不满足,并且有兜底的 otherwise 标签则拼接里面的 SQL if (defaultSqlNode != null) { defaultSqlNode.apply(context); return true; } return false; }
foreach 子标签里面的 SQL 抽象,可以通过标签里面的 item 和 index 设置的变量获取对应的值。index 是数组以及集合的索引值而 Map 类型则是 key 里面的值,item 则是数组以及集合里面的元素而 Map 类型则是 value 里面的值。
public class ForeachSqlNodeDemo { public static void main(String[] args) { Configuration configuration = new Configuration(); // 实参对象 Map<String, Object> paraMap = new HashMap<>(); // Map<String, String> param = new HashMap<>(); // param.put("wenhai", "文海"); // param.put("wenhai2", "文海2"); // paraMap.put("map", param); List<String> list = new ArrayList<>(); list.add("wenhai"); list.add("wenhai2"); paraMap.put("list", list); DynamicContext dynamicContext = new DynamicContext(configuration, paraMap); SqlNode staticTextSqlNode = new StaticTextSqlNode("SELECT * FROM user WHERE name in"); // 通过 DynamicContext 拼接 SQL staticTextSqlNode.apply(dynamicContext); // String collection = "map"; String collection = "list"; String item = "item"; String index = "index"; String open = "("; String close = ")"; String separator = ","; ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, new StaticTextSqlNode("#{index}"), collection, index, item, open, close, separator); forEachSqlNode.apply(dynamicContext); // 获取 SQL 语句 String sql = dynamicContext.getSql(); // 控制台输出 :SELECT * FROM user WHERE name in ( #{__frch_index_0} , #{__frch_index_1} ) // 同时 DynamicContext 里面的 _parameter 多出以 __frch_#index_n 和 __frch_#item_n 属性值 // 便于后续通过 System.out.println(sql); } }
/** * ForEachSqlNode 构造函数 * * @param configuration 全局 Configuration 对象 * @param contents foreach 标签里面的 SQL 抽象 * @param collectionExpression foreach 标签里面的 collection 属性值 * @param index foreach 标签里面的 index 属性值 * @param item foreach 标签里面的 item 属性值 * @param open foreach 标签里面的 open 属性值 * @param close foreach 标签里面的 close 属性值 * @param separator foreach 标签里面的 separator 属性值 */ 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; } @Override public boolean apply(DynamicContext context) { // 获取参数列表 Map<String, Object> bindings = context.getBindings(); // 通过 OGNL 获取 collectionExpression 表达式的值,该值不能为 null, // 只能是 Iterable 实例和数组已经 Map 实例,其他都会报错 final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings); if (!iterable.iterator().hasNext()) { return true; } // 是否是第一次,第一次不用拼接 separator 值 boolean first = true; // 如果设置了 open 属性值,则先拼接 open 属性值 applyOpen(context); int i = 0; for (Object o : iterable) { DynamicContext oldContext = context; // 如果是第一次或者是分隔符没有设置则通过 PrefixedContext 包装 DynamicContext 对象 // 在 appendSql 方法进行拼接 SQL 时候加上设置的前缀(此处就是 “”) if (first || separator == null) { context = new PrefixedContext(context, ""); } else { context = new PrefixedContext(context, separator); } // 获取唯一序列号递增用于集合的索引 int uniqueNumber = context.getUniqueNumber(); // 为 DynamicContext 中的类型为 ContextMap 属性保存 foreach 遍历对应的值 // 以 __frch_#{index}_uniqueNumber 和 __frch_#{item}_uniqueNumber 为 key if (o instanceof Map.Entry) { @SuppressWarnings("unchecked") Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o; applyIndex(context, mapEntry.getKey(), uniqueNumber); applyItem(context, mapEntry.getValue(), uniqueNumber); } else { applyIndex(context, i, uniqueNumber); applyItem(context, o, uniqueNumber); } // 通过 FilteredDynamicContext 包装 PrefixedContext 替换 foreach 标签里面 // 以 #{} 占位符并且使用正则表达式匹配 item 以及 index 属性值为 __frch_#{index}_uniqueNumber 和 __frch_#{item}_uniqueNumber contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber)); if (first) { first = !((PrefixedContext) context).isPrefixApplied(); } context = oldContext; i++; } // 如果 foreach 标签里面的 close 属性设置了则拼接在 SQL 语句后面 applyClose(context); context.getBindings().remove(item); context.getBindings().remove(index); return true; }
剩余的 SqlNode 就不分析了都是类似,通过包装 DynamicContext 以达到效果。
此节分析了 Mapper.xml 中的 SQL 语句抽象为 SqlNode,通过实参传递给 DynamicContext 来动态拼接 SQL 语句,为后面学习 SqlSource 打下坚实的基础。