美文网首页
Mybatis为啥要这么设计动态SQL?

Mybatis为啥要这么设计动态SQL?

作者: 程序员麦冬 | 来源:发表于2020-10-28 14:18 被阅读0次

    1. Mybatis支持的动态sql及基本用法

    org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.nodeHandlers(String)部分源码。

      NodeHandler nodeHandlers(String nodeName) {
        Map<String, NodeHandler> map = new HashMap<String, NodeHandler>();
        map.put("trim", new TrimHandler());
        map.put("where", new WhereHandler());
        map.put("set", new SetHandler());
        map.put("foreach", new ForEachHandler());
        map.put("if", new IfHandler());
        map.put("choose", new ChooseHandler());
        map.put("when", new IfHandler());
        map.put("otherwise", new OtherwiseHandler());
        map.put("bind", new BindHandler());
        return map.get(nodeName);
      }
    

    Mybatis所支持的动态sql标签:trim|where|set|foreach|if|choose|when|otherwise|bind。

        <select id="findStudents" parameterType="customMap" resultType="StudentResult">
            select * from STUDENTS where 1 = 1 
            <choose>
                <when test="name != null">
                    and name = #{name}
                </when>
                <when test="email != null">
                    and EMAIL = #{email}
                </when>
                <otherwise>
                    and PHONE = "123"
                </otherwise>
            </choose>
        </select>
    
        <select id="countAll" resultType="int">
            select count(1) from (
                select 
                stud_id as studId
                , name, email
                , dob
                , phone
            from students
            <where>
                <if test="id != null">
                    AND STUD_ID < 310
                </if>
            </where>
            ) tmp 
        </select>
    
        <select id="findAllStudents" resultMap="StudentResult" parameterMap="customMap">
            <bind name="status" value="'status'"/>
            SELECT * FROM STUDENTS WHERE STUD_ID > #{id}, #{status},${driver}
        </select>
    
        <insert id="insertStudents" useGeneratedKeys="true" keyProperty="studId" parameterType="java.util.ArrayList">
            INSERT INTO
            STUDENTS(STUD_ID, NAME, EMAIL, DOB, PHONE)
            VALUES
            <foreach collection="list" item="item" index="index" separator=","> 
                (#{item.studId},#{item.name},#{item.email},#{item.dob}, #{item.phone}) 
            </foreach> 
        </insert>
    

    为了避免篇幅过长,我们简单列举了几个动态sql的基本用法,我们的重点依然是剖析Mybatis动态sql的底层设计原理。

    2. SqlSource

    在Mybatis中,每一个select|insert|update|delete标签,都会被解析为一个MappedStatement对象,SqlSource就是MappedStatement对象中一个属性,其最终执行的sql字符串就是由SqlSource提供的。

    public final class MappedStatement {
        private SqlSource sqlSource;
    }
    

    org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode()部分源码:

    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    

    (Made In IntelliJ Idea IDE)

    DynamicSqlSource:处理动态sql。

    RawSqlSource:处理静态sql,其内部装饰StaticSqlSource。

    StaticSqlSource:处理静态sql,无论是静态sql,还是动态sql,最终的处理结果,都是静态sql。

    ProviderSqlSource:处理注解Annotation形式的sql。

    DynamicSqlSource和StaticSqlSource的最大区别在于:StaticSqlSource的String sql,可以直接获取使用,而DynamicSqlSource的String sql需要逐一根据条件解析并拼接出最终的sql,方能使用。

    3. DynamicSqlSource以及SqlNode

    public class DynamicSqlSource implements SqlSource {
    
      private Configuration configuration;
      private SqlNode rootSqlNode;
    }
    
    public interface SqlNode {
      boolean apply(DynamicContext context);
    }
    

    boolean apply(DynamicContext context):该方法的含义为,将sql的处理结果,append到DynamicContext context对象中,DynamicContext可以理解为StringBuilder对象的功能,它的作用就是计算sql片段并append到一起,形成最终的sql。对该方法的理解非常重要,只有理解了这个方法,才能真正明白一个完整sql是如何组装出来的。

    下面的伪代码,展示了SqlNode.apply(DynamicContext)方法设计的核心原理。

            StringBuilder sb = new StringBuilder();
            IfSqlNode.apply(StringBuilder sb) {
                sb.append("select ");
            }
            SetSqlNode.apply(StringBuilder sb) {
                sb.append("* from ss ");
            }
            sb.toString();
            //output: select * from ss
    

    DynamicSqlSource为动态sql源,而SqlNode则具体代表了动态sql源中具体的动态sql类型。

    (Made In IntelliJ Idea IDE)

    上面的SqlNode,基本上见名知意,我们着重解释一下容易迷惑的两个SqlNode。

    VarDeclSqlNode:处理动态sql标签<bind>的SqlNode类。

    public class VarDeclSqlNode implements SqlNode {
    
      private final String name;
      private final String expression;
    
      public VarDeclSqlNode(String var, String exp) {
        name = var;
        expression = exp;
      }
    
      @Override
      public boolean apply(DynamicContext context) {
        final Object value = OgnlCache.getValue(expression, context.getBindings());
        // 由于没有sql可append,仅是把bind标签的变量名和值保存至上下文参数列表内
        context.bind(name, value);
        return true;
      }
    }
    

    MixedSqlNode:意为混合的SqlNode,它保存了其他多种SqlNode的集合,可以看做是一个List<SqlNode>列表,事实也确实如此。

    DynamicSqlSource中的SqlNode rootSqlNode属性,通常都是MixedSqlNode对象(完全是静态sql时,可能是一个StaticTextSqlNode),而MixedSqlNode对象又保存了所有的List<SqlNode>集合,这也是通过一个rootSqlNode,就能找到所有SqlNode的深层原因。

    4. SqlNode的组合设计模式

    public class ForEachSqlNode implements SqlNode {
     private SqlNode contents;
    }
    

    SqlNode,采用了组合设计模式,组合设计模式可以用来表示经典的树型结构,有人不禁要问,组合设计模式,它的属性,应该List<SqlNode>集合,怎么会是单一的SqlNode呢?

    前面说的MixedSqlNode,就代表了List<SqlNode>集合,所以,它是换汤不换药的经典组合设计模式。

    举例:ForEachSqlNode内部,可能是一个StaticTextSqlNode,看XML就一目了然。

    <foreach collection="list" item="item" index="index" separator=","> 
                (#{item.studId},#{item.name},#{item.email},#{item.dob}, #{item.phone}) 
    </foreach>
    

    5. NodeHandler

    SqlNode是由NodeHandler创建出来的。

    image

    (Made In EDrawMax)

      private class ChooseHandler implements NodeHandler {
        public ChooseHandler() {
          // Prevent Synthetic Access
        }
    
        @Override
        public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
          List<SqlNode> whenSqlNodes = new ArrayList<SqlNode>();
          List<SqlNode> otherwiseSqlNodes = new ArrayList<SqlNode>();
          handleWhenOtherwiseNodes(nodeToHandle, whenSqlNodes, otherwiseSqlNodes);
          SqlNode defaultSqlNode = getDefaultSqlNode(otherwiseSqlNodes);
          ChooseSqlNode chooseSqlNode = new ChooseSqlNode(whenSqlNodes, defaultSqlNode);
          targetContents.add(chooseSqlNode);
        }
    
        private void handleWhenOtherwiseNodes(XNode chooseSqlNode, List<SqlNode> ifSqlNodes, List<SqlNode> defaultSqlNodes) {
          List<XNode> children = chooseSqlNode.getChildren();
          for (XNode child : children) {
            String nodeName = child.getNode().getNodeName();
            NodeHandler handler = nodeHandlers(nodeName);
            if (handler instanceof IfHandler) {
              handler.handleNode(child, ifSqlNodes);
            } else if (handler instanceof OtherwiseHandler) {
              handler.handleNode(child, defaultSqlNodes);
            }
          }
        }
    //...
    }
    

    上面的例子,可以清楚看出,<choose>标签是和<when>、<otherwise>标签配合使用的,创建ChooseSqlNode时,就同时创建了when、otherwise的逻辑,而when会转换为if标签处理,otherwise则转换为SqlNode处理,一般是StaticTextSqlNode。

    map.put("if", new IfHandler());
    map.put("when", new IfHandler());
    

    因篇幅问题,我们不再逐一描述,读者可自行查看。

    6. LanguageDriver

    LanguageDriver是一个辅助工具类,用于创建SqlSource。

    (Made In IntelliJ Idea IDE)
    XMLLanguageDriver:用于创建动态、静态SqlSource。

    RawLanguageDriver:在确保只有静态sql时,可以使用,不得含有任何动态sql的内容,否则,请使用XMLLanguageDriver。它其实是对XMLLanguageDriver创建的结果进行唯静态sql检查而已,发现有动态sql的内容,就抛异常。

    /**
     * As of 3.2.4 the default XML language is able to identify static statements
     * and create a {@link RawSqlSource}. So there is no need to use RAW unless you
     * want to make sure that there is not any dynamic tag for any reason.
     * 
     * @since 3.2.0
     * @author Eduardo Macarron
     */
    public class RawLanguageDriver extends XMLLanguageDriver {
    
      @Override
      public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        SqlSource source = super.createSqlSource(configuration, script, parameterType);
        checkIsNotDynamic(source);
        return source;
      }
    // ...
    }
    

    最后

    感谢大家看到这里,如果本文有什么不足之处,欢迎多多指教;如果你觉得对你有帮助,请给我点个赞。

    也欢迎大家关注我的公众号:程序员麦冬,每天更新行业资讯!

    相关文章

      网友评论

          本文标题:Mybatis为啥要这么设计动态SQL?

          本文链接:https://www.haomeiwen.com/subject/eynqvktx.html