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()方法
总结与反思:
#{id}与${id}的区别?
通用解释:#会预编译,$直接被替换成设定值
根本原因是不同SqlSource的实现类- StaticSqlSource、DynamicSqlSource、RawSqlSource对于getBoundSql()过程区别?
- StaticSqlSource启动阶段直接将
#{}解析成?,getBoundSql()不做更改 - DynamicSqlSource根据不同的SqlNode组装SQL,将预编译符替换为?,非预编译符替换为值,返回SQL语句
- RawSqlSource封装一个任意类型的SqlSource,实际调用成员变量的sqlSource.getBoundSql()方法
- StaticSqlSource启动阶段直接将
- mapper.java中的语句(mapper.xml的SQL语句逻辑与1重合,代码可见XMLLanguageDriver)
- 包含
<script>时- 非动态时(没有mapper标签且没有$符号),属于RawSqlSource,封装StaticSqlSource
- 动态时,属于DynamicSqlSource、MixedSqlNode,启动后通过rootSqlNode.apply(context)解析
- 不包含
<script>时- 只有
#{id}时,属于RawSqlSource、RawSqlSource封装StaticSqlSource,启动前直接被解析为? - 只有
${id}时,属于DynamicSqlSource、TextSqlNode,执行rootSqlNode.apply(context)直接被替换为设定值 - 都有时,属于DynamicSqlSource、TextSqlNode,执行
rootSqlNode.apply(context)${id}直接被替换为设定值,#{id}最终被解析为?
- 只有
- 包含
--1处MixedSqlNode遍历contents拼接SQL的过程分析:
INSERT INTO tree属于StaticTextSqlNode,直接添加<trim></trim>标签属于TrimSqlNode,节点内容属于MixedSqlNodeid,属于StaticTextSqlNode,直接添加<if></if>标签属于IfSqlNode,节点内容属于MixedSqlNode- 是否满足if表达式
pid != null,满足则添加StaticTextSqlNode的pid,
- 是否满足if表达式
id,pid,添加去掉最后一个逗号,添加括号,此时的SQL语句为INSERT INTO tree (id,pid)
VALUES属于StaticTextSqlNode,直接添加,此时的SQL语句为INSERT INTO tree (id,pid) VALUES- 继续分析
<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的过程分析:
- SELECT
- ew.sqlSelect为空,所以为选择id,pid
- FROM tree
<if></if>标签中,ew != null,进入标签内逻辑- ew.entity == null
- ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere
- ew.nonEmptyOfNorm不满足,所以${ew.sqlSegment}
- ew.sqlSegment执行的是
AbstractWrapper->getSqlSegment(),它返回拼接的SQL语句(where+groupBy+having+orderBy),此处为id > #{ew.paramNameValuePairs.MPGENVAL1} AND pid = #{ew.paramNameValuePairs.MPGENVAL2} GROUP BY id
- ew.sqlSegment执行的是
- ew.nonEmptyOfNorm不满足,所以${ew.sqlSegment}
- ew.emptyOfWhere为false
- ew.sqlComment == null
- 最终返回的sql语句是:
SELECT id,pid FROM tree WHERE id > #{ew.paramNameValuePairs.MPGENVAL1} AND pid = #{ew.paramNameValuePairs.MPGENVAL2} GROUP BY id - 同样的,#都会被解析成为?,因此最终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对象中