mybatis中sqlNode

243 阅读3分钟

mybatis中sqlNode

TextSqlNode

HashMap<String,String> parameterObject = new HashMap<>();
parameterObject.put("id","123");
parameterObject.put("name","huskyui");
TextSqlNode textSqlNode = new TextSqlNode("select * from blog where id = #{id} and name = ${name}");
SqlNode mixedSqlNode = new MixedSqlNode(Arrays.asList(textSqlNode));
DynamicSqlSource dynamicSqlSource = new DynamicSqlSource(new Configuration(),mixedSqlNode);
BoundSql boundSql = dynamicSqlSource.getBoundSql(parameterObject);
System.out.println(boundSql.getSql());
// 这句话最后输出的结果是
// select * from blog where id = ? and name = huskyui
// 使用dynamicSqlSource.getBoundSql时,会调用mixedSqlNode的apply方法。而mixedSqlNode会foreach调用构造参数里面的Arrays.asList
// (textSqlNode)的apply方法,会触发对${}进行解析,并替换

下面我们来模拟一个解析的

// TextSqlNode里面的对${}进行解析,下面就是一个大概实例,实际中使用到了OgnlCache
GenericTokenParser tokenParser = new GenericTokenParser("${", "}", new TokenHandler() {
    @Override
    // 这个方法主要针对${context},对context进行处理
    public String handleToken(String content) {
        HashMap<String,String> map = new HashMap<>();
        map.put("name","huskyui");
        return map.get(content);
    }
});
String parsedText = tokenParser.parse("select * from blog where name = ${name}");
System.out.println(parsedText);

#{参数名}最终解析出来的 是用于java原生自带的PreparedStatement这种方式来进行,防止 sql注入

当然,我们也需要注意到的一点是${}是会直接进行替换的,这里就会出现上面可能出现的 sql注入问题

参考: www.cnblogs.com/question-sk…

IfSqlNode

在xml中出现的方式有多种,我们举个最简单的例子

 select * from user
 WHERE 1 = 1
 <if test="phone != null and phone != ''">
 AND phone = #{phone}
 </if>

最终解析为

HashMap<String,Object> propertyMap = new HashMap<>();
propertyMap.put("phone","110");
TextSqlNode textSqlNode = new TextSqlNode("select * from user\n" +
" WHERE 1 = 1");
IfSqlNode ifSqlNode = new IfSqlNode(new TextSqlNode("AND phone = #{phone}"),"phone != null and phone != ''");
SqlNode mixedSqlNode = new MixedSqlNode(Arrays.asList(textSqlNode,ifSqlNode));
DynamicSqlSource dynamicSqlSource = new DynamicSqlSource(new Configuration(),mixedSqlNode);
System.out.println(dynamicSqlSource.getBoundSql(propertyMap).getSql());

解释

在调用IfSqlNode的apply方法时,会调用自身封装的Ognl处理 "phone != null and phone != ''"这段语句,如果为true,则调用TextSqlNode里面的apply方法,解析${}并将解析完的语句append到结果上面去。

我当时一直在想,这个test里面的值,到底是如何处理的,难道是mybatis自己写的解析,最终发现了这个项目引进了 ognl.

不重复造轮子,站在巨人的肩膀上。

那么,我们就来模拟出一个ognl是如何实现对这个解析的

        OgnlContext context = new OgnlContext(null,null,new DefaultMemberAccess(true));
        Map<String,Object> propertyMap = new HashMap<>();
        propertyMap.put("phone","110");
        System.out.println(Ognl.getValue("phone!=null and phone != ''", context, propertyMap));

ChooseSqlNode

  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <choose>
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>

大概解析为,并不相符,我只是举个例子,由于在工作中很少用到这个choose

HashMap<String,Object> propertyMap = new HashMap<>();
//        propertyMap.put("id",1);
//        propertyMap.put("name","huskyui");
TextSqlNode textSqlNode = new TextSqlNode("select * from blog where 1 = 1");
List<SqlNode> ifNodeList = new ArrayList<>();
ifNodeList.add(new IfSqlNode(new TextSqlNode("and name = #{name}"),"name != null and name != ''"));
ifNodeList.add(new IfSqlNode(new TextSqlNode("and id = #{id}"),"id != null and id != ''"));
TextSqlNode defaultSqlNode = new TextSqlNode("and id > 0");
ChooseSqlNode chooseSqlNode = new ChooseSqlNode(ifNodeList,defaultSqlNode);
MixedSqlNode mixedSqlNode = new MixedSqlNode(Arrays.asList(textSqlNode,chooseSqlNode));
DynamicSqlSource dynamicSqlSource = new DynamicSqlSource(new Configuration(),mixedSqlNode);
System.out.println(dynamicSqlSource.getBoundSql(propertyMap).getSql());

chooseSqlNode有点像java中的switch-case,但不是全像(chooseSqlNode不能多个条件执行同一个)。我们来看一下ChooseSqlNode里面的apply方法

  @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;
  }

代码写的很清楚,一看就懂

TrimSqlNode

我们先讲解TrimSqlNode,然后再讲解WhereSqlNode和SetSqlNode

