文章目录
- 介绍
- 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初始化
@Overridepublic 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 中的 ? 占位符上,因此在参数传递过程中能确保参数的正确替换位置。