MyabtisPlus(5):启动后动态SQL拼接

4,571 阅读3分钟

save(T entity)方法动态生成并插入字段不为NULL的SQL语句,list(Wrapper<T> queryWrapper)拼接SQL语句的条件,本节将论述这两个典型方法关于SQL语句的拼接实现。

save方法动态拼接

<script>
    INSERT INTO tree
        <trim prefix="(" suffix=")" suffixOverrides=",">
            id,
            <if test="pid != null">pid,</if>
        </trim> 
    VALUES 
        <trim prefix="(" suffix=")" suffixOverrides=",">
            #{id},
            <if test="pid != null">#{pid},</if>
        </trim>
</script>
BaseStatementHandler->BaseStatementHandler():
    if (boundSql == null) { 
        generateKeys(parameterObject);
        boundSql = mappedStatement.getBoundSql(parameterObject);
    }
MappedStatement->getBoundSql():
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
//  insert方法属于动态sql源
DynamicSqlSource->getBoundSql():
    // DynamicContext的主要作用是用于组装SQL,在其下的sqlBuilder成员变量体现
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // rootSqlNode属于混合sql结点模式,其下有成员变量List<SqlNode>类型的contents,contents一一执行apply方法
    // 最终context.sqlBuilder的SQL语句为:INSERT INTO tree (id,pid) VALUES (#{id},#{pid})
    rootSqlNode.apply(context); // --1
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    // 解析SQL语句,主要是把预编译符替换成?,然后返回静态sql源
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    // BoundSql的成员变量中,sql是sql语句,parameterMappings包含预编译名称等信息,parameterObject包含参数名称和参数值等信息
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    // 至此,最终生成的SQL语句在boundSql的sql中
    return boundSql;

附录:SqlNode的apply()方法

  • StaticTextSqlNode 直接添加text
  • TextSqlNode text部分含有非预编译符${},将这部分内容替换为设定值
  • IfSqlNode 满足test条件,执行其下的contents.apply()方法
  • MixedSqlNode 组合sql节点,成员变量contents是SqlNode的List集合,apply()方法遍历contents并执行apply()方法

总结与反思:

  1. #{id}${id}的区别?
    通用解释:#会预编译,$直接被替换成设定值
    根本原因是不同SqlSource的实现类
  2. StaticSqlSource、DynamicSqlSource、RawSqlSource对于getBoundSql()过程区别?
    • StaticSqlSource启动阶段直接将#{}解析成?,getBoundSql()不做更改
    • DynamicSqlSource根据不同的SqlNode组装SQL,将预编译符替换为?,非预编译符替换为值,返回SQL语句
    • RawSqlSource封装一个任意类型的SqlSource,实际调用成员变量的sqlSource.getBoundSql()方法
  3. mapper.java中的语句(mapper.xml的SQL语句逻辑与1重合,代码可见XMLLanguageDriver)
    1. 包含<script>
      1. 非动态时(没有mapper标签且没有$符号),属于RawSqlSource,封装StaticSqlSource
      2. 动态时,属于DynamicSqlSource、MixedSqlNode,启动后通过rootSqlNode.apply(context)解析
    2. 不包含<script>
      1. 只有#{id}时,属于RawSqlSource、RawSqlSource封装StaticSqlSource,启动前直接被解析为?
      2. 只有${id}时,属于DynamicSqlSource、TextSqlNode,执行rootSqlNode.apply(context)直接被替换为设定值
      3. 都有时,属于DynamicSqlSource、TextSqlNode,执行rootSqlNode.apply(context)${id}直接被替换为设定值,#{id}最终被解析为?

