自实现
先不谈mybatis是怎么解析SQL的,我们不妨自己先设计一个SQL解析,最基本的功能是,在解析sql的时候,要把接口参数替换到sql里去。
先设定要实现的sql如下,完整的代码见github。
1 | <select id="getInfoById" parameterType="java.lang.Long" |
为了满足参数替换,我写出如下简单的sql解析方法,传入sql语句和参数列表,用正则匹配出所有#{}的占位符,用参数挨个替换。
1 | public String parseSql(String sql, Object[] args) { |
这个方法但对于一个框架的sql解析来说,用正则匹配的话代码可读性差,且不利于灵活修改。并且如果sql是比较动态的,例如
1 | <select id="getInfoById" parameterType="java.lang.Long" |
要怎么解析呢?结合mybatis的功能,我们的解析还需要实现如下功能:
- 动态节点:动态 SQL 逻辑的节点,如
<if>
,<choose>
,<where>
,<set>
,<foreach>
等。比如对于if节点,在拼接sql前会先判断是否满足if节点里的内容。 - 动态 SQL 逻辑:在这些特殊节点中,可以使用 OGNL(Object-Graph Navigation Language)表达式以及条件判断语句来根据不同情况动态生成 SQL 片段。
- SQL 拼接顺序:MyBatis 会按照 XML 文件中节点的排列顺序来拼接最终的 SQL 语句,考虑到各个节点之间的逻辑关系。
- 字段映射:通过配置 ResultMap,MyBatis 可以将数据库查询结果映射到 Java 实体类的属性上,确保查询结果正确地映射到实体对象中。
- 结果集格式转化:MyBatis 还提供了 TypeHandler 机制,在查询结果映射到 Java 对象时,可以通过自定义 TypeHandler 来实现不同类型之间的转换,从而对结果集进行统一的格式转化。
一起来看看mybatis是怎么做的吧
原理解析
先说结论,mybatis会把每一个xml节点解析成不同的SqlNode,比如
mybatis解析动态SQL主要是parseScriptNode()这个方法,这里以If节点的解析为例,附上源码以及简化后的伪代码进行讲解。去掉一些无关的内容改成伪代码如下:
1 | List<SqlNode> parseDynamicTags(XNode node) { |
首先从\
第二个节点就是动态节点了,动态节点需要调用其重写的handleNode()方法,把处理逻辑交给每个动态节点的apply()去处理。一般在在动态节点的handleNode()方法里,会递归地调用parseDynamicTags(),去依次获取遍历自己的每一个子节点,再把自己放入content。
第三个节点的空的文本节点,把自身文本节点加入content里,文本内容是空。
第四个节点是IF动态节点,在动态节点的handleNode()方法里递归地调用parseDynamicTags(),去依次遍历获取自己的每一个子节点,等递归结束后构造自己IF节点,自身IF动态节点加入content里。
第五个节点是IF动态节点的子IF动态节点,由于它没有子节点了,自身IF动态节点加入content里,结束递归返回。
第六个节点的IF动态节点的子文本节点,把自身文本节点加入content里,文本内容是空。结束本层递归返回上一层。
第七个节点是文本节点,把自身文本节点加入content里,文本内容是”LIMIT 1”。
完整源码走读
附上从mybatis启动开始,怎么执行到解析动态sql的,这部分了解即可。
从最开始讲起,在从mybatis启动开始,会加载xml文件并进行解析。org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement
具体看看下面这行代码对于select|insert|update|delete增删改查节点是怎么解析的
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
evalNodes()方法已经对xml进行了解析,把该节点内所有的”<…>“都转化成了XNode,返回一个List
接着我们跳入org.apache.ibatis.builder.xml.XMLMapperBuilder#buildStatementFromContext
跳入org.apache.ibatis.builder.xml.XMLMapperBuilder#buildStatementFromContext(java.util.List<org.apache.ibatis.parsing.XNode>, java.lang.String)
跳入org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
parseStatementNode()方法把增删改查节点还有一些特有的属性,比如parameterType、resultMap、resultSetType、useGeneratedKeys,解析出来的。
重点代码是下面这行,返回了一个SqlSource。这里的LanguageDriver默认会使用XMLLanguageDriver创建SqlSource(Configuration构造函数中设置)。
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
跳入org.apache.ibatis.scripting.xmltags.XMLLanguageDriver#createSqlSource
跳入org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
到这里,我们就可以不跳来跳去了,慢下来仔细看看动态SQL的实际执行代码。
1 | public SqlSource parseScriptNode() { |