<select id="selectSome">
select * from blog
<trim prefix="where" prefixOverrides="and|or">
<if test="name!=null">
and `name` = #{name}
</if>
<if test="age!=null">
and age =#{age}
</if>
</trim>
</select>
TextSqlNode textSqlNode = new TextSqlNode("select * from blog");
IfSqlNode ifSqlNode1 = new IfSqlNode(new TextSqlNode("and name = #{name} "),"true");
IfSqlNode ifSqlNode2 = new IfSqlNode(new TextSqlNode("and age = #{age}"),"true");
MixedSqlNode mixedSqlNode = new MixedSqlNode(Arrays.asList(ifSqlNode1,ifSqlNode2));
TrimSqlNode trimSqlNode = new TrimSqlNode(new Configuration(),mixedSqlNode,"where","and|or",null,null);
DynamicSqlSource dynamicSqlSource = new DynamicSqlSource(new Configuration(),new MixedSqlNode(Arrays.asList(textSqlNode,trimSqlNode)));
System.out.println(dynamicSqlSource.getBoundSql(null).getSql());

我们来看看TrimSqlNode的构造参数

public TrimSqlNode(Configuration configuration, SqlNode contents, String prefix, String prefixesToOverride, String suffix, String suffixesToOverride) {
    this(configuration, contents, prefix, parseOverrides(prefixesToOverride), suffix, parseOverrides(suffixesToOverride));
}

此处会将prefixesToOveride字符串进行根据|分割

查看apply方法

@Override
public boolean apply(DynamicContext context) {
    FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
    // 此句执行,MixedSqlNode中apply方法,而apply方法就是forEach依次执行子的apply方法
    // and name = #{name} and age = #{age}
    // 生成这样的
    boolean result = contents.apply(filteredDynamicContext);
    // 这句话会对上面这句话进行解析,举例上面的,去除语句前面的and|or,然后加上where,当然只是加上了前缀解析
    filteredDynamicContext.applyAll();
    return result;
}

WhereSqlNode

WhereSqlNode本质上是基础了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);
}
<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG
  <where>
    <if test="state != null">
         state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
  </where>
</select>

SetSqlNode

<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>

SetSqlNode构造参数

  private static final List<String> COMMA = Collections.singletonList(",");

  public SetSqlNode(Configuration configuration,SqlNode contents) {
    super(configuration, contents, "SET", COMMA, null, COMMA);
  }

解释:去除拼接好后的语句username= ? ,password= ? ,email= ? ,bio= ? 去除前后的,,并在前加上set ,后不加

ForeachSqlNode

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  WHERE ID in
  <foreach item="item" index="index" collection="list"
      open="(" separator="," close=")">
        #{item}
  </foreach>
</select>

上面这句话如果正常的话,会解析成

select * from blog where id in ( ? , ? , ? , ? , ? , ? )

Map<String,Object> properties = new HashMap<>();
properties.put("list",Arrays.asList(1,2,3,4,5,6));
TextSqlNode textSqlNode = new TextSqlNode("select * from blog where id in ");
ForEachSqlNode forEachSqlNode = new ForEachSqlNode(new Configuration(),new TextSqlNode("#{item}")
,"list","index","item","(",")",",");
DynamicSqlSource dynamicSqlSource = new DynamicSqlSource(new Configuration(),new MixedSqlNode(Arrays.asList(textSqlNode,forEachSqlNode)));
System.out.println(dynamicSqlSource.getBoundSql(properties).getSql());

我们就来看看这个ForEachNode的apply方法是如何实现的

@Override
  public boolean apply(DynamicContext context) {
    Map<String, Object> bindings = context.getBindings();
    // 通过Ognl.getValue来判断当前是否是可以迭代的
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
     // 如果是不可以迭代的,那么就此结束
    if (!iterable.iterator().hasNext()) {
      return true;
    }
      // 标志位
    boolean first = true;
      // 在sql语句中先加入open
    applyOpen(context);
    int i = 0;
    for (Object o : iterable) {
      DynamicContext oldContext = context;
      if (first || separator == null) {
        context = new PrefixedContext(context, "");
      } else {
        context = new PrefixedContext(context, separator);
      }
        // 为了index而准备的。就是一个累加器。从0开始
      int uniqueNumber = context.getUniqueNumber();
      // Issue #709
        // 如果是map类型的
      if (o instanceof Map.Entry) {
        @SuppressWarnings("unchecked")
        Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
          //key做为index。
        applyIndex(context, mapEntry.getKey(), uniqueNumber);
        applyItem(context, mapEntry.getValue(), uniqueNumber);
      } else {
          // 否则就是collection,key是下标
        applyIndex(context, i, uniqueNumber);
        applyItem(context, o, uniqueNumber);
      }
        // 这个地方调用的是TextSqlNode的apply方法,apply方法里面调用了FiltedDynamicContext的appendSql方法
      contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
      if (first) {
        first = !((PrefixedContext) context).isPrefixApplied();
      }
      context = oldContext;
      i++;
    }
      // 在末尾加上close
    applyClose(context);
    context.getBindings().remove(item);
    context.getBindings().remove(index);
    return true;
  }

最终会生成select * from blog where id in ( #{__frch_item_0} , #{__frch_item_1} , #{__frch_item_2} , #{__frch_item_3} , #{__frch_item_4} , #{__frch_item_5} )

在调用DynamicSqlSource调用getBoundSql会将#{}替换成?