Mybatis源码解析 —— 动态设置参数机制
来源:互联网 发布:java字符串只保留汉字 编辑:程序博客网 时间:2024/05/17 08:41
前言
在上一篇文章Mybaits Sql解析过程中,笔者介绍了Mybatis是如何根据注解生成动态Sql语句的。但是,在常用的查询中,除了生成语句之外,为Sql查询语句添加参数也是非常重要的一个环节。
本篇文章将会对Mybatis参数解析,设置的全过程,进行分析。简单点来说,就是我们会围绕着一下的方法调用例子,了解Mybatis如何将方法中的参数转化为Sql中的参数。
@Select({"<script>", "SELECT account", "FROM user", "WHERE id IN", "<foreach item='item' index='index' collection='list'", "open='(' separator=',' close=')'>", "#{item}", "</foreach>", "</script>"}) List<String> selectAccountsByIds(@Param("list") int[] ids);
Mybatis中的Sql参数设定与执行
首先,我们来通过一张简略的图片,来大概了解整个过程。
通过这张图,我们可以了解到几个重点。
通过MethodSignature将参数转化为Map
调用的具体方法如下
public Object getNamedParams(Object[] args) { final int paramCount = names.size(); if (args == null || paramCount == 0) { return null; } else if (!hasParamAnnotation && paramCount == 1) { return args[names.firstKey()]; } else { final Map<String, Object> param = new ParamMap<Object>(); int i = 0; for (Map.Entry<Integer, String> entry : names.entrySet()) { //首先,根据@param中注解的值,将相关参数添加到Map中 param.put(entry.getValue(), args[entry.getKey()]); // 然后,再将参数的通用名,也添加到Map中 final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1); if (!names.containsValue(genericParamName)) { param.put(genericParamName, args[entry.getKey()]); } i++; } return param; } }
通过以上的转换之后,我们在后续的函数调用中,就可以以键值对的形式来获取方法参数的值了。
DynamicSqlSource中的参数解析
在上一片文章中,我们提到过,SqlNode
中的apply
方法,会将我们定义在注解中的Sql,转化成带有占位符的Sql。而实际上,除了Sql的转化之外,它还会向DynamicContext
中添加Binding
(参数值与对应键的绑定)。
ForeachSqlNode
中的apply
方法
public boolean apply(DynamicContext context) { Map<String, Object> bindings = context.getBindings(); //首先,通过解析器,将参数中的值解析成为一个可遍历的集合。 //初始状态下,bindins中只有_parameter -> { 'list' -> [1,2]} //提取出来后的集合为[1,2] final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings); if (!iterable.iterator().hasNext()) { return true; } boolean first = true; applyOpen(context); int i = 0; for (Object o : iterable) { DynamicContext oldContext = context; if (first) { context = new PrefixedContext(context, ""); } else if (separator != null) { context = new PrefixedContext(context, separator); } else { context = new PrefixedContext(context, ""); } int uniqueNumber = context.getUniqueNumber(); // Issue #709 if (o instanceof Map.Entry) { @SuppressWarnings("unchecked") Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o; applyIndex(context, mapEntry.getKey(), uniqueNumber); applyItem(context, mapEntry.getValue(), uniqueNumber); } else { //将对应的binding添加到DynamicContext中。 //第一次遍历时,增加到binding 为 { '__frch_index_0' -> 0 } applyIndex(context, i, uniqueNumber); //第一次遍历时,增加到binding 为 { '__frch_item_0' -> 1 } applyItem(context, o, uniqueNumber); } contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber)); if (first) { first = !((PrefixedContext) context).isPrefixApplied(); } context = oldContext; i++; } applyClose(context); return true; }
当SqlNode解析完成后,我们得到的DynamicContext中Sql如下:
SELECT account FROM user WHERE id IN ( #{__frch_item_0} , #{__frch_item_1} )
而在DynamicContext中的binding,则如下所示:
'__frch_index_0' -> 0,'__frch_item_0' -> 1,'__frch_index_1' -> 1,'__frch_item_0' -> 2,'_parameter' -> { 'list' -> [1,2] , 'param1' ->[1,2] },'_databaseID' -> null
SqlSourceBuilder中获取ParameterMapping
虽然在DynamicContext中已经有了Bindings,但是Mybatis并不会直接使用这些binding进行查询。它会从含有占位符的语句中提取ParameterMapping关系,然后再根据ParameterMapping来对参数进行设置。
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) { //ParameterMappingTokenHandler,在查找到特定的token之后,对token进行处理,并且返回处理后的字符串 ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); //只负责找到被#{}包围的字符串,然后交由tokenHandler进行处理 GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); String sql = parser.parse(originalSql); return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); }
ParameterMappingTokenHandler
public String handleToken(String content) { //首先,使用token中的标志符构造ParameterMapping,然后返回"?" parameterMappings.add(buildParameterMapping(content)); return "?"; }
而在这一次处理之后,我们就能够能到可以被直接执行的Sql了。
SELECT account FROM user WHERE id IN ( ? , ? )
同时,也生成了对应的ParameterMapping(暂时忽略一些其它属性)
[{ property: '__frch_item_0'},{ property: '__frch_item_1'}]
最后,在构造BoundSql时,Mybatis还会做如下的事情:
//将默认的参数,也作为BoundSql的默认参数BoundSql boundSql = sqlSource.getBoundSql(parameterObject); //在context中生成的binding,则会做为额外的参数,也传给boundSql for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) { boundSql.setAdditionalParameter(entry.getKey(), entry.getValue()); } return boundSql;
参数的使用——DefaultParameterHandler
在这个类里面,我们只需要关注一个方法setParameters
。
public void setParameters(PreparedStatement ps) { ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { //parameterMapping是根据Sql中占位符逐个生成的,因此数组中的顺序也等同于sql中对应参数的顺序,直接进行遍历以及参数设置即可。 for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); if (boundSql.hasAdditionalParameter(propertyName)) { // 首先,从额外的参数中获取参数值,获取顺序很重要,这个与Mybatis中的issue #448有关 value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { //然后,再从parameterObject中进行获取。 MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) { jdbcType = configuration.getJdbcTypeForNull(); } try { //考虑到Java类型与Mysql类型还有一个映射关系,所以使用typeHandler进行处理 typeHandler.setParameter(ps, i + 1, value, jdbcType); } catch (TypeException e) { throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); } catch (SQLException e) { throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); } } } } }
到这里,参数的设定就已经完成了。
结语
在这片文章中,我们围绕着一个例子解释了Mybatis是如何进行参数设定的。在这里面,我们能够发现几个有意思的地方。
参数与ParameterMapping的分离。ParameterMapping根据解析后带占位符的Sql解析而得到,参数则始终保存在BoundSql的Parameter或者是AdditionalParameter中。这样的分离使得相互的职责更为明确,也更易于单元测试。
简单的设计。BoundSql作为执行的主体,里面只包含有Sqls,Parameter以及parameterMappings。
- Mybatis源码解析 —— 动态设置参数机制
- Mybatis工作机制源码分析—初始化—sax解析
- MyBatis源码解析(一)——MyBatis初始化过程解析
- MyBatis源码(五)之动态Sql解析运行阶段参数处理
- Mybatis工作机制源码分析—初始化—config配置文件解析
- Mybatis工作机制源码分析—初始化—mapper配置文件解析
- Mybatis工作机制源码分析—初始化
- MyBatis源码解析(二)——动态代理实现函数调用
- MyBatis 源码分析——动态代理
- Mybatis工作机制源码分析—SqlSessionUtils.getSqlSession工作机制
- Mybatis源码解析 —— Sql解析详解
- NestedScrolling机制(三)——机制本质以及源码解析
- NestedScrolling机制(三)——机制本质以及源码解析
- Mybatis工作机制源码分析—缓存机制及事务机制
- mybatis+spring源码解析(动态代理 spring初始化)
- Mybatis 动态SQL之<trim>,<where>,<set>源码解析
- MyBatis官方教程及源码解析——入门
- Hadoop源码解析之java动态代理机制
- EditText插入图片
- 关于2005-Unknown MySQL server host 'localhost'(0)
- 仿RadioGroup实现底部导航
- Twemproxy 介绍与使用
- 数据结构封装之《LinkStack链栈》
- Mybatis源码解析 —— 动态设置参数机制
- springboot全局异常捕捉
- Android Studio 使用第三方库的方法
- OpenDDS开发手册---第三章(服务质量)1
- log4j使用及详细配置说明
- c++用分治法对随机数组归并排序
- 1384 全排列
- Thinkphp5学习(17)输入和验证
- Unsafe 与 CAS