简介
平时我们基于 MyBaits 框架进行编写的 Mapper.xml 中每一个 insert/update/delete/select
标签里面的每一行 SQL(包括 include 标签被替换成 SQL ) 文本被抽象为 SqlNode。
SqlNode 分类
- StaticTextSqlNode:纯 SQL 语句和
#{}
占位符,不包含任何动态 SQL 语句(包含${}
占位符 ) - TextSqlNode: SQL 语句中含有
${}
占位符; - IfSqlNode:if/when 子标签里面的 SQL 语句;
- ChooseSqlNode:choose 子标签里面的 SQL 语句;
- ForEachSqlNode:foreach 子标签里面的 SQL 语句;
- VarDecSqlNode:bind 子标签里面的 SQL 语句;
- TrimSqlNode:trim 子标签里面的 SQL 语句;
- WhereSqlNode:where 子标签里面的 SQL 语句;
- SetSqlNode:set 子标签里面的 SQL 语句;
- MixedSqlNode: 如果
insert/update/delete/select
标签的 SQL 文本不止一行,则把所有的 SqlNode 组装在一起的 SqlNode。
类图
SqlNode 接口只定义了一个 boolean apply(DynamicContext context)
方法,通过 DynamicContext 对象把各个 SqlNode 组装成一条完整的 SQL 语句。
DynamicContext
DynamicContext 就像上图串串的竹签,而 SqlNode 就是竹签上一块块肉肉,一个竹签上的所有肉肉就是 MixedSqlNode,通过竹签把肉肉串在一起,就组成了美味的烧烤——SQL!!烧烤怎么少了佐料,就如 SQL 语句怎么少了参数呢?参数保存在 DynamicContext 中 bindings 字段中。通过 getSql() 方法获取 StringJoiner 拼接 SQL 语句。
源码解读
StaticTextSqlNode
由于不包含任何动态 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 语句后面。
TextSqlNode
由于 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;
}
IfSqlNode
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;
}
ChooseSqlNode
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;
}
ForEachSqlNode
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 打下坚实的基础。