mybatis源码学习(四):动态SQL的解析
之前的一片文章中我们已经了解了MappedStatement中有一个SqlSource字段,而SqlSource又有一个getBoundSql方法来获得BoundSql对象。而BoundSql中的sql字段表示了绑定的SQL语句
而且我们也已经了解过了SqlSource中的静态SQL的解析过程(RawSqlSource),这次我们来了解下动态SQL的解析过程。
动态SQL对应的SqlSource实现主要是DynamicSqlSource:
public class DynamicSqlSource implements SqlSource { private final Configuration configuration;
private final SqlNode rootSqlNode; public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
} @Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
} }
我们知道无论RawSqlSource还是DynamicSqlSource,都会将getBoundSql的方法委托给内部的StaticSqlSource对象。
但是对比RawSqlSource和DynamicSqlSource的字段值,我们可以很直观的发现RawSqlSource直接有一个SqlSource属性,构造函数中通过configuration和SqlNode直接解析SqlSource对象,
而DynamicSqlSource相反,他没有SqlSource属性,反而是保留了configuration和SqlNode作为属性,只有在getBoundSql时,才会去创建SqlSource对象。
这正是因为动态Sql的sqlsource是无法直接确定的,需要在运行时根据条件才能确定。
所以,对于动态SQL的解析其实是分为两阶段的:
1.解析XML资源:之前的解析过程都类似(可参考前一篇文章),XMLScriptBuilder会将XML中的节点解析成各个类型的SqlNode,然后封装成MixedSqlNode,它和Configuration对象一起作为参数,创建DynamicSqlSource对象。
2.执行SQL:SQL的执行过程我也在之前的文章中介绍过了,我们知道在Executor在执行SQL时,会通过MappedStatement对象获取BoundSql对象,而文章一开始我们已经说了MappedStatement对象是把这一操作委托给SqlSource。因此,这时候DynamicSqlSource才会真的执行getBoundSql方法,解析得到BoundSql对象。
介绍了大概的过程,我们通过一些简单的示例来更清晰的认识这一过程。
测试用的Mapper.xml:
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.apache.ibatis.domain.blog.mappers.AuthorMapper"> <select id="selectAuthorWithInlineParams" parameterType="int"
resultType="org.apache.ibatis.domain.blog.Author">
select * from author where id = ${id}
</select> </mapper>
我们可以看到SQL语句中简单的包含了${}。因此它会被判定为动态SQL。
测试代码:
public void dynamicParse() throws Exception{
//阶段一:启动时
String resource = "org/apache/ibatis/builder/MapperConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //阶段二:执行时
SqlSession sqlSession = sqlSessionFactory.openSession();
AuthorMapper mapper = sqlSession.getMapper(AuthorMapper.class);
mapper.selectAuthorWithInlineParams(1);
}
我已经在代码中注释出了两个阶段,正是之前我们介绍的两个阶段。
Mybatis相关的配置文件相对简单,对数据源做了配置并引入了相应的mapper文件,不再贴出。
AuthorMapper类也比较简单,同样不贴出。
接下来让我们跟着断点,来具体看一下动态节点的解析。
为了直观起见,除了会对代码做一些说明外,我还会在代码右侧注释出一些关键对象的信息。
阶段一中有部分过程之前的解析过程和静态SQL的解析过程是一致的,因此我们从XMLLanguageDriver开始,
//XNode对象是XNode对象是xml中由XPathParser对象解析出来的节点对象,parameterType是要传入SQL的参数类型
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {//script:<select resultType="org.apache.ibatis.domain.blog.Author" parameterType="int" id="selectAuthorWithInlineParams">select * from author where id = ${id}</select> //创建XMLScriptBuilder对象,通过它创建SqlSource对象,建造者模式的应用。
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
接着看XMLScriptBuilder.parseScrpitNode方法:
public SqlSource parseScriptNode() {
//解析XNode成一系列SqlNode对象,并封装成MixedSqlNode对象,并会判断此SQL是否为动态
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource = null;
if (isDynamic) {//动态SQL则创建DynamicSqlSource
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {//静态SQL则创建RawSqlSource
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
我们看看真正的解析过程parseDynamicTags:
//他会解析XNode成一系列SqlNode对象,并封装成MixedSqlNode对象
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<SqlNode>();
//获取当前XNode下的子节点,并遍历解析子节点
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
//子节点,本例中只有一个子节点,为一个文本节点
XNode child = node.newXNode(children.item(i));//child:<#text>select * from author where id = ${id}</#text>
//如果是文本节点,则解析成TextSqlNode
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));
}
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { //其他节点类型将被解析成对应的SqlNode类型,
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;
}
}
//将解析得到的SqlNode,封装成MixedSqlNode对象
return new MixedSqlNode(contents);
}
上面的过程和解析静态SQL的过程有一个不同的地方是静态SQL会再根据TextSqlNode的文本创建出StaticTextSqlNode。而动态SQL对于文本节点,仍然使用TextSqlNode。
再回到之前的parseScrpitNode方法中,它根据解析结果创建了一个DynamicSqlSource对象,它保存了解析过程所得的Configuration对象和MixedSqlNode对象。
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
这样我们就解析得到了DynamicSqlSource对象,即完成了动态SQL解析的阶段一的过程。
接下来我们来看阶段二的过程,前面的过程也不再赘述,直接看SqlSource调用getBoundSql方法时:
@Override
public BoundSql getBoundSql(Object parameterObject) {//参数对象:{"id"->1 ; "param1" -> 1}
//传入configuration和运行时的参数,创建DynamicContext对象
DynamicContext context = new DynamicContext(configuration, parameterObject);
//应用每个SqlNode,拼接Sql片段,这里只替换动态部分
rootSqlNode.apply(context);//此时context的sqlBuilder已经被解析成了:select * from author where id = 1
//继续解析SQL,将#{}替换成?
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
//创建BoundSql对象
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
先了解一下DynamicContext对象,可以将它理解为Sql片段的一个容器,用于之后拼接出Sql。同时它还有一个bindings属性,可以用来保存运行信息,比如绑定的参数,数据库ID等:
public class DynamicContext { public static final String PARAMETER_OBJECT_KEY = "_parameter";
public static final String DATABASE_ID_KEY = "_databaseId"; static {
OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor());
} //用来保存运行时上下文参数
private final ContextMap bindings;
//用来拼接SQL片段
private final StringBuilder sqlBuilder = new StringBuilder();
private int uniqueNumber = 0; public DynamicContext(Configuration configuration, Object parameterObject) {
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
} else {
bindings = new ContextMap(null);
}
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
} public Map<String, Object> getBindings() {
return bindings;
} public void bind(String name, Object value) {
bindings.put(name, value);
} public void appendSql(String sql) {
sqlBuilder.append(sql);
sqlBuilder.append(" ");
} public String getSql() {
return sqlBuilder.toString().trim();
} public int getUniqueNumber() {
return uniqueNumber++;
} static class ContextMap extends HashMap<String, Object> {
private static final long serialVersionUID = 2977601501966151582L; private MetaObject parameterMetaObject;
public ContextMap(MetaObject parameterMetaObject) {
this.parameterMetaObject = parameterMetaObject;
} @Override
public Object get(Object key) {
String strKey = (String) key;
if (super.containsKey(strKey)) {
return super.get(strKey);
} if (parameterMetaObject != null) {
// issue #61 do not modify the context when reading
return parameterMetaObject.getValue(strKey);
} return null;
}
} static class ContextAccessor implements PropertyAccessor { @Override
public Object getProperty(Map context, Object target, Object name)
throws OgnlException {
Map map = (Map) target; Object result = map.get(name);
if (map.containsKey(name) || result != null) {
return result;
} Object parameterObject = map.get(PARAMETER_OBJECT_KEY);
if (parameterObject instanceof Map) {
return ((Map)parameterObject).get(name);
} return null;
} @Override
public void setProperty(Map context, Object target, Object name, Object value)
throws OgnlException {
Map<Object, Object> map = (Map<Object, Object>) target;
map.put(name, value);
} @Override
public String getSourceAccessor(OgnlContext arg0, Object arg1, Object arg2) {
return null;
} @Override
public String getSourceSetter(OgnlContext arg0, Object arg1, Object arg2) {
return null;
}
}
}
至此,我们就获得了BoundSql的对象,之后的过程就和静态SQL的使用过程是一致的。
mybatis源码学习(四):动态SQL的解析的更多相关文章
- mybatis源码学习(四)--springboot整合mybatis原理
我们接下来说:springboot是如何和mybatis进行整合的 1.首先,springboot中使用mybatis需要用到mybatis-spring-boot-start,可以理解为mybati ...
- Mybatis源码学习之parsing包(解析器)(二)
简述 大家都知道mybatis中,无论是配置文件mybatis-config.xml,还是SQL语句,都是写在XML文件中的,那么mybatis是如何解析这些XML文件呢?这就是本文将要学习的就是,m ...
- mybatis源码学习: 动态代理的应用(慢慢改)
动态代理概述 在学spring的时候知道使用动态代理实现aop,入门的列子:需要计算所有方法的调用时间.可以每个方法开始和结束都获取当前时间咋办呢.类似这样: long current=system. ...
- mybatis源码学习:基于动态代理实现查询全过程
前文传送门: mybatis源码学习:从SqlSessionFactory到代理对象的生成 mybatis源码学习:一级缓存和二级缓存分析 下面这条语句,将会调用代理对象的方法,并执行查询过程,我们一 ...
- mybatis源码学习(一) 原生mybatis源码学习
最近这一周,主要在学习mybatis相关的源码,所以记录一下吧,算是一点学习心得 个人觉得,mybatis的源码,大致可以分为两部分,一是原生的mybatis,二是和spring整合之后的mybati ...
- mybatis源码学习:一级缓存和二级缓存分析
目录 零.一级缓存和二级缓存的流程 一级缓存总结 二级缓存总结 一.缓存接口Cache及其实现类 二.cache标签解析源码 三.CacheKey缓存项的key 四.二级缓存TransactionCa ...
- mybatis源码学习:插件定义+执行流程责任链
目录 一.自定义插件流程 二.测试插件 三.源码分析 1.inteceptor在Configuration中的注册 2.基于责任链的设计模式 3.基于动态代理的plugin 4.拦截方法的interc ...
- Mybatis源码学习第六天(核心流程分析)之Executor分析
今Executor这个类,Mybatis虽然表面是SqlSession做的增删改查,其实底层统一调用的是Executor这个接口 在这里贴一下Mybatis查询体系结构图 Executor组件分析 E ...
- Spring mybatis源码学习指引目录
前言: 分析了很多方面的mybatis的源码以及与spring结合的源码,但是难免出现错综的现象,为了使源码陶冶更为有序化.清晰化,特作此随笔归纳下分析过的内容.博主也为mybatis官方提供过pul ...
随机推荐
- 基于 HTML5 WebGL 的 水泥工厂可视化系统
前言 如今的制造行业,基于数据进行生产策略制定与管理已经成为一种趋势,特别是 工业4.0 的浪潮下,数据战略已经成为很多制造企业的优先战略,而数据可视化以更直观的方式,帮助指导决策,成为数据分析传递信 ...
- SpringCloud(二)之我学 Ribbon
1.负载均衡 Ribbon 虽然不是显示的配置为一个子项目,但是无论是在 API 网关的转发请求,还是服务之间的调用 Feign ,都是通过 Ribbon 来做负载均衡的. 负载均衡,主要是为了对系统 ...
- linux中忘记了mysql的root用户的密码怎么办?
1.vim /etc/my.cnf skip-grant-tables #取消此行的注释 2.重启mysql systemctl restart mysqld 3.mysql 登陆mysql mys ...
- NS网络仿真,小白起步版,双节点之间的模拟仿真(基于TCP和FTP流)
set ns [new Simulator] set tracefd [open one.tr w] #开启跟踪文件,记录分组传送的过程 $ns trace-all $tracefd set namt ...
- javascript入门 之 zTree (一)
1.安装: 我用的bower工具,所以执行: bower install ztree 2.详细功能与配制,请考官方文档: http://www.treejs.cn/v3/main.php#_zTree ...
- vscode如何安装eslint插件 代码自动修复
ESlint:是用来统一JavaScript代码风格的工具,不包含css.html等. 方法和步骤: 通常情况下vue项目都会添加eslint组件,我们可以查看webpack的配置文件package. ...
- [一起面试AI]NO.9 如何判断函数凸或非凸
首先定义凸集,如果x,y属于某个集合M,并且所有的θx+(1-θ)f(y)也属于M,那么M为一个凸集.如果函数f的定义域是凸集,并且满足 f(θx+(1-θ)y)≤θf(x)+(1-θ)f(y) 则该 ...
- Linux c++ vim环境搭建系列(3)——Ubuntu18.04.4编译安装youcompleteme
3. youcompleteme编译安装 参考网址: https://github.com/ycm-core/YouCompleteMe#linux-64-bit 建议不要用这个博客的方法: http ...
- 程序员小张的第一篇博文 --记Markdown的使用学习
1.前言 为了即将到来的面试做准备,以及记录一下平日里自己的学习过程和生活日常,我开始进驻博客园啦!这就是我的第一篇博客(有点小激动)~ 作为一只新手,首先记录一下今晚的编写博文的学习过程吧~ 2.使 ...
- day02,静态库和动态库
一.首先我们来看一下什么是静态库和动态库,在这之前我们来看一下编译成可执行文件的过程: 1.静态库(.a..lib):就是在使用的时候会把代码复制到文件中: 它的优点:独立,在链接后不需要静态库源文件 ...