精通Mybatis之动态sql全流程解析
Posted 木兮君
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了精通Mybatis之动态sql全流程解析相关的知识,希望对你有一定的参考价值。
前言
小编的精通mybatis之讲解就快结束了,希望大家坚持不懈坚持到底了。好了进入今天的正题,动态sql全流程。
动态Sql定义
定义:每次构建sql脚本时,根据预先编写的脚本以及参数动态构建可执行的sql。
动态SQL是MyBatis 强大功能之一,他免除了在JAVA代码中拼装SQL字符串麻烦,同时保留了我们对SQL的自主控制,更方便进行SQL性能优化改造。这也是大部分的编程伙伴喜欢用mybatis的原因。
对动态sql的使用大家应该很熟悉了吧,如果有需要大家可以看官网使用动态 SQL。首先小编带大家看下sql脚本元素:
如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类就上面这些了。其实官网里面还有bind和接口注解方式的script。
那小编带大家先了解一下OGNL的表达式吧。
OGNL表达式
OGNL全称是对象导航图语言(Object Graph Navigation Language)是一种JAVA表达示语言,可以方便的存取对象属和方法,已用于逻辑判断。其支持以下特性:
- 获取属性属性值,以及子属性值进行逻辑计算
id != null || autho.name != null - 表达示中可直接调用方法,(如果是无参方法,可以省略括号)
! comments.isEmpty && comments.get(0) != null - 通过下标访问数组或集合
comments[0].id != null
遍历集合
Iterable<?> comments = evaluator.evaluateIterable(“comments”, blog);
接下来小编用代码示例演示一下
这边使用了官网的示例,Blog ,commit,user三个对象
public class OgnlTest {
@Test
public void ognlExpressionTest() {
ExpressionEvaluator expressionEvaluator = new ExpressionEvaluator();
Blog blog = new Blog();
blog.setId(1);
User user = new User();
user.setId(2);
blog.setAuthor(user);
List<Comment> commentList = new ArrayList<>();
Comment comment = new Comment();
comment.setBody("123");
commentList.add(comment);
blog.setComments(commentList);
//这里author如果为空的话author.id肯定会报错
boolean hasAuthor = expressionEvaluator.evaluateBoolean("id !=null && author.id!=null", blog);
System.out.println(hasAuthor);
boolean hasComments = expressionEvaluator.evaluateBoolean("comments !=null && !comments.isEmpty", blog);
System.out.println(hasComments);
boolean hasCommentBody = expressionEvaluator.evaluateBoolean("comments !=null && comments.get(0).body!=null", blog);
System.out.println(hasCommentBody);
boolean hasCommentBody2 = expressionEvaluator.evaluateBoolean("comments !=null && comments[0].body!=null", blog);
System.out.println(hasCommentBody2);
Iterable<?> comments = expressionEvaluator.evaluateIterable("comments", blog);
for (Object o : comments) {
System.out.println(o);
}
}
}
测试结果:
true
true
true
true
org.coderead.mybatis.bean.Comment@5b1d2887
SqlSource解析过程
说完了OGNL表达式,咱们来说一下sql的解析过程,即从数据源到我们可以执行的sql。SqlSource是sql的数据源,他可以通过注解的方式或xml方式得到对应的源。右边的BoundSql就是可执行sql的所有东西。StatementHandler就是根据他去执行sql的(当然获取他的时候还包装了一层:MappedStatement),待会儿小编会带大家看他的源码,一看就明白了。
小编稍微对sqlSource的实现类讲解一下:
- ProviderSqlSource :第三方法SQL源,每次获取SQL都会基于参数动态创建静态数据源,然后在创建BoundSql
- DynamicSqlSource:动态SQL源包含了SQL脚本,每次获取SQL都会基于参数以及脚本,动态创建创建BoundSql
- RawSqlSource:不包含任何动态元素,原生文本的SQL。但这个SQL是不能直接执行的,需要转换成BoundSql
- StaticSqlSource:包含可执行的SQL,以及参数映射,可直接生成BoundSql。前面三个数据源都要先创建StaticSqlSource然后才创建BoundSql。
因为第三方很少涉及,一般我们只是使用静态或动态,静态很简单直接将#{}变成?,然后将参数值设置进去即可(这边在变成问号的时候,参数值映射也是一一对应的,有兴趣的小伙伴可以去看下源码)。如:select * from user where user_id =#{userId},所以小编着重讲一下动态sql源的解析过程。
动态Sql源解析
先看下动态sql源的解析流程。
看到这儿大家是不是很懵,这个小编是根据源码来写的,首先还是得让大家知道什么是SqlNode,以及我们的动态sql是如何和SqlNode建立起关系的。
SqlNode这里使用了解释器模式,小编这里简单的解释一下这个设计模式。
解释器模式(Interpreter Pattern)提供了评估语言的语法或表达式的方式,它属于行为型模式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等。
简单介绍
意图:给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
主要解决:对于一些固定文法构建一个解释句子的解释器。
何时使用:如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。
如何解决:构建语法树,定义终结符与非终结符。
关键代码:构建环境类,包含解释器之外的一些全局信息,一般是 HashMap。
SqlNode主要是来解析Mybatis中的Sql脚本元素,之后将解析完毕sql添加到DynamicContext中去。小编简单说下各个SqlNode的作用:
- SqlNode是总接口只有一个方法:apply(DynamicContext context),作用如上面小编所讲,各个sqlNode处理完对应的逻辑然后将对应sql添加到DynamicContext
- MixedSqlNode包含多个子sqlNode,是个list然后循环调用子节点的逻辑
- ChooseSqlNode,IfSqlNode,ForEachSqlNode,TrimSqlNode这些节点就是来处理对应的sql脚本元素
- StaticTextSqlNode为静态脚本node,直接拼接的是静态脚本,如:select * from user
- TextSqlNode为文本脚本node,他主要是用来替换${}占位符的,直接替换成文本如:select * from ${table_name}
题外话:这边小编想起了一个mybatis的面试题,说${}与#{}替换符的区别,其实结论大家都知道,但是具体实现可能没有真正的看过,有兴趣的小伙伴可以看下。结论是很简单且正确的,但求证的却很少
这些SqlNode是怎样的数据结构才能解析我们的动态sql呢,看过解释器模式就知道他其实构建语法树,怎么构建的看下图:
脚本之间是呈现嵌套关系的。比如if元素中会包含一个MixedSqlNode ,而MixedSqlNode下又会包含1至1至多个其它节点。最后组成一课脚本语法树。如上图左边的SQL元素组成右边的语法树。在节点最底层一定是一个StaticTextNode或 TextNode。
这就是xm中的sqll脚本解析变成语法树的结构。这边小编不一一讲各种元素,这里挑选几个讲一下
if、where、foreach
模拟if,where解析过程:
@Test
public void sqlNodeTest(){
Company company = new Company();
company.setId(1L);
company.setCompanyName("伟大的公司");
DynamicContext dynamicContext = new DynamicContext(configuration,company);
StaticTextSqlNode staticTextSqlNode = new StaticTextSqlNode("select * from company");
staticTextSqlNode.apply(dynamicContext);
IfSqlNode ifIdSqlNode = new IfSqlNode(new StaticTextSqlNode("id = #{id}"),"id != null");
IfSqlNode ifNameSqlNode = new IfSqlNode(new StaticTextSqlNode(" and company_name = #{companyName}"),"companyName != null");
MixedSqlNode mixedSqlNode = new MixedSqlNode(Arrays.asList(ifIdSqlNode,ifNameSqlNode));
WhereSqlNode whereSqlNode= new WhereSqlNode(configuration,mixedSqlNode);
whereSqlNode.apply(dynamicContext);
System.out.println(dynamicContext.getSql());
}
测试结果:
这边大家有没有发现,if其实不需要MixedSqlNode,包装一个StaticTextSqlNode即可,为什么小编在上面的脚本树中包含的是MixedSqlNode,这是因为if里面还可以加if所以里面需要包装使用MixedSqlNode,在mybatis实际过程中也是如此。
froeach
@Test
public void foreachNodeTest(){
Map<String,Object> paramMap = new HashMap<>(1);
List<Long> idList = Arrays.asList(1L,2L,3L);
paramMap.put("idList",idList);
DynamicContext dynamicContext = new DynamicContext(configuration,paramMap);
StaticTextSqlNode staticTextSqlNode = new StaticTextSqlNode("select * from company where id in");
ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration,
new MixedSqlNode(Arrays.asList(new StaticTextSqlNode("#{item}"))),"idList","index","item","(",")",",");
staticTextSqlNode.apply(dynamicContext);
forEachSqlNode.apply(dynamicContext);
System.out.println(dynamicContext.getSql());
}
测试结果:
看到这个参数的替换是不是不一样啊。这边小编提个问,为什么List的参数小编要转成map参数传进去?
如果不了解的话建议看下精通Mybatis之Jdbc处理器StatementHandler中的参数转换。
小结
其实真正的解析也差不多,小编只是将他们拆开来了,实际过程根据xml里面的sql语句分段然后使用SqlNode拼装成脚本结构树,顶层只有MixedSqlNode 就是根节点,然后在执行的时候根据结构树变成sql。稍微有点区别的是,sqlNode还会进行一次包装。
动态标签xml解析过程
上面是小编是在底层拆开揉碎了展开的,那从用户角度,咱们编写好xml他是如何解析的呢,其实不难,听小编慢慢道来,SqlSource 是基于XML解析而来,解析的底层是使用Dom4j 把XML解析成一个个子节点,在通过 XMLScriptBuilder 遍历这些子节点最后生成对应的Sql源。其解析流程如下图:
nodeHandler 类图:
源码阅读:
小编编写了稍微复杂的xml的sql然后进行debug调试,测试xml的sql如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mapper.EmployeeMapper">
<resultMap id="employMap" type="entity.Employee" autoMapping="true"/>
<select id="selectByIdListAndName" resultType="collection" resultMap="employMap">
select * from employee
<where>
<if test="idList != null and !idList.isEmpty">
id in
<foreach collection="idList" separator="," index="index" item="item" open="(" close=")">
#{item}
</foreach>
</if>
<if test="name != null">
name = #{name}
</if>
</where>
</select>
</mapper>
XMLScriptBuilder源码(太多了小编捡一些重点):
//构造方法,其中context 就是上面的<select>标签的所有内容,parameterType为属性值的class类型
public XMLScriptBuilder(Configuration configuration, XNode context, Class<?> parameterType) {
super(configuration);
this.context = context;
this.parameterType = parameterType;
initNodeHandlerMap();
}
//初始化NodeHandlerMap,处理各脚本元素的handler,各节点最后是相应的sqlNode
//然后添加到rootNode的contexts列表中(rootNode 类型是MixedSqlNode,
//而contexts为MixedSqlNode中的属性 类型为List<SqlNode>)
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());
}
//开始解析
public SqlSource parseScriptNode() {
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
//如果是动态的就包装成动态的sqlsource
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
//否则就是静态sqlSource
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
protected MixedSqlNode parseDynamicTags(XNode node) {
//创建sqlNode子节点树
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));
//拿到各个节点的数据,首先判断是否是文本数据
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
//判断是否是脚本元素 动态sql
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
contents.add(new StaticTextSqlNode(data));
}
//如果是元素node
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
//取出node的名称
String nodeName = child.getNode().getNodeName();
//更加元素node取出对应的NodeHandler
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
//根据对应的nodeHandler处理对应的node节点
handler.handleNode(child, contents);
isDynamic = true;
}
}
//根节点始终为MixedSqlNode
return new MixedSqlNode(contents);
}
NodeHandler源码:这是XMLScriptBuilder的内部接口和内部实现(下面比较简单,不停递归调用最后扔进相应的sqlNode即可)
private interface NodeHandler {
void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
}
private class BindHandler implements NodeHandler {
public BindHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
final String name = nodeToHandle.getStringAttribute("name");
final String expression = nodeToHandle.getStringAttribute("value");
final VarDeclSqlNode node = new VarDeclSqlNode(name, expression);
targetContents.add(node);
}
}
private class TrimHandler implements NodeHandler {
public TrimHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
String prefix = nodeToHandle.getStringAttribute("prefix");
String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");
String suffix = nodeToHandle.getStringAttribute("suffix");
String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");
TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);
targetContents.add(trim);
}
}
private class WhereHandler implements NodeHandler {
public WhereHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
targetContents.add(where);
}
}
private class SetHandler implements NodeHandler {
public SetHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
targetContents.add(set);
}
}
private class ForEachHandler implements NodeHandler {
public ForEachHandler() {
// Prevent Synthetic Access
}
@Override
public void handleNode(XNode nodeToHandle, List<SqlNodeMyBatis从入门到精通:MyBatis动态Sql之if标签的用法
Mybatis -- 动态Sql概述动态Sql之<if>(包含<where>)动态Sql之<foreach>sql片段抽取