组合( Composite )模式就是把对象组合成树形结构,以表示“部分-整体”的层次结构,用户可以像处理一个简单对象一样来处理一个复杂对象,从而使得调用者无需了解复杂元素的内部结构。
组合模式中的角色有:
具体组合模式的例子可以参考 设计模式整理
现在我们来说一下SqlNode是什么,来看这么一段配置文件
<select id="findByGameTypeCount" resultType="java.lang.Long">
select count(*)
from betdetails a inner join UserBetOrder b on a.orderId = b.id
<where>
<if test="gameType != null and gameType > 0">
a.gameType = #{gameType} and
</if>
<if test="currDrawno != null">
b.currentDrawno = #{currDrawno} and
</if>
<if test="orderId != null and orderId > 0">
a.orderId = #{orderId} and
</if>
<if test="status != null and status >= 0">
a.status = #{status} and
</if>
<if test="userId != null and userId > 0">
b.userId = #{userId} and
</if>
<if test="start != null">
a.createTime >= #{start} and
</if>
<if test="end != null">
a.createTime <= #{end} and
</if>
1 = 1
</where>
</select>
<insert id="insertBetdetailsByBatch" parameterType="java.util.List">
insert into betdetails(id,orderId,actorIndex,createTime,ballIndex,ballValue,betAmount,rate1,rate2,rate3,gameType,status,betResult,awardAmount,ballName) values
<foreach collection="list" item="item" index="index" separator=",">
(#{item.id},#{item.orderId},#{item.actorIndex},#{item.createTime},#{item.ballIndex},#{item.ballValue},#{item.betAmount},#{item.rate1},#{item.rate2},#{item.rate3},#{item.gameType},#{item.status},#{item.betResult},#{item.awardAmount},#{item.ballName})
</foreach>
</insert>
这其中的<if><where><foreach>节点就是SqlNode节点,SqlNode是一个接口,代表着组合模式中的容器。只要是有SqlNode,那就代表着一定是一个动态的SQL,里面就有可能会有参数#{}
public interface SqlNode {
//SqlNode接口中定义的唯一方法,该方法会根据用户传入的实参,解析该SqlNode所记录的动态SQL节点,并调用DynamicContext.appendSql()方法将解析后的SQL片段追加到
//DynamicContext.sqlBuilder中保存
//当SQL节点下的所有SqlNode完成解析后,就可以从DynamicContext中获取一条动态生成的完整的SQL语句
boolean apply(DynamicContext context);
}
我们先来看一下DynamicContext是什么,它的核心字段如下
private final ContextMap bindings; //参考上下文
//在SqlNode解析动态SQL时,会将解析后的SQL语句片段添加到该属性中保存,最终拼凑出一条完成的SQL语句
private final StringBuilder sqlBuilder = new StringBuilder();
ContextMap是一个内部类,继承于HashMap,重写了get方法
static class ContextMap extends HashMap<String, Object> {
private static final long serialVersionUID = 2977601501966151582L;
//将用户传入的参数封装成MetaObject对象(类实例中检查类的属性是否包含getter,setter方法)
private MetaObject parameterMetaObject;
public ContextMap(MetaObject parameterMetaObject) {
this.parameterMetaObject = parameterMetaObject;
}
@Override
public Object get(Object key) {
String strKey = (String) key;
//如果ContextMap中已经包含了该key,则直接返回
if (super.containsKey(strKey)) {
return super.get(strKey);
}
//如果不包含该key,从parameterMetaObject中查找对应属性
if (parameterMetaObject != null) {
// issue #61 do not modify the context when reading
return parameterMetaObject.getValue(strKey);
}
return null;
}
}
public void appendSql(String sql) {
sqlBuilder.append(sql);
sqlBuilder.append(" ");
}
public void bind(String name, Object value) {
bindings.put(name, value);
}
SqlNode的实现类如下
其中MixedSqlNode是树枝,TextSqlNode是树叶....
我们先来看一下TextSqlNode,TextSqlNode表示的是包含${}占位符的动态SQL节点。它的接口实现方法如下
@Override
public boolean apply(DynamicContext context) {
//将动态SQL(带${}占位符的SQL)解析成完成SQL语句的解析器,即将${}占位符替换成实际的变量值
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
//将解析后的SQL片段添加到DynamicContext中
context.appendSql(parser.parse(text));
return true;
}
BindingTokenParser是TextNode中定义的内部类,继承了TokenHandler接口,它的主要作用是根据DynamicContext.bindings集合中的信息解析SQL语句节点中的${}占位符。
private DynamicContext context;
private Pattern injectionFilter; //需要匹配的正则表达式
@Override
public String handleToken(String content) {
//获取用户提供的实参
Object parameter = context.getBindings().get("_parameter");
//如果实参为null
if (parameter == null) {
//将参考上下文的value key设为null
context.getBindings().put("value", null);
//如果实参是一个常用数据类型的类(Integer.class,String.class,Byte.class等等)
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
//将参考上下文的value key设为该实参
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;
}
private void checkInjection(String value) {
if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
}
}
在OgnlCache中,对原生的OGNL进行了封装。OGNL表达式的解析过程是比较耗时的,为了提高效率,OgnlCache中使用了expressionCashe字段(ConcurrentHashMap<String,Object>类型)对解析后的OGNL表达式进行缓存。为了说明OGNL,我们先来看一个例子
@Data
@ToString
public class User {
private int id;
private String name;
}
public class OGNLDemo {
public void testOgnl1() throws OgnlException {
OgnlContext context = new OgnlContext();
context.put("cn","China");
String value = (String) context.get("cn");
System.out.println(value);
User user = new User();
user.setId(100);
user.setName("Jack");
context.put("user",user);
Object u = context.get("user");
System.out.println(u);
Object ognl = Ognl.parseExpression("#user.id");
Object value1 = Ognl.getValue(ognl,context,context.getRoot());
System.out.println(value1);
User user1 = new User();
user1.setId(200);
user1.setName("Mark");
context.setRoot(user1);
Object ognl1 = Ognl.parseExpression("id");
Object value2 = Ognl.getValue(ognl1,context,context.getRoot());
System.out.println(value2);
Object ognl2 = Ognl.parseExpression("@@floor(10.9)");
Object value3 = Ognl.getValue(ognl2, context, context.getRoot());
System.out.println(value3);
}
public static void main(String[] args) throws OgnlException {
OGNLDemo demo = new OGNLDemo();
demo.testOgnl1();
}
}
运行结果:
China
User(id=100, name=Jack)
100
200
10.0
private static final Map<String, Object> expressionCache = new ConcurrentHashMap<String, Object>();
public static Object getValue(String expression, Object root) {
try {
//创建OgnlContext对象
Map<Object, OgnlClassResolver> context = Ognl.createDefaultContext(root, new OgnlClassResolver());
//使用OGNL执行expression表达式
return Ognl.getValue(parseExpression(expression), context, root);
} catch (OgnlException e) {
throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
}
}
private static Object parseExpression(String expression) throws OgnlException {
//查找缓存
Object node = expressionCache.get(expression);
if (node == null) {
//解析表达式
node = Ognl.parseExpression(expression);
//将表达式的解析结果添加到缓存中
expressionCache.put(expression, node);
}
return node;
}
StaticTextSqlNode很简单,就是直接返回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;
}
}
IfSqlNode是解析<if>节点,字段含义如下
//用于解析<if>节点的test表达式的值
private final ExpressionEvaluator evaluator;
//记录<if>节点中test表达式
private final String test;
//记录了<if>节点的子节点
private final SqlNode contents;
接口方法如下
@Override
public boolean apply(DynamicContext context) {
//检测test属性中记录的表达式
if (evaluator.evaluateBoolean(test, context.getBindings())) {
//如果test表达式为true,则执行子节点的apply()方法
contents.apply(context);
return true; //返回test表达式的结果为true
}
return false; //返回test表达式的结果为false
}
在ExpressionEvaluator中
public boolean evaluateBoolean(String expression, Object parameterObject) {
//用OGNL解析expression表达式
Object value = OgnlCache.getValue(expression, parameterObject);
//处理Boolean类型
if (value instanceof Boolean) {
return (Boolean) value;
}
//处理数字类型
if (value instanceof Number) {
return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
}
return value != null;
}
TrimSqlNode会根据子节点的解析结果,添加或删除响应的前缀或后缀,比如有这么一段配置
<insert id="insertNotNullBetdetails" parameterType="com.cloud.model.game.Betdetails">
insert into betdetails
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
<if test="orderId != null">orderId,</if>
<if test="actorIndex != null">actorIndex,</if>
<if test="ballIndex != null">ballIndex,</if>
<if test="ballValue != null">ballValue,</if>
<if test="betAmount != null">betAmount,</if>
<if test="createTime != null">createTime,</if>
<if test="rate1 != null">rate1,</if>
<if test="rate2 != null">rate2,</if>
<if test="rate3 != null">rate3,</if>
<if test="gameType != null">gameType,</if>
<if test="status != null">status,</if>
<if test="betResult != null">betResult,</if>
<if test="awardAmount != null">awardAmount,</if>
<if test="ballName != null">ballName,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
<if test="orderId != null">#{orderId},</if>
<if test="actorIndex != null">#{actorIndex},</if>
<if test="createTime != null">#{createTime},</if>
<if test="ballIndex != null">#{ballIndex},</if>
<if test="ballValue != null">#{ballValue},</if>
<if test="betAmount != null">#{betAmount},</if>
<if test="rate1 != null">#{rate1},</if>
<if test="rate2 != null">#{rate2},</if>
<if test="rate3 != null">#{rate3},</if>
<if test="gameType != null">#{gameType},</if>
<if test="status != null">#{status},</if>
<if test="betResult != null">#{betResult},</if>
<if test="awardAmount != null">#{awardAmount},</if>
<if test="ballName != null">#{ballName},</if>
</trim>
</insert>
TrimSqlNode中字段含义如下
private final SqlNode contents; //该<trim>节点的子节点
private final String prefix; //记录了前缀字符串(为<trim>节点包裹的SQL语句添加的前缀)
private final String suffix; //记录了后缀字符串(为<trim>节点包裹的SQL语句添加的后缀)
//如果<trim>节点包裹的SQL语句是空语句,删除指定的前缀,如where
private final List<String> prefixesToOverride;
//如果<trim>节点包裹的SQL语句是空语句,删除指定的后缀,如逗号
private final List<String> suffixesToOverride;
它的接口方法如下
@Override
public boolean apply(DynamicContext context) {
//创建FilteredDynamicContext对象,FilteredDynamicContext是TrimSqlNode的内部类,继承于DynamicContext
FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
//调用子节点的apply()方法进行解析,注意收集SQL语句的是filteredDynamicContext
boolean result = contents.apply(filteredDynamicContext);
//处理前缀和后缀
filteredDynamicContext.applyAll();
return result;
}
FilteredDynamicContext为DynamicContext的代理类,它的字段属性含义如下
private DynamicContext delegate; //底层封装委托的DynamicContext对象
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);
}
//将解析后的结果SQL片段添加到DynamicContext的StringBuilder中
delegate.appendSql(sqlBuffer.toString());
}
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
if (!prefixApplied) { //如果还没有处理过前缀
prefixApplied = true; //更新为已处理
if (prefixesToOverride != null) { //如果需要删除的前缀列表不为null
//遍历该前缀列表
for (String toRemove : prefixesToOverride) {
//如果<trim>子节点收集上来的SQL语句以该前缀开头
if (trimmedUppercaseSql.startsWith(toRemove)) {
//从<trim>子节点收集上来的StringBuilder中删除该前端
sql.delete(0, toRemove.trim().length());
break;
}
}
}
//如果有前缀字符串(比如说"("),将前缀字符串插入StringBuilder最前端
if (prefix != null) {
sql.insert(0, " ");
sql.insert(0, prefix);
}
}
}
private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
if (!suffixApplied) { //如果还没有处理过后缀
suffixApplied = true; //更新为已处理后缀
if (suffixesToOverride != null) { //如果需要处理的后缀列表不为null
//遍历该后缀列表
for (String toRemove : suffixesToOverride) {
//如果从<trim>子节点收集上来的SQL语句以该后缀结尾
if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
//获取该后缀的起始位置
int start = sql.length() - toRemove.trim().length();
//获取该后缀的终止位置
int end = sql.length();
//从<trim>子节点收集上来的StringBuilder中删除该后端
sql.delete(start, end);
break;
}
}
}
//如果有后缀字符串(比如说")"),将前缀字符串拼接上StringBuilder最后端
if (suffix != null) {
sql.append(" ");
sql.append(suffix);
}
}
}
WhereSqlNode和SetSqlNode都继承于TrimSqlNode,他们只是在TrimSqlNode的属性中指定了固定的标记。
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);
}
}
public class SetSqlNode extends TrimSqlNode {
private static List<String> suffixList = Arrays.asList(",");
public SetSqlNode(Configuration configuration,SqlNode contents) {
super(configuration, contents, "SET", null, null, suffixList);
}
}
ForEachSqlNode,在动态SQL语句中,通常需要对一个集合进行迭代,Mybatis提供了<foreach>标签实现该功能。在使用<foreach>标签迭代集合时,不仅可以使用集合的元素和索引值,还可以在循环开始之前或结束之后添加指定的字符串,也允许在迭代过程中添加指定的分隔符。配置样例如下
List参数的
<insert id="insertBetdetailsByBatch" parameterType="java.util.List">
insert into betdetails(id,orderId,actorIndex,createTime,ballIndex,ballValue,betAmount,rate1,rate2,rate3,gameType,status,betResult,awardAmount,ballName) values
<foreach collection="list" item="item" index="index" separator=",">
(#{item.id},#{item.orderId},#{item.actorIndex},#{item.createTime},#{item.ballIndex},#{item.ballValue},#{item.betAmount},#{item.rate1},#{item.rate2},#{item.rate3},#{item.gameType},#{item.status},#{item.betResult},#{item.awardAmount},#{item.ballName})
</foreach>
</insert>
Map参数的
<select id="findAgentUserBalance" parameterType="java.util.Map" resultType="java.math.BigDecimal">
SELECT SUM(banlance) FROM app_user
<where>
<if test="null != userIds">
AND id IN
<foreach collection="userIds" item="item" index="index" separator="," open="(" close=")">
#{item}
</foreach>
</if>
</where>
</select>
ForEachSqlNode中各个字段含义如下:
public static final String ITEM_PREFIX = "__frch_";
//用于判断循环的终止条件
private final ExpressionEvaluator evaluator;
//迭代的集合表达式
private final String collectionExpression;
//记录了该ForEachSqlNode节点的子节点
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;
接口方法为
@Override
public boolean apply(DynamicContext context) {
//获取用户传入的参数的上下文
Map<String, Object> bindings = context.getBindings();
//将参数上下文作为root传入OGNL解析出类似#{item.id}的原值后将迭代的集合表达式还原成集合本身
final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
//如果集合没有数据,直接返回true
if (!iterable.iterator().hasNext()) {
return true;
}
boolean first = true;
//在循环开始前处理要添加的字符SQL片段
applyOpen(context);
int i = 0;
//开始遍历集合,进入循环
for (Object o : iterable) {
//将context缓存为另一个对象
DynamicContext oldContext = context;
//如果是集合的第一项,或者分隔符为null
if (first || separator == null) {
//以空前缀来构建context为PrefixedContext对象
context = new PrefixedContext(context, "");
} else { //如果不是集合第一项,或者分隔符不为null,以分隔符为前缀来构建context为PrefixedContext对象
context = new PrefixedContext(context, separator);
}
//获取迭代计数器
int uniqueNumber = context.getUniqueNumber();
// Issue #709
if (o instanceof Map.Entry) {
//如果集合是Map类型,将集合中key和value添加到DynamicContext.bindings集合中保存
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
applyIndex(context, mapEntry.getKey(), uniqueNumber);
applyItem(context, mapEntry.getValue(), uniqueNumber);
} else {
//将集合中的索引和元素添加到DynamicContext.bindings集合中保存
applyIndex(context, i, uniqueNumber);
applyItem(context, o, uniqueNumber);
}
//调用子节点的apply()收集SQL语句,放入DynamicContext的代理类FilteredDynamicContext的StringBuilder中
//此处解析的是类如#{_frch_index_0},#{__frch_item_0}的标识
contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
if (first) {
//如果是第一项,将first更新为false(在applyOpen(context)中,prefixApplied已经被更新为true)
first = !((PrefixedContext) context).isPrefixApplied();
}
//还原context为原始DynamicContext,重新进入下一项循环
context = oldContext;
i++;
}
//在循环结束后添加要处理的SQL片段
applyClose(context);
context.getBindings().remove(item);
context.getBindings().remove(index);
return true;
}
private void applyOpen(DynamicContext context) {
if (open != null) {
//将开始循环前的SQL片段添加到DynamicContext的StringBuilder中
context.appendSql(open);
}
}
private void applyIndex(DynamicContext context, Object o, int i) {
if (index != null) {
//将集合的索引标识与放入的对象(map对象放入的是key,List对象放入的是真正的索引值)存入参考上下文中
//以上面配置的Map为例,假如传入的userIds为("小李飞刀",1),("霸天虎",2),此处假设用户名不能重复,且执行到第一个
//此处存入的是("index","小李飞刀"),如果执行到第二个的时候,此处存入的是("index","霸天虎")
context.bind(index, o);
//将集合的索引标识与计数器的连接绑定,与放入的对象存入参考上下文中
//此处存入的是("_frch_index_0","小李飞刀"),执行到第二个的时候,此处存入的是("_frch_index_1","霸天虎")
context.bind(itemizeItem(index, i), o);
}
}
private static String itemizeItem(String item, int i) {
//返回的值类似为__frch_item_0(假设item="item",i=0)
return new StringBuilder(ITEM_PREFIX).append(item).append("_").append(i).toString();
}
private void applyItem(DynamicContext context, Object o, int i) {
if (item != null) {
//将集合的内容标识与放入的对象(map对象放入的为value,List对象放入的为列表中的对象元素)存入参考上下文中
//对应索引的内容,此处存入的是("item",1),执行到第二个的时候,此处存入的是("item",2)
context.bind(item, o);
//将集合的内容标识与计数器的连接绑定,与放入的对象存入参考上下文中,为子节点的进一步解析(真正替换每一次迭代项为集合实际的值)做准备
//此处存入的是("__frch_item_0",1),执行到第二个的时候,此处存入的是("__frch_item_1",2)
context.bind(itemizeItem(item, i), o);
}
}
private void applyClose(DynamicContext context) {
if (close != null) {
//将结束循环后的SQL片段添加到DynamicContext的StringBuilder中
context.appendSql(close);
}
}
PrefixedContext继承于DynamicContext,其实就是DynamicContext的一个代理类,各字段的含义如下
private final DynamicContext delegate; //对DynamicContext的委托
private final String prefix; //指定的前缀
private boolean prefixApplied; //是否已经处理过前缀
最主要的地方就是它重写了appendSql()方法,其他地方都是通过对delegate的委托,来实现一样的功能
@Override
public void appendSql(String sql) {
//如果没有处理过前缀且要添加的SQL语句不为空
if (!prefixApplied && sql != null && sql.trim().length() > 0) {
//先把指定的前缀SQL片段添加到delegate中
delegate.appendSql(prefix);
//修改为前缀已处理
prefixApplied = true;
}
//再把处理完的SQL语句片段添加到StringBuilder中
delegate.appendSql(sql);
}
FilteredDynamicContext为DynamicContext的代理类,负责处理#{}占位符,它的各个字段含义如下
private final DynamicContext delegate; //委托的DynamicContext
private final int index; //对应集合项在集合中的索引位置
private final String itemIndex; //对应集合项的index(索引标识)
private final String item; //对应集合项的item(元素标识)
重写了appendSql()方法
@Override
public void appendSql(String sql) {
//解析类如("_frch_index_0","小李飞刀"),("__frch_item_0",1)的映射,给_frch_index_0,__frch_item_0变为OGNL可以识别的
//#{_frch_index_0},#{__frch_item_0}
GenericTokenParser parser = new GenericTokenParser("#{", "}", new TokenHandler() {
@Override
public String handleToken(String content) {
String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
if (itemIndex != null && newContent.equals(content)) {
newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
}
return new StringBuilder("#{").append(newContent).append("}").toString();
}
});
//通过OGNL来进行解析成对应映射的值
delegate.appendSql(parser.parse(sql));
}
在ExpressionEvaluator中
首先我们要知道,所有的集合接口(比如List,Set)都继承于Iterable<?>
public interface Collection<E> extends Iterable<E>
public Iterable<?> evaluateIterable(String expression, Object parameterObject) {
//使用OGNL来解析expression
Object value = OgnlCache.getValue(expression, parameterObject);
if (value == null) {
throw new BuilderException("The expression '" + expression + "' evaluated to a null value.");
}
//处理可迭代集合类型
if (value instanceof Iterable) {
return (Iterable<?>) value;
}
//处理数组类型,转化为List,并返回该List
if (value.getClass().isArray()) {
// the array may be primitive, so Arrays.asList() may throw
// a ClassCastException (issue 209). Do the work manually
// Curse primitives! :) (JGB)
int size = Array.getLength(value);
List<Object> answer = new ArrayList<Object>();
for (int i = 0; i < size; i++) {
Object o = Array.get(value, i);
answer.add(o);
}
return answer;
}
//处理Map类型,返回Map的entrySet集合(Set)
if (value instanceof Map) {
return ((Map) value).entrySet();
}
throw new BuilderException("Error evaluating expression '" + expression + "'. Return value (" + value + ") was not iterable.");
}
ChooseSqlNode,如果在编写动态SQL语句时需要类似Java中的switch语句的功能,可以使用<choose>、<when>、<otherwise>三个标签的组合。配置样例如下
<select id="findList" parameterType="java.util.Map" resultType="com.cloud.model.user.AppUser">
SELECT a.id,a.username,a.password,a.nickname,a.headImgUrl,a.phone,a.sex,a.enabled,a.type,a.createTime,a.updateTime,a.banlance,a.control,a.win,a.loginTime
FROM app_user a
<if test="groupId != null">
<choose>
<when test="groupId != 0">
INNER JOIN user_group b ON a.id=b.user_id AND b.group_id=#{groupId}
</when>
<otherwise>
LEFT JOIN user_group b ON a.id=b.user_id
</otherwise>
</choose>
</if>
</select>
ChooseSqlNode中各字段的含义如下
private final SqlNode defaultSqlNode; //<otherwise>节点对应的SqlNode
private final List<SqlNode> ifSqlNodes; //<when>节点对应的IfSqlNode集合
接口方法如下
@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表示的是动态SQL语句中的<bind>节点,我们来看一下<bind>节点的用法
<if test="userNarne != null and userNarne != ' '">
and user name like concat('毛',#{userNarne},'毡')
</if>
改为
<if test="userName != null and userName !=' '">
<bind name="userNarneLike" value="'毛' + userName + '毡'"/>
and user name like #{userNarneLike}
</if>
VarDeclSqlNode的字段内容如下
private final String name; //<bind>节点名称
private final String expression; //<bind>节点的value表达式
接口方法
@Override
public boolean apply(DynamicContext context) {
//解析OGNL表达式的值
final Object value = OgnlCache.getValue(expression, context.getBindings());
//将name,value存入DynamicContext的bindings集合中,提供其他SqlNode的下一步解析
context.bind(name, value);
return true;
}