Mybatis源码系列文章
Mybatis源码解析(四):sql语句及#{}、${}的解析
Mybatis源码解析(五):SqlSession会话的创建
前言
- 本文主要讲解<select><insert><update><delete>标签内将sql语句替换#{}和${}为?、并保存下来属性值、入参对象(字符串|对象|Map)组装成BoundSql,后续执行JDBC操作需要的值从此对象中获取
- 承接上一篇文章Mybatis源码解析(三):映射配置文件的解析,映射配置文件会解析成MappedStatement对象,而BoundSql只是其中一部分
- 如下认识下BoundSql类
一、解析入口
- 这里说明一下:获取BoundSql对象通过SqlSource的getBoundSql方法获取,构建SqlSource则是创建BoundSql
- configuration:全局配置文件,我们最终创建的对象SqlSource要挂在MappedStatement对象(每一个实体映射配置文件)下,然后MappedStatement又要挂在Configuration下面
- context:<select>标签的属性及sql语句组成的XNode对象
- parameterTypeClass:入参对象的Class
...
// *******创建SqlSource,解析SQL,封装SQL语句(未参数绑定)和入参信息
// 问题:sql占位符如何进行的替换?动态sql如何进行的解析?
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
...
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}
进入createSqlSource创建方法
- 与解析全局配置文件和映射配置文件套路一样,都是先创建一个xxxBuilder对象,然后再调用解析方法
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
// 初始化了动态SQL标签处理器
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
// 解析动态SQL
return builder.parseScriptNode();
}
进入XMLScriptBuilder对象构造方法
- 这里主要作用加载动态sql标签的处理器,后面创建sql(带?)时,如果是动态sql则需要通过对应处理器动态构建sql
public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
super(configuration);
this.context = context;
this.parameterType = parameterType;
// 初始化动态SQL中的节点处理器集合
initNodeHandlerMap();
}
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}
二、解析SQL
- 以上准备工作已完成,接下来开始解析工作
- 回到createSqlSource创建方法,进入builder.parseScriptNode();解析方法
- parseDynamicTags:解析动态标签
- RawSqlSource和DynamicSqlSource都是在创建BoundSql的构建方法getBoundSql
public SqlSource parseScriptNode() {
// ****将带有${}号的SQL信息封装到TextSqlNode
// ****将带有#{}号的SQL信息封装到StaticTextSqlNode
// ****将动态SQL标签中的SQL信息分别封装到不同的SqlNode中
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
// 如果SQL中包含${}和动态SQL语句,则将SqlNode封装到DynamicSqlSource
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
// 如果SQL中包含#{},则将SqlNode封装到RawSqlSource中,并指定parameterType
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
1、解析动态标签
parseDynamicTags方法流程图,下面代码结合着流程图看
进入parseDynamicTags解析动态标签方法
- 方法总结:解析select\insert\ update\delete标签中的SQL语句,最终将解析到的SqlNode封装到MixedSqlNode中的List集合中
- 将带有${}号的SQL信息封装到TextSqlNode
- 将带有#{}号的SQL信息封装到StaticTextSqlNode
- 将动态SQL标签中的SQL信息分别封装到不同的SqlNode
- 如下图:一个<select>标签分两部分,文本节点和元素节点;XNode node传递过来的正是此标签解析的对象
- SQL语句中带有${}的话,就是表达式文本
- isDynamic:是否动态sql标志
- #{}:带?的sql可以直接生成(因为不会变化),调用时候直接获取
- ${}或动态sql:每次传入参数不同,带?的sql会变,调用时候生成
- nodeHandlerMap是上面XMLScriptBuilder构造方法里初始化的动态sql标签的处理器map集合;然后将对应标签的SqlNode对象添加到contents中,如:ChooseSqlNode、IfSqlNode
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
//获取<select>\<insert>等4个标签的子节点,子节点包括元素节点和文本节点
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("");
// 将文本内容封装到SqlNode中
TextSqlNode textSqlNode = new TextSqlNode(data);
// SQL语句中带有${}的话,就表示是dynamic的
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
// SQL语句中(除了${}和下面的动态SQL标签),就表示是static的
// StaticTextSqlNode的apply只是进行字符串的追加操作
contents.add(new StaticTextSqlNode(data));
}
//处理元素节点
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
String nodeName = child.getNode().getNodeName();
// 动态SQL标签处理器
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
// 动态SQL标签是dynamic的
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
2、带#{}SQL构建BoundSql
进入RawSqlSource构造方法
- getSql方法:从SqlNode对象中获取String类型sql语句(目前还是不带?的#{}状态)
- SqlSourceBuilder构造方法则是参数赋值,主要看下面的parse解析方法
public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
//先调用 getSql(configuration, rootSqlNode)获取sql,再走下面的构造函数
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
// 解析SQL语句
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
// 获取入参类型
Class<?> clazz = parameterType == null ? Object.class : parameterType;
// 开始解析
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
进入parse解析方法
- 这里先是创建解析器,并将#{}符号确定下来,真正的解析内容在parser.parse中
- StaticSqlSource:最后获取BoundSql通过此对象getBoundSql方法
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);
}
// 将解析之后的SQL信息,封装到StaticSqlSource对象中
// SQL字符串是带有?号的字符串,?相关的参数信息,封装到ParameterMapping集合中
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
1)parser.parse解析
- 传入参数originalSql是带#{}的sql,content则是把“#{”和“}"去掉的属性值
- 这里只截取了核心代码,主要有两个作用
- 将sql语句带#{属性值}解析成sql带?
- 将属性值添加到List<ParameterMapping>集合中;ParameterMapping:属性值、jdbc类型等
builder.append(handler.handleToken(expression.toString()));
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
2)查看StaticSqlSource类
- 创建完StaticSqlSource对象,则带?的sql语句,及#{}中的参数值已准备就绪,getBoundSql则是拿来创建对象返回即可
- 这也是上面所说的,不是动态sql:带?的sql是固定的,直接生成
3、带${}或动态SQL构建BoundSql
查看DynamicSqlSource类
- 创建完DynamicSqlSource对象,只是赋值对应参数,无其他动作
- 这也是上面所说的,动态sql:带?的sql是随参数会变化,只在获取时候生成
- ${}替换?与#{}一样原理,动态sql则是通过判断参数利用动态sql标签处理器叠加的处理修改sql
总结
- 本文的核心就是创建BoundSql对象,后续需要带?的sql、#{}里属性值和入参对象则通过SqlSource对象的getBoundSql获取