前言

废话不多说,直接进入文章。

我们在使用mybatis的时候,会在xml中编写sql语句。

比如这段动态sql代码:

  1. <update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
  2. UPDATE users
  3. <trim prefix="SET" prefixOverrides=",">
  4. <if test="name != null and name != ''">
  5. name = #{name}
  6. </if>
  7. <if test="age != null and age != ''">
  8. , age = #{age}
  9. </if>
  10. <if test="birthday != null and birthday != ''">
  11. , birthday = #{birthday}
  12. </if>
  13. </trim>
  14. where id = ${id}
  15. </update>

mybatis底层是如何构造这段sql的?

这方面的知识网上资料不多,于是就写了这么一篇文章。

下面带着这个疑问,我们一步一步分析。

介绍MyBatis中一些关于动态SQL的接口和类

SqlNode接口,简单理解就是xml中的每个标签,比如上述sql的update,trim,if标签:

  1. public interface SqlNode {
  2. boolean apply(DynamicContext context);
  3. }

SqlSource Sql源接口,代表从xml文件或注解映射的sql内容,主要就是用于创建BoundSql,有实现类DynamicSqlSource(动态Sql源),StaticSqlSource(静态Sql源)等:

  1. public interface SqlSource {
  2. BoundSql getBoundSql(Object parameterObject);
  3. }

BoundSql类,封装mybatis最终产生sql的类,包括sql语句,参数,参数源数据等参数:

XNode,一个Dom API中的Node接口的扩展类。

BaseBuilder接口及其实现类(属性,方法省略了,大家有兴趣的自己看),这些Builder的作用就是用于构造sql:

下面我们简单分析下其中4个Builder:

1 XMLConfigBuilder

解析mybatis中configLocation属性中的全局xml文件,内部会使用XMLMapperBuilder解析各个xml文件。

2 XMLMapperBuilder

遍历mybatis中mapperLocations属性中的xml文件中每个节点的Builder,比如user.xml,内部会使用XMLStatementBuilder处理xml中的每个节点。

3 XMLStatementBuilder

解析xml文件中各个节点,比如select,insert,update,delete节点,内部会使用XMLScriptBuilder处理节点的sql部分,遍历产生的数据会丢到Configuration的mappedStatements中。

4 XMLScriptBuilder

解析xml中各个节点sql部分的Builder。

LanguageDriver接口及其实现类(属性,方法省略了,大家有兴趣的自己看),该接口主要的作用就是构造sql:

简单分析下XMLLanguageDriver(处理xml中的sql,RawLanguageDriver处理静态sql):

XMLLanguageDriver内部会使用XMLScriptBuilder解析xml中的sql部分。

ok, 大部分比较重要的类我们都已经介绍了,下面源码分析走起。

源码分析走起

Spring与Mybatis整合的时候需要配置SqlSessionFactoryBean,该配置会加入数据源和mybatis xml配置文件路径等信息:

  1. <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  2. <property name="dataSource" ref="dataSource"/>
  3. <property name="configLocation" value="classpath:mybatisConfig.xml"/>
  4. <property name="mapperLocations" value="classpath*:org/format/dao/*.xml"/>
  5. </bean>

我们就分析这一段配置背后的细节:

SqlSessionFactoryBean实现了Spring的InitializingBean接口,InitializingBean接口的afterPropertiesSet方法中会调用buildSqlSessionFactory方法

buildSqlSessionFactory方法内部会使用XMLConfigBuilder解析属性configLocation中配置的路径,还会使用XMLMapperBuilder属性解析mapperLocations属性中的各个xml文件。

部分源码如下:

由于XMLConfigBuilder内部也是使用XMLMapperBuilder,我们就看看XMLMapperBuilder的解析细节。

我们关注一下,增删改查节点的解析。

XMLStatementBuilder的解析:

默认会使用XMLLanguageDriver创建SqlSource(Configuration构造函数中设置)。

XMLLanguageDriver创建SqlSource:

XMLScriptBuilder解析sql:

得到SqlSource之后,会放到Configuration中,有了SqlSource,就能拿BoundSql了,BoundSql可以得到最终的sql。

实例分析

我以以下xml的解析大概说下parseDynamicTags的解析过程:

  1. <update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
  2. UPDATE users
  3. <trim prefix="SET" prefixOverrides=",">
  4. <if test="name != null and name != ''">
  5. name = #{name}
  6. </if>
  7. <if test="age != null and age != ''">
  8. , age = #{age}
  9. </if>
  10. <if test="birthday != null and birthday != ''">
  11. , birthday = #{birthday}
  12. </if>
  13. </trim>
  14. where id = ${id}
  15. </update>

在看这段解析之前,请先了解dom相关的知识,xml dom知识, dom博文

parseDynamicTags方法的返回值是一个List,也就是一个Sql节点集合。SqlNode本文一开始已经介绍,分析完解析过程之后会说一下各个SqlNode类型的作用。

1 首先根据update节点(Node)得到所有的子节点,分别是3个子节点

(1)文本节点 \n UPDATE users

(2)trim子节点 ...

(3)文本节点 \n where id = #{id}

2 遍历各个子节点

(1) 如果节点类型是文本或者CDATA,构造一个TextSqlNode或StaticTextSqlNode

(2) 如果节点类型是元素,说明该update节点是个动态sql,然后会使用NodeHandler处理各个类型的子节点。这里的NodeHandler是XMLScriptBuilder的一个内部接口,其实现类包括TrimHandler、WhereHandler、SetHandler、IfHandler、ChooseHandler等。看类名也就明白了这个Handler的作用,比如我们分析的trim节点,对应的是TrimHandler;if节点,对应的是IfHandler...

这里子节点trim被TrimHandler处理,TrimHandler内部也使用parseDynamicTags方法解析节点

3 遇到子节点是元素的话,重复以上步骤

trim子节点内部有7个子节点,分别是文本节点、if节点、是文本节点、if节点、是文本节点、if节点、文本节点。文本节点跟之前一样处理,if节点使用IfHandler处理

遍历步骤如上所示,下面我们看下几个Handler的实现细节。

IfHandler处理方法也是使用parseDynamicTags方法,然后加上if标签必要的属性。

  1. private class IfHandler implements NodeHandler {
  2. public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
  3. List<SqlNode> contents = parseDynamicTags(nodeToHandle);
  4. MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
  5. String test = nodeToHandle.getStringAttribute("test");
  6. IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
  7. targetContents.add(ifSqlNode);
  8. }
  9. }

TrimHandler处理方法也是使用parseDynamicTags方法,然后加上trim标签必要的属性。

  1. private class TrimHandler implements NodeHandler {
  2. public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
  3. List<SqlNode> contents = parseDynamicTags(nodeToHandle);
  4. MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
  5. String prefix = nodeToHandle.getStringAttribute("prefix");
  6. String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
  7. String suffix = nodeToHandle.getStringAttribute("suffix");
  8. String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
  9. TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
  10. targetContents.add(trim);
  11. }
  12. }

以上update方法最终通过parseDynamicTags方法得到的SqlNode集合如下:

trim节点:

由于这个update方法是个动态节点,因此构造出了DynamicSqlSource。

DynamicSqlSource内部就可以构造sql了:

DynamicSqlSource内部的SqlNode属性是一个MixedSqlNode。

然后我们看看各个SqlNode实现类的apply方法

下面分析一下两个SqlNode实现类的apply方法实现:

MixedSqlNode:

  1. public boolean apply(DynamicContext context) {
  2. for (SqlNode sqlNode : contents) {
  3. sqlNode.apply(context);
  4. }
  5. return true;
  6. }

MixedSqlNode会遍历调用内部各个sqlNode的apply方法。

StaticTextSqlNode:

  1. public boolean apply(DynamicContext context) {
  2. context.appendSql(text);
  3. return true;
  4. }

直接append sql文本。

