文档章节

Mybatis3.4.x技术内幕(十六):Mybatis之sqlFragment(可复用的sql片段)

祖大俊
 祖大俊
发布于 2016/06/05 12:41
字数 1255
阅读 2189
收藏 16

Mybatis目前最新版本为3.4.0,因此,我也将我的项目由3.3.1替换为3.4.0。在上一篇博文中,详细分析了Mybatis在使用foreach循环进行批量insert,返回主键id列表时,如果使用BatchExecutor,那么由于Mybatis存在bug,返回的id列表将是null值。很遗憾的告诉大家,Mybatis3.4.0依然是没有修复该bug的,该bug依然存在。

今天,我们将分析Mybatis之sqlFragment,可以翻译为sql片段,它的存在价值在于可复用sql片段,避免到处重复编写。

在工作中,往往有这样的需求,对于同一个sql条件查询,首先需要统计记录条数,用以计算pageCount,然后再对结果进行分页查询显示,看下面一个例子。

	<sql id="studentProperties"><!--sql片段-->
		select 
			stud_id as studId
			, name, email
			, dob
			, phone
		from students
	</sql>
	
	<select id="countAll" resultType="int">
		select count(1) from (
			<include refid="studentProperties"></include><!--复用-->
		) tmp
	</select>
	
	<select id="findAll" resultType="Student" parameterType="map">
		select * from (
			<include refid="studentProperties"></include><!--复用-->
		) tmp limit #{offset}, #{pagesize}
	</select>

这就是sqlFragment,它可以为select|insert|update|delete标签服务,可以定义很多sqlFragment,然后使用include标签引入多个sqlFragment。在工作中,也是比较常用的一个功能,它的优点很明显,复用sql片段,它的缺点也很明显,不能完整的展现sql逻辑,如果一个标签,include了四至五个sqlFragment,其可读性就非常差了。

sqlFragment里的内容是可以随意写的,它不需要是一个完整的sql,它可以是“,phone”这么简单的文本。

1.sqlFragment的解析过程

sqlFragment存储于Configuration内部。

protected final Map<String, XNode> sqlFragments = new StrictMap<XNode>("XML fragments parsed from previous mappers");

解析sqlFragment的过程非常简单。

org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XNode)方法部分源码。

// 解析sqlFragment
sqlElement(context.evalNodes("/mapper/sql"));
// 为select|insert|update|delete提供服务
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

sqlFragment存储于Map<String, XNode>结构当中。其实最关键的,是它如何为select|insert|update|delete提供服务的。

2.select|insert|update|delete标签中,解析include标签的过程

org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode()方法源码。

// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
// 重点关注的方法
includeParser.applyIncludes(context.getNode());

// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

注释“pre: <selectKey> and <include> were parsed and removed”,含义为解析完,并移除。为什么要移除呢?秘密都隐藏在applyIncludes()方法内部了。

