【MyBatis源码】SqlSource对象创建流程
文章目录
- 介绍
- XMLScriptBuilder初始化
- parseDynamicTags解析动态节点
- RawSqlSource分析
- 代码分析
- 实例化
介绍
代码入口:
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
languageRegistry:用于注册LanguageDriver,LanguageDriver用于解析SQL配置,将配置信息转换为SqlSource对象。
MyBatis中的SqlSource用于描述SQL资源,MyBatis可以通过两种方式配置SQL信息,一种是通过@Selelect、@Insert、@Delete、@Update或者@SelectProvider、@InsertProvider、@DeleteProvider、@UpdateProvider等注解;另一种是通过XML配置文件。SqlSource就代表Java注解或者XML文件配置的SQL资源。
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}
SqlSource接口的定义非常简单,只有一个getBoundSql()方法,该方法返回一个BoundSql实例。BoundSql是对SQL语句及参数信息的封装,它是SqlSource解析后的结果。如图9-1所示,SqlSource接口有4个不同的实现,分别为StaticSqlSource、DynamicSqlSource、RawSqlSource和ProviderSqlSource。
这4种SqlSource实现类的作用如下。
ProviderSqlSource:用于描述通过@Select、@SelectProvider等注解配置的SQL资源信息。DynamicSqlSource:用于描述Mapper XML文件中配置的SQL资源信息,这些SQL通常包含动态SQL配置或者${}参数占位符,需要在Mapper调用时才能确定具体的SQL语句。
RawSqlSource:用于描述Mapper XML文件中配置的SQL资源信息,与DynamicSqlSource不同的是,这些SQL语句在解析XML配置的时候就能确定,即不包含动态SQL相关配置。
StaticSqlSource:用于描述ProviderSqlSource、DynamicSqlSource及RawSqlSource解析后得到的静态SQL资源。
XMLScriptBuilder初始化
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
在 MyBatis 源码中,initNodeHandlerMap 方法的作用是初始化一个用于处理不同节点类型的映射表(nodeHandlerMap)。每个节点类型(例如 “trim”、“where”、“set” 等)对应一个处理器(例如 TrimHandler、WhereHandler 等)。这些处理器负责解析和处理相应的 SQL 语句节点。
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());
}
• trim:用于去除 SQL 语句两端的空格或特定字符。
• where:用于生成 WHERE 子句。
• set:用于生成 SET 子句,通常在更新操作中使用。
• foreach:用于处理集合中的元素,通常用于生成批量插入或更新的 SQL。
• if:用于根据条件动态生成 SQL 片段。
• choose、when、otherwise:类似于 Java 的 switch-case 语句,用于动态选择生成不同的 SQL 片段。
parseDynamicTags解析动态节点
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));
// 如果子节点是文本节点或 CDATA 节点,提取其文本内容并创建 TextSqlNode 对象
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE
|| child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
// 如果是动态的,添加到 contents 列表中,并标记为动态;否则,创建一个静态文本节点并添加
if (textSqlNode.isDynamic()) {
// 检查这个文本节点是否为动态,即是否包含 ${}动态表达式
contents.add(textSqlNode);
isDynamic = true;
} else {
// 将带有#{}符号的SQL封装到StaticTextSqlNode节点上
contents.add(new StaticTextSqlNode(data));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
// 如果子节点是元素节点,获取节点名称并查找对应的处理器(NodeHandler)
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;
}
}
// 将所有解析的 SQL 节点封装到 MixedSqlNode 对象中并返回
return new MixedSqlNode(contents);
}
以下面这个SQL语句作为范例进行描述解析
select * from t_user where id = #{id}
<choose>
<when test="name != null">
AND name like #{name}
</when>
<otherwise>
AND name = #{name}
</otherwise>
</choose>
select * from t_user where id = #{id} 文本走的是Node.TEXT_NODE
而对于 都属于Node.ELEMENT_NODE
如果子节点是元素节点,获取节点名称并查找对应的处理器(NodeHandler)
// 如果子节点是元素节点,获取节点名称并查找对应的处理器(NodeHandler)
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;
解析完各个Node节点后,封装SqlSource
public SqlSource parseScriptNode() {
// 解析动态 SQL 标签,并生成一个 MixedSqlNode 对象
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
// 如果是包含${}符号或者包含动态SQL的元素节点
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
// 纯文本SQL,仅包含#{}
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
RawSqlSource分析
代码分析
RawSqlSource 的作用是将没有动态 SQL 标签的 SQL 语句(例如 、 等)直接解析成静态 SQL,避免了动态解析的开销。它会将 SQL 中的 #{} 占位符参数解析成 StaticSqlSource,然后存储为 BoundSql 对象,以便在执行时直接使用。
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
// 解析SQL语句
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
// 获取入参类型
Class<?> clazz = parameterType == null ? Object.class : parameterType;
//解析SQL语句
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}
下面这段代码是 SqlSourceParser 的 parse 方法,它负责将包含 #{} 占位符的 SQL 字符串解析为 StaticSqlSource 对象。StaticSqlSource 表示一个纯静态的 SQL 源,适合不含动态 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());
}
ParameterMappingTokenHandler 是 MyBatis 中一个用于处理 #{} 占位符的处理器。它会根据 parameterType 和 additionalParameters 等信息,将 #{} 占位符替换为相应的 JDBC 参数标记(如 ?),并生成对应的 ParameterMapping 列表,用于描述每个参数的类型和属性。
GenericTokenParser 是 MyBatis 中一个通用的占位符解析器,它能够识别带有指定前后缀的占位符。这里的 parser 会针对 #{} 占位符进行解析,将匹配到的 #{} 内的内容交给 handler 处理。
通过 GenericTokenParser 对 SQL 字符串进行解析,处理 #{} 占位符。
removeExtraWhitespaces(originalSql) 方法会去除多余的空白字符,确保 SQL 语句结构更简洁。
最终的 sql 字符串中,#{} 占位符将被替换为 ?,以便后续参数绑定
解析完成后,parse 方法会返回一个 StaticSqlSource 对象。StaticSqlSource 是 MyBatis 中用于表示静态 SQL 的 SqlSource 实现,适合不包含动态逻辑的 SQL。handler.getParameterMappings() 获取所有参数的映射信息,以便在执行时能够将参数正确绑定到 SQL 的 ?占位符上。
实例化
示例SQL:
select * from t_user where id = #{id} name = #{name}
parse 方法的整体流程是:解析 #{} 占位符,将其替换为 ?,并记录参数映射信息。
经过 parse 处理后,原始 SQL 将被替换为:select * from t_user where id = ? name = ?
handler.getParameterMappings() 的作用是返回一个 ParameterMapping 列表,用于描述 SQL 中的 #{} 占位符参数的映射信息。ParameterMapping 对象包含了每个参数的名称、类型、Java 属性等,这些信息在 SQL 执行时用于将实际参数绑定到 ? 占位符上。
ParameterMappingTokenHandler 会生成一个 ParameterMapping 列表,包含两个参数的映射信息。
在 SQL 执行时,MyBatis 会使用 parameterMappings 列表来将 User 对象中的 id 和 name 属性值绑定到 SQL 中的 ? 占位符上。
ParameterMappingTokenHandler 在解析 #{} 占位符时,会将每个参数按出现的顺序记录到 parameterMappings列表中。比如,#{id} 出现在 #{name} 之前,那么 parameterMappings 列表中,id 的映射会排在 name 之前。MyBatis 通过 parameterMappings 中记录的参数顺序、名称等信息,将参数依次正确绑定到 SQL 中的 ? 占位符上,因此在参数传递过程中能确保参数的正确替换位置。