IfSqlNode:

  1. public boolean apply(DynamicContext context) {
  2. if (evaluator.evaluateBoolean(test, context.getBindings())) {
  3. contents.apply(context);
  4. return true;
  5. }
  6. return false;
  7. }

这里的evaluator是一个ExpressionEvaluator类型的实例,内部使用了OGNL处理表达式逻辑。

TrimSqlNode:

  1. public boolean apply(DynamicContext context) {
  2. FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
  3. boolean result = contents.apply(filteredDynamicContext);
  4. filteredDynamicContext.applyAll();
  5. return result;
  6. }
  7. public void applyAll() {
  8. sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
  9. String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
  10. if (trimmedUppercaseSql.length() > 0) {
  11. applyPrefix(sqlBuffer, trimmedUppercaseSql);
  12. applySuffix(sqlBuffer, trimmedUppercaseSql);
  13. }
  14. delegate.appendSql(sqlBuffer.toString());
  15. }
  16. private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
  17. if (!prefixApplied) {
  18. prefixApplied = true;
  19. if (prefixesToOverride != null) {
  20. for (String toRemove : prefixesToOverride) {
  21. if (trimmedUppercaseSql.startsWith(toRemove)) {
  22. sql.delete(0, toRemove.trim().length());
  23. break;
  24. }
  25. }
  26. }
  27. if (prefix != null) {
  28. sql.insert(0, " ");
  29. sql.insert(0, prefix);
  30. }
  31. }
  32. }

TrimSqlNode的apply方法也是调用属性contents(一般都是MixedSqlNode)的apply方法,按照实例也就是7个SqlNode,都是StaticTextSqlNode和IfSqlNode。 最后会使用FilteredDynamicContext过滤掉prefix和suffix。

总结

大致讲解了一下mybatis对动态sql语句的解析过程,其实回过头来看看不算复杂,还算蛮简单的。 之前接触mybaits的时候遇到刚才分析的那一段动态sql的时候总是很费解。

  1. <update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
  2. UPDATE users
  3. <trim prefix="SET" prefixOverrides=",">
  4. <if test="name != null and name != ''">
  5. name = #{name}
  6. </if>
  7. <if test="age != null and age != ''">
  8. , age = #{age}
  9. </if>
  10. <if test="birthday != null and birthday != ''">
  11. , birthday = #{birthday}
  12. </if>
  13. </trim>
  14. where id = ${id}
  15. </update>

想搞明白这个trim节点的prefixOverrides到底是什么意思(从字面上理解就是前缀覆盖),而且官方文档上也没这方面知识的说明。我将这段xml改成如下:

  1. <update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">
  2. UPDATE users
  3. <trim prefix="SET" prefixOverrides=",">
  4. <if test="name != null and name != ''">
  5. , name = #{name}
  6. </if>
  7. <if test="age != null and age != ''">
  8. , age = #{age}
  9. </if>
  10. <if test="birthday != null and birthday != ''">
  11. , birthday = #{birthday}
  12. </if>
  13. </trim>
  14. where id = ${id}
  15. </update>

(第二段第一个if节点多了个逗号) 结果我发现这2段xml解析的结果是一样的,非常迫切地想知道这到底是为什么,然后这也促使了我去看源码的决心。最终还是看下来了。

文章有点长,而且讲的也不是非常直观,希望对有些人有帮助。

