玖叶教程网

前端编程开发入门

动态SQL实现原理四-SqlNode详解

SqlNode用于描述Mapper SQL配置中的SQL节点,它是MyBatis框架实现动态SQL的基石。我们首先来看一下SqlNode接口的内容,代码如下:

public interface SqlNode {
  boolean apply(DynamicContext context);
}

如上面的代码所示,SqlNode接口的内容非常简单,只有一个apply()方法,该方法用于解析SQL节点,根据参数信息生成静态SQL内容。apply()方法需要接收一个DynamicContext对象作为参数,DynamicContext对象中封装了Mapper调用时传入的参数信息及MyBatis内置的_parameter和_databaseId参数。

在使用动态SQL时,我们可以使用<if>、<where>、<trim>等标签,这些标签都对应一种具体的SqlNode实现类,这些实现类如图所示。

这些SqlNode实现类的作用如下。

IfSqlNode:用于描述动态SQL中<if>标签的内容,XMLLanguageDriver在解析Mapper SQL配置生成SqlSource时,会对动态SQL中的<if>标签进行解析,将<if>标签转换为IfSqlNode对象。

ChooseSqlNode:用于描述动态SQL配置中的<choose>标签内容,Mapper解析时会把<choose>标签配置内容转换为ChooseSqlNode对象。

ForEachSqlNode:用于描述动态SQL配置中的<foreach>标签,<foreach>标签配置信息在Mapper解析时会转换为ForEachSqlNode对象。

MixedSqlNode:用于描述一组SqlNode对象,通常一个Mapper配置是由多个SqlNode对象组成的,这些SqlNode对象通过MixedSqlNode进行关联,组成一个完整的动态SQL配置。

SetSqlNode:用于描述动态SQL配置中的<set>标签,Mapper解析时会把<set>标签配置信息转换为SetSqlNode对象。

WhereSqlNode:用于描述动态SQL中的<where>标签,动态SQL解析时,会把<where>标签内容转换为WhereSqlNode对象。

TrimSqlNode:用于描述动态SQL中的<trim>标签,动态SQL解析时,会把<trim>标签内容转换为TrimSqlNode对象。在学习MyBatis动态SQL使用时,我们了解到<where>标签和<set>标签实际上是<trim>标签的一种特例,<where>标签和<set>标签实现的内容都可以使用<trim>标签来完成,因此WhereSqlNode和SetSqlNodel类设计为TrimSqlNode类的子类,属于特殊的TrimSqlNode。

StaticTextSqlNode:用于描述动态SQL中的静态文本内容

TextSqlNode:该类与StaticTextSqlNode类不同的是,当静态文本中包含${}占位符时,说明${}需要在Mapper调用时将${}替换为具体的参数值。因此,使用TextSqlNode类来描述。

VarDeclSqlNode:用于描述动态SQL中的<bind>标签,动态SQL解析时,会把<bind>标签配置信息转换为VarDeclSqlNode对象。

了解了各个SqlNode实现类的作用后,接下来我们来了解一下SqlNode与动态SQL配置之间的对应关系。假如我们有如下Mapper配置:

    <select id="getUserByEntity"  resultType="com.blog4java.mybatis.example.entity.UserEntity">
        select
        <include refid="userAllField"/>
        from user
        <where>
            <if test="id != null">
                AND id = #{id}
            </if>
            <if test="name != null">
                AND name = #{name}
            </if>
            <if test="phone != null">
                AND phone = #{phone}
            </if>
        </where>
    </select>

