Mybatis 处理 sql 语句,以查询为例
本文 mybatis 处理 sql 语句主要分为编译期间处理和执行期间处理
另外 sql 语句又分为 静态 sql 语句 和动态 sql 语句,用参数修饰符来区分。
静态 sql 语句:没有参数或者参数修饰符全部是 #{} 这种,并且没有其他标签
动态 sql 语句:参数其中之一被 ${} 修饰,或者有标签
Mybatis 对动态 sql 的定义
mybatis 初始化完成后 所有处理完的 sql 语句都存在 SqlSource
Configuration —》 MappedStatement–》 SqlSource
编译期间处理
编译期间对语句的处理主要分为:预编译及非预编译,拼接动态 sql 语句。
预编译
预编译主要操作的是静态 sql 语句。将 #{xx} 替换成 ?的占位符。
原 sql 语句
select * from user_bo ur where ur.id = #{ id} and ur.user_name = #{name }
处理完
select * from user_bo ur where ur.id = ? and ur.user_name = ?
非预编译
原 sql 语句
select * from user_bo ur where ur.id = #{ id} and ur.user_name = ${name }
处理完
select * from user_bo ur where ur.id = #{ id} and ur.user_name = ${name }
拼接动态 sql 语句
拼接动态 sql 语句 主要是处理 动态 sql 的标签。
原 sql 语句
select * from user_bo ur
<where>
<if test="id!=null">
and ur.id = #{ id}
</if>
<if test="name!=null">
and ur.user_name = #{name }
</if>
</where>
处理完
select * from user_bo ur where ur.id = #{ id} and ur.user_name = #{name }
或者
原 sql 语句
select * from user_bo ur
<where>
<if test="id!=null">
and ur.id = ${ id}
</if>
<if test="name!=null">
and ur.user_name = #{name }
</if>
</where>
处理完
select * from user_bo ur where ur.id = #{ id} and ur.user_name = ${name }
运行期间处理
运行期间对语句的处理主要分为: sql 参数赋值
在运行期间对参数赋值主要还是封装的原生 JDBC 的操作。
源码解析
节点:不管是静态 sql 语句 还是 动态 sql 语句 解析完后都是一个节点集合,在代码种用 SqlNode 对象代替, 而节点有很多类型 例如 WhereSqlNode,IfSqlNode 等等…
预编译期间 占位符 替换 #{}
能够满足预编译期间占位符替换 #{} 的 静态 sql 语句主要流程为
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql;
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);
}
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
GenericTokenParser 的 parse(…) 方法主要是 通过Java String 的截取替换操作来完成的。
而返回的正是 一个 StaticSqlSource 静态 sql 资源
动态节点解析
以下文为例
<select id="selectAll03" parameterType="u" resultType="u">
select * from user_bo ur
<where>
<if test="id!=null">
and ur.id = #{ id}
</if>
<if test="name!=null">
and ur.user_name = #{name }
</if>
</where>
</select>
先看一下流程图 1 解析节点 完成 sql 的预编译(大纲流程)
从 parseDynamicTags 到 解析select 节点 开始
XMLScriptBuilder 对象里面 script 属性就是节点信息,并且在上一步初始化了很多种不同的 NodeHandler 节点处理器
XMLScriptBuilder-context-body: select * from user_bo ur
script:
<select parameterType="u" id="selectAll03" resultType="u">
<where>
<if test="id!=null">
and ur.id = #{ id}
</if>
<if test="name!=null">
and ur.user_name = #{name }
</if>
</where>
</select>
开始解析的源码:
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
langDriver.createSqlSource:
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
builder.parseScriptNode():
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
parseDynamicTags(context):
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
String nodeName = child.getNode().getNodeName();
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE)
获取 body: select * from user_bo ur 并不是节点
使用文本类型节点修饰
TextSqlNode textSqlNode = new TextSqlNode(data);
是否为动态类型
textSqlNode.isDynamic():
public boolean isDynamic() {
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
GenericTokenParser parser = createParser(checker);
parser.parse(text);
return checker.isDynamic();
}
判断有没有${xxx} ,所以不是动态的
private GenericTokenParser createParser(TokenHandler handler) {
return new GenericTokenParser("${", "}", handler);
}
转化为静态文本sql 节点 存放到 List contents 节点集合中
contents.add(new StaticTextSqlNode(data));
继续循环第二层节点
XNode child:
<where>
<if test="id!=null">
and ur.id = #{ id}
</if>
<if test="name!=null">
and ur.user_name = #{name }
</if>
</where>
这是一个节点对象,所以会走nodeType = 1
child.getNode().getNodeType() == Node.ELEMENT_NODE
通过节点名称 选择节点处理器
NodeHandler handler = nodeHandlerMap.get(nodeName);
处理节点
handler.handleNode(child, contents);
这个节点是where,也是最后一个节点,where 里面又有节点
先进入 WhereHandler
WhereHandler #handleNode
private class WhereHandler implements NodeHandler {
public WhereHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
targetContents.add(where);
}
}
又递归掉回来啦 处理里面的 if 节点
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
会处理 \n 空格,并且会将空格 转化成 静态的文本sql 节点
contents.add(new StaticTextSqlNode(data));
再处理 if 节点 ,第一个 if 节点里面有 and ,正确的 sql 语句中 where 后面跟着的不是 and 。
<if test="id!=null">
and ur.id = #{ id}
</if>
再回到 IfHandler 里面 又回来啦
if 里面的 and ur.id = #{ id} 并不是节点,所以 会放到 节点的集合里面去,注意这个节点是在 where 节点里面的,where 节点的解析还未完成
解析完第一个 if 节点 并封装成 一个 MixedSqlNode 对象
new MixedSqlNode(contents);
再回去 IfHandler # handleNode 里面的第二行
private class IfHandler implements NodeHandler {
public IfHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
String test = nodeToHandle.getStringAttribute("test");
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
}
targetContents 目标 Contents 就是刚刚那个节点集合
此时节点里面还未将 第一个 and 去掉
再来处理 where 里面的第二个 if 节点
处理之前还会处理一次 \n 的空格 并且
contents.add(new StaticTextSqlNode(data));
再调回来 将 and ur.user_name = #{name } 生成一个静态文本节点,放到节点集合,和上面的 if 节点一样,属于 where 的节点集合
contents.add(new StaticTextSqlNode(data));
处理完 第二个 if 节点 再处理一次 \n 空格 生成一个
contents.add(new StaticTextSqlNode(data));
再回到 WhereHandler # handleNode 的第二行
private class WhereHandler implements NodeHandler {
public WhereHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
targetContents.add(where);
}
}
将where 节点信息 封装成一个 WhereSqlNode
WhereSqlNode 继承了 TrimSqlNode 顾名思义
处理空格的节点(TrimHandler)再返回之前解析的代码
解析完一共有三个节点
select * from user_bo ur
where 节点
空格
where节点 一共5个
最后封装成动态 sql 对象 DynamicSqlSource 保存到 MappedStatement。
运行期间参数赋值
运行期间为 ${xxx} 赋值
运行期间为 ${xxx} 赋值 时需要参数类型与数据库字段类型匹配
既然是为 ${xxx} 赋值,那么肯定是一段动态 sql ,但是动态 sql 也可能不包含 ${xxx} 符号修饰的参数,文中的案例使用 id=“selectAll03” 的例子。
PS:如果此动态 sql 没有 ${xxx} 符号修饰,还是会处理此 sql 。
比如案例中的 处理完 后
select * from user_bo ur where ur.id = #{ id} and ur.name = xxx
where 是怎么动态添加的,第一个 and 又是怎么去掉的
<select id="selectAll03" parameterType="u" resultType="u">
select * from user_bo ur
<where>
<if test="id!=null">
and ur.id = #{ id}
</if>
<if test="name!=null">
and ur.user_name = ${name }
</if>
</where>
</select>
源码:
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
executor.query:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
ms.getBoundSql(parameterObject):
BoundSql boundSql = sqlSource.getBoundSql(parameterObject) 有删减
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
rootSqlNode.apply(context) 关键代码
rootSqlNode 类型为 MixedSqlNode
三种策略都会执行
第一种 StaticTextSqlNode
第二种 WhereSqlNode 这个节点里面又有五种,上面解析过的
回来循环第二种 WhereSqlNode
第二种的 第一个,第三个,第五个 StaticTextSqlNode 都是处理 \n 空格
我们先看第一个 ifSqlNode ,先 ifSqlNode 再跳转 MixedSqlNode
再跳转 StaticTextSqlNode 处理空格
处理第二个 IfSqlNode
这一段里面有一个 ${xxx} 修饰的参数,会进入到参数赋值
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}
parse(text):部分代码
builder.append(handler.handleToken(expression.toString()));
然后通过类型进行赋值
赋值完成之后再回到 那个 for 循环里面 再处理 where 标签内返回的结果
先全部 大写 AND UR.ID = #{ ID} AND UR.USER_NAME = AAA
public void applyAll() {
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
if (trimmedUppercaseSql.length() > 0) {
applyPrefix(sqlBuffer, trimmedUppercaseSql);
applySuffix(sqlBuffer, trimmedUppercaseSql);
}
delegate.appendSql(sqlBuffer.toString());
}
去掉第一个 and applyPrefix(sqlBuffer, trimmedUppercaseSql);
TrimSqlNode 在创建构造方法的时候将 WHERE 传进来啦
将 where 放在 最前面
当前的 sql 为 WHERE ur.id = #{ id} and ur.user_name = aaa处理后缀 applySuffix(sqlBuffer, trimmedUppercaseSql);
处理完 ${xxx} 后再返回 处理 #{xxx} 变成 ?占位符
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
SqlSourceBuilder # parse 进行占位符替换
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql;
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);
}
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
占位符 完成替换后 再返回到 具体的查询方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
创建缓存 key 进行查询方法 ,先根据key 从二级缓存中去拿,再从一级缓存中去拿,最后去查询
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
由四大对象中的 ParameterHandler 来处理参数赋值操作
运行期间为 为占位符赋值
stmt = prepareStatement(handler, ms.getStatementLog());
原生 JDBC 完成 预编译工作
再回来行进参数赋值
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
开始赋值,先获取参数类型,在通过策略模式 使用该参数类型对象完成参数赋值,也是封装的 JDBC
通过基类路由进来,完成具体的赋值操作
再看一下流程图 2 解析 sql 的 SqlSource
再看一下流程图 3 执行 sql 语句 完成 SqlSource 的预编译及${xxx} 的赋值
最终返回到 query 方法交给子类去doQuery
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}