--1处MixedSqlNode遍历contents拼接SQL的过程分析:

  1. INSERT INTO tree属于StaticTextSqlNode,直接添加
  2. <trim></trim>标签属于TrimSqlNode,节点内容属于MixedSqlNode
    1. id,属于StaticTextSqlNode,直接添加
    2. <if></if>标签属于IfSqlNode,节点内容属于MixedSqlNode
      1. 是否满足if表达式pid != null,满足则添加StaticTextSqlNode的pid,
    3. id,pid,添加去掉最后一个逗号,添加括号,此时的SQL语句为INSERT INTO tree (id,pid)
  3. VALUES属于StaticTextSqlNode,直接添加,此时的SQL语句为INSERT INTO tree (id,pid) VALUES
  4. 继续分析<trim></trim>标签,过程同2,最终SQL语句为INSERT INTO tree (id,pid) VALUES (#{id},#{pid})

list方法动态拼接

QueryWrapper

<script>
    SELECT
        <choose>
            <when test="ew != null and ew.sqlSelect != null">
                $ { ew.sqlSelect }
            </when>
            <otherwise>
                id,pid
            </otherwise>
        </choose>
    FROM tree
        <if test="ew != null">
            <where>
                <if test="ew.entity != null">
                    <if test="ew.entity.id != null">id=#{ew.entity.id}</if>
                    <if test="ew.entity.pid != null"> AND pid=#{ew.entity.pid}</if>
                </if>
                <if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere">
                    <if test="ew.nonEmptyOfEntity and ew.nonEmptyOfNormal"> 
                        AND
                    </if> 
                      ${ew.sqlSegment}
                </if>
            </where>
            <if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.emptyOfWhere">
                ${ew.sqlSegment}
            </if>
        </if> 
        <choose>
            <when test="ew != null and ew.sqlComment != null">
                ${ew.sqlComment}
            </when>
            <otherwise></otherwise>
        </choose>
</script>
DemoController->test():
    QueryWrapper<Tree> queryWrapper = new QueryWrapper<>();
    queryWrapper.gt("id", 1); // --2
    queryWrapper.eq("pid", "2");
    queryWrapper.groupBy("id");
    // 合并段表达式位于expression变量中
    service.list(queryWrapper);
// 此处的Compare是MP的一个类
Compare->gt(): // --2
    return gt(true, column, val);
AbstractWrapper->gt():
    return addCondition(condition, column, GT, val);
AbstractWrapper->addCondition():
    // 注意第二、四个参数是lambda表达式,运行时才会执行
    // columnToString(column)直接返回字段名
    /* formatSql("{0}", val)
    AbstractWrapper有个paramNameSeq的原子类,当有一个条件时,paramNameSeq+1
    假设paramNameSeq为1,则paramNameValuePairs记录key为MPGENVAL1,value为1
    最终返回字符串#{ew.paramNameValuePairs.MPGENVAL1}
    */
    return doIt(condition, () -> columnToString(column), sqlKeyword, () -> formatSql("{0}", val));
AbstractWrapper->doIt():
    expression.add(sqlSegments);
MergeSegments->add():
    // 参数合并成sql段对象,添加进normal合并列中,返回
    ......
----------------------------------------------------------------------------------------
// 接下来进入动态代理
MybatisMapperMethod->MybatisMapperMethod():
    // 如果MybatisMapperProxy中的methodCache没有methodName的话,会new
    // command封装了参数方法名和sql命令类型
    this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);
    // method封装了方法的参数信息 返回类型信息等,详细见参数值设置
    this.method = new MapperMethod.MethodSignature(config, mapperInterface, method);
----------------------------------------------------------------------------------------
// 仍然执行DynamicSqlSource->getBoundSql()方法,在这一过程中:
ExpressionEvaluator->evaluateBoolean():
    // 这一句会先获取拼接的SQL值,一大段ognl的方法之后,最终进入AbstractWrapper的getSqlSegment()方法,拼接先前在QueryWrapper的expression里设置的sql片段,形成一段SQL语句
    // 发现sqlSegment不为空,where也不为空,返回true
    Object value = OgnlCache.getValue(expression, parameterObject);
    // 最终的sql语句就为SELECT语句+WHERE语句+GROUP BY 语句了

List<T> selectList(@Param("ew") Wrapper<T> queryWrapper)遍历contents拼接SQL的过程分析:

  1. SELECT
  2. ew.sqlSelect为空,所以为选择id,pid
  3. FROM tree
  4. <if></if>标签中,ew != null,进入标签内逻辑
    1. ew.entity == null
    2. ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere
      1. ew.nonEmptyOfNorm不满足,所以${ew.sqlSegment}
        1. ew.sqlSegment执行的是AbstractWrapper->getSqlSegment(),它返回拼接的SQL语句(where+groupBy+having+orderBy),此处为id > #{ew.paramNameValuePairs.MPGENVAL1} AND pid = #{ew.paramNameValuePairs.MPGENVAL2} GROUP BY id
    3. ew.emptyOfWhere为false
  5. ew.sqlComment == null
  6. 最终返回的sql语句是:SELECT id,pid FROM tree WHERE id > #{ew.paramNameValuePairs.MPGENVAL1} AND pid = #{ew.paramNameValuePairs.MPGENVAL2} GROUP BY id
  7. 同样的,#都会被解析成为?,因此最终BoundSQL里的sql语句为SELECT id,pid FROM tree WHERE id > ? AND pid = ? GROUP BY id

LambdaQueryWrapper

与QueryWrapper不同的是,LambdaQueryWrapper传入的是lambda表达式
columnToString(column)以及formatSql("{0}", val)等方法,会先解析lambda,返回字符串名称
除此两点外,其余与QueryWrapper并无非常大的区别

总结与反思:

第一步:先找到它的原始sql语句
第二步:分析出它属于什么样的sql源,sql节点有哪些
第三步:运行时会根据实际情况动态生成sql语句,并绑定在boundSql对象中