Mybatis解析动态sql原理分析的更多相关文章

  1. MyBatis框架——动态SQL、缓存机制、逆向工程

    MyBatis框架--动态SQL.缓存机制.逆向工程 一.Dynamic SQL 为什么需要动态SQL?有时候需要根据实际传入的参数来动态的拼接SQL语句.最常用的就是:where和if标签 1.参考 ...

  2. 使用Mybatis实现动态SQL(一)

    使用Mybatis实现动态SQL 作者 : Stanley 罗昊 [转载请注明出处和署名,谢谢!] 写在前面:        *本章节适合有Mybatis基础者观看* 前置讲解 我现在写一个查询全部的 ...

  3. mybatis中的.xml文件总结——mybatis的动态sql

    resultMap resultType可以指定pojo将查询结果映射为pojo,但需要pojo的属性名和sql查询的列名一致方可映射成功. 如果sql查询字段名和pojo的属性名不一致,可以通过re ...

  4. 利用MyBatis的动态SQL特性抽象统一SQL查询接口

    1. SQL查询的统一抽象 MyBatis制动动态SQL的构造,利用动态SQL和自定义的参数Bean抽象,可以将绝大部分SQL查询抽象为一个统一接口,查询参数使用一个自定义bean继承Map,使用映射 ...

  5. 使用Mybatis实现动态SQL(二)

    使用Mybatis实现动态SQL 作者 : Stanley 罗昊 [转载请注明出处和署名,谢谢!] 写在前面:        *本章节适合有Mybatis基础者观看* 使用Mybatis实现动态SQL ...

  6. MyBatis的动态SQL详解

    MyBatis的动态SQL是基于OGNL表达式的,它可以帮助我们方便的在SQL语句中实现某些逻辑,本文详解mybatis的动态sql,需要的朋友可以参考下 MyBatis 的一个强大的特性之一通常是它 ...

  7. mybatis 使用动态SQL

    RoleMapper.java public interface RoleMapper { public void add(Role role); public void update(Role ro ...

  8. MyBatis探究-----动态SQL详解

    1.if标签 接口中方法:public List<Employee> getEmpsByEmpProperties(Employee employee); XML中:where 1=1必不 ...

  9. mybatis.5.动态SQL

    1.动态SQL,解决关联sql字符串的问题,mybatis的动态sql基于OGNL表达式 if语句,在DeptMapper.xml增加如下语句; <select id="selectB ...

随机推荐

  1. mysql substring_index substring left right方法

    函数简介: SUBSTRING(str,pos) , SUBSTRING(str FROM pos) SUBSTRING(str,pos,len) , SUBSTRING(str FROM pos F ...

  2. 随便选择两个城市作为预选旅游目标。实现两个独立的线程分别显示10次城市名,每次显示后休眠一段随机时间(1000ms以内),哪个先显示完毕,就决定去哪个城市。分别用Runnable接口和Thread类实现。

    public class Testlvyou extends Thread{ @Override public void run() { test(); } private void test() { ...

  3. java的finalize()函数

    在说明finalize()的用法之前要树立有关于java垃圾回收器几个观点: "对象可以不被垃圾回收" : java的垃圾回收遵循一个特点, 就是能不回收就不会回收.只要程序的内存 ...

  4. 安装mysql odbc遇到error 1918.errror installing ODBC driver mysql ODBC 5.3 ANSI Drive

    环境:Windows server2008r2 安装mysql-connector-odbc-5.3.6-win32 报错 相信错误信息:Error 1918.errror installing OD ...

  5. C#读取XML文件

    --硬盘Xml文件存储路径:d:\xmlFile\Testxml.xml xml文件内容: <Root> <Tab> <ID>245575913</ID> ...

  6. android 布局下划线

    <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_cont ...

  7. Type mismatch: cannot convert from MainFragment to Fragment 报错

    源码: FragmentTransaction mFragmentTranscation = getSupportFragmentManager().beginTransaction(); Fragm ...

  8. [转载] Linux启动过程详解-《别怕Linux编程》之八

    本原创文章属于<Linux大棚>博客,博客地址为http://roclinux.cn.文章作者为rocrocket.为了防止某些网站的恶性转载,特在每篇文章前加入此信息,还望读者体谅. = ...

  9. EarthWarrior3D游戏ios源码

    这是一款不错的ios源码源码,EarthWarrior3D游戏源码, 并且游戏源代码支持多平台. 适用于cocos v2.1.0.0版本 源码下载:http://code.662p.com/view/ ...

  10. DirectX API 编程起步 #01 项目设置

    =========================================================== 目录: DirectX API 编程起步 #02 窗口的诞生 DirectX A ...