上面是一个完整的Mapper SQL配置,从MyBatis动态SQL的角度来看,它是由4个SqlNode对象构成的。该Mapper配置转换为SqlNode代码如下.

 @Test
    public void testSqlNode() {
        // 构建SqlNode
        SqlNode sn1 = new StaticTextSqlNode("select * from user where 1=1");
        SqlNode sn2 = new IfSqlNode(new StaticTextSqlNode(" AND id = #{id}"),"id != null");
        SqlNode sn3 = new IfSqlNode(new StaticTextSqlNode(" AND name = #{name}"),"name != null");
        SqlNode sn4 = new IfSqlNode(new StaticTextSqlNode(" AND phone = #{phone}"),"phone != null");
        SqlNode mixedSqlNode = new MixedSqlNode(Arrays.asList(sn1, sn2, sn3, sn4));
        // 创建参数对象
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("id","1");
        // 创建动态SQL解析上下文
        DynamicContext context = new DynamicContext(sqlSession.getConfiguration(),paramMap);
        // 调用SqlNode的apply()方法解析动态SQL
        mixedSqlNode.apply(context);
        // 调用DynamicContext对象的getSql()方法获取动态SQL解析后的SQL语句
        System.out.println(context.getSql());
    }

在上面的代码中,我们创建了一个StaticTextSqlNode和三个IfSqlNode来描述Mapper中动态SQL的配置,其中IfSqlNode由一个StaticTextSqlNode和条件表达式组成。

接着创建了一个MixedSqlNode将这些SqlNode组合起来,这样就完成了通过Java对象来描述动态SQL配置。

SqlNode对象创建完毕后,我们就可以调用MixedSqlNode的apply()方法根据参数内容动态地生成SQL内容了。该方法接收一个DynamicContext对象作为参数,DynamicContext对象中封装了Mapper调用时的参数信息。上面的代码中,我们创建了一个DynamicContext,然后调用MixedSqlNode对象的apply()方法,动态SQL的解析结果封装在DynamicContext对象中,我们只需要调用DynamicContext对象的getSql()方法即可获取动态SQL解析后的SQL语句。运行上面这段代码后,生成的SQL内容如下:

select * from user where 1=1 AND id = #{id}

接下来我们再来了解一下SqlNode解析生成SQL语句的过程。首先来看MixedSqlNode的实现,代码如下:

public class MixedSqlNode implements SqlNode {
  private final List<SqlNode> contents;

  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }

  @Override
  public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : contents) {
      sqlNode.apply(context);
    }
    return true;
  }
}

如上面的代码所示,MixedSqlNode类的实现比较简单,通过一个List对象维护所有的SqlNode对象,MixedSqlNode类的apply()方法中对所有SqlNode对象进行遍历,以当前DynamicContext对象作为参数,调用所有SqlNode对象的apply()方法。接下来我们再来看一下StaticTextSqlNode的实现,代码如下:

public class StaticTextSqlNode implements SqlNode {
    // 静态SQL文本内容
    private final String text;

    public StaticTextSqlNode(String text) {
        this.text = text;
    }

    @Override
    public boolean apply(DynamicContext context) {
        // 追加SQL内容
        context.appendSql(text);
        return true;
    }

}

如上面的代码所示,StaticTextSqlNode实现类比较简单,该类中维护了Mapper配置中的静态SQL节点内容。调用apply()方法时,将静态SQL文本内容追加到DynamicContext对象中。

最后我们了解一下实现动态SQL比较关键的SqlNode实现类之一——IfSqlNode的实现,代码如下:

public class IfSqlNode implements SqlNode {
  // evaluator属性用于解析OGNL表达式
  private final ExpressionEvaluator evaluator;
  // 保存<if>标签test属性内容
  private final String test;
  // <if>标签内Sql内容
  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) {
    // 如果OGNL表达式值为true,则调用<if>标签内容对应的SqlNode的apply()方法
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
      contents.apply(context);
      return true;
    }
    return false;
  }

}

如上面的代码所示,IfSqlNode中维护了一个ExpressionEvaluator类的实例,该实例用于根据当前参数对象解析OGNL表达式。另外,IfSqlNode维护了<if>标签test属性指定的表达式内容和<if>标签中的SQL内容对应的SqlNode对象。

在IfSqlNode类的apply()方法中,首先解析test属性指定的OGNL表达式,只有当表达式值为true的情况下,才会执行<if>标签中SQL内容对应的SqlNode的apply()方法。这样就实现了只有当<if>标签test属性表达式值为true的情况下,才会追加<if>标签中配置的SQL信息。

其他SqlNode实现类(例如ForEachSqlNode、TrimSqlNode)的原理与IfSqlNode类似,可自行阅读其源码。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言