org.apache.ibatis.builder.xml.XMLIncludeTransformer.applyIncludes(Node, Properties)方法源码。

  /**
   * Recursively apply includes through all SQL fragments.
   * @param source Include node in DOM tree
   * @param variablesContext Current context for static variables with values
   */
  private void applyIncludes(Node source, final Properties variablesContext) {
    if (source.getNodeName().equals("include")) {
      // new full context for included SQL - contains inherited context and new variables from current include node
      Properties fullContext;

      String refid = getStringAttribute(source, "refid");
      // replace variables in include refid value
      refid = PropertyParser.parse(refid, variablesContext);
      Node toInclude = findSqlFragment(refid);
      Properties newVariablesContext = getVariablesContext(source, variablesContext);
      if (!newVariablesContext.isEmpty()) {
        // merge contexts
        fullContext = new Properties();
        fullContext.putAll(variablesContext);
        fullContext.putAll(newVariablesContext);
      } else {
        // no new context - use inherited fully
        fullContext = variablesContext;
      }
      // 递归调用
      applyIncludes(toInclude, fullContext);
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      // 将include节点,替换为sqlFragment节点
      source.getParentNode().replaceChild(toInclude, source);
      while (toInclude.hasChildNodes()) {
        // 将sqlFragment的子节点(也就是文本节点),插入到sqlFragment的前面
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      // 移除sqlFragment节点
      toInclude.getParentNode().removeChild(toInclude);
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
      NodeList children = source.getChildNodes();
      for (int i=0; i<children.getLength(); i++) {
        // 递归调用
        applyIncludes(children.item(i), variablesContext);
      }
    } else if (source.getNodeType() == Node.ATTRIBUTE_NODE && !variablesContext.isEmpty()) {
      // replace variables in all attribute values
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    } else if (source.getNodeType() == Node.TEXT_NODE && !variablesContext.isEmpty()) {
      // replace variables ins all text nodes
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
  }

上面是对源码的解读,为了便于理解,我们接下来采用图示的办法,演示其过程。

3.图示过程演示

①解析节点

<select id="countAll" resultType="int">
		select count(1) from (
			<include refid="studentProperties"></include>
		) tmp
	</select>

②include节点替换为sqlFragment节点

	<select id="countAll" resultType="int">
		select count(1) from (
				<sql id="studentProperties">
					select 
						stud_id as studId
						, name, email
						, dob
						, phone
					from students
				</sql>
		) tmp
	</select>

③将sqlFragment的子节点(文本节点)insert到sqlFragment节点的前面。注意,对于dom来说,文本也是一个节点,叫TextNode。

	<select id="countAll" resultType="int">
		select count(1) from (
				select 
						stud_id as studId
						, name, email
						, dob
						, phone
					from students
				<sql id="studentProperties">
					select 
						stud_id as studId
						, name, email
						, dob
						, phone
					from students
				</sql>
		) tmp
	</select>

④移除sqlFragment节点

	<select id="countAll" resultType="int">
		select count(1) from (
				select 
						stud_id as studId
						, name, email
						, dob
						, phone
					from students
		) tmp
	</select>

⑤最终结果如图所示

(Made In QQ截图及时编辑)

如此一来,TextNode1 + TextNode2 + TextNode3,就组成了一个完整的sql。遍历select的三个子节点,分别取出TextNode的value,append到一起,就是最终完整的sql。

这也是为什么要移除<selectKey> and <include>节点的原因。

这就是Mybatis的sqlFragment,以上示例,均为静态sql,即static sql,有关动态sql,即dynamic sql,将在后续博文中进行仔细分析。

 

版权提示:文章出自开源中国社区,若对文章感兴趣,可关注我的开源中国社区博客(http://my.oschina.net/zudajun)。(经过网络爬虫或转载的文章,经常丢失流程图、时序图,格式错乱等,还是看原版的比较好)

© 著作权归作者所有

祖大俊
粉丝 802
博文 32
码字总数 52477
作品 0
昌平
私信 提问
Mybatis3.4.x技术内幕(十七):Mybatis之动态Sql设计原本(上)

上一篇博文中,介绍了可复用的sql片段,通过<include>标签进行引入,而<include>标签内一般存放的是静态sql,其实,sql片段也是可以放置动态sql标签内容。 1. Mybatis支持的动态sql及基本用法...

祖大俊
2016/08/20
3.1K
2
Mybatis3.4.x技术内幕(二十三):Mybatis面试问题集锦(大结局)

Mybatis技术内幕系列博客,从原理和源码角度,介绍了其内部实现细节,无论是写的好与不好,我确实是用心写了,由于并不是介绍如何使用Mybatis的文章,所以,一些参数使用细节略掉了,我们的目...

祖大俊
2016/09/17
15.4K
36
Mybatis3.4.x技术内幕(十八):Mybatis之动态Sql设计原本(下)

上一篇博文中,简要介绍了Mybatis动态sql的基本用法和基本设计结构,本篇博文重点阐述一些动态sql的技术细节,#{name}和${name}的区别,将在本篇博文中揭晓。也许读者早已了解它们之间的区别...

祖大俊
2016/08/21
1K
1
Mybatis3.4.x技术内幕(十九):Mybatis之plugin插件设计原理

大多数框架,都支持插件,用户可通过编写插件来自行扩展功能,Mybatis也不例外。 我们从插件配置、插件编写、插件运行原理、插件注册与执行拦截的时机、初始化插件、分页插件的原理等六个方面...

祖大俊
2016/08/28
4K
3
Mybatis3.4.x技术内幕(二十二):Mybatis一级、二级缓存原理分析

Mybatis的一级缓存,指的是SqlSession级别的缓存,默认开启;Mybatis的二级缓存,指的是SqlSessionFactory级别的缓存,需要配置。缓存是针对select来说的。 1、一级缓存 localCacheScope用于...

祖大俊
2016/09/17
1K
6

没有更多内容

加载失败,请刷新页面

加载更多

Mybatis Plus删除

/** @author beth @data 2019-10-17 00:30 */ @RunWith(SpringRunner.class) @SpringBootTest public class DeleteTest { @Autowired private UserInfoMapper userInfoMapper; /** 根据id删除......

一个yuanbeth
今天
4
0
总结

一、设计模式 简单工厂:一个简单而且比较杂的工厂,可以创建任何对象给你 复杂工厂:先创建一种基础类型的工厂接口,然后各自集成实现这个接口,但是每个工厂都是这个基础类的扩展分类,spr...

BobwithB
今天
5
0
java内存模型

前言 Java作为一种面向对象的,跨平台语言,其对象、内存等一直是比较难的知识点。而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚。比如本文我们要讨论的JVM内存结构、Java内存模...

ls_cherish
今天
4
0
友元函数强制转换

友元函数强制转换 p522

天王盖地虎626
昨天
5
0
js中实现页面跳转(返回前一页、后一页)

本文转载于:专业的前端网站➸js中实现页面跳转(返回前一页、后一页) 一:JS 重载页面,本地刷新,返回上一页 复制代码代码如下: <a href="javascript:history.go(-1)">返回上一页</a> <a h...

前端老手
昨天
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部