Mybatis中参数解析对于开发人员来说是至关重要的,不然很容易出小问题,举个例子,假设现在方法为,当然这个是很糟糕的写法,这里只是想要搞清楚参数如何解析,项目中万万不可这样写.
参数输入解析 1 2 3 4 5 6 7 8 User findUser (@Param("name") String name,int age,String email) <select id="findUser" resultType="cn.mrdear.users.dao.User" > SELECT * FROM user WHERE username = #{name} AND age = #{age} AND email = #{email} </select> final User user = userMapper.findUser("quding" , 18 , "[email protected] " );
那么这里Mybatis会怎么解析参数呢?这个xml会构造失败不?首先是MapperMethod
中使用ParamNameResolver
对输入参数解析,针对上述输入参数会得到下面的结果.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public ParamNameResolver (Configuration config, Method method) { final Class<?>[] paramTypes = method.getParameterTypes(); final Annotation[][] paramAnnotations = method.getParameterAnnotations(); final SortedMap<Integer, String> map = new TreeMap <Integer, String>(); int paramCount = paramAnnotations.length; for (int paramIndex = 0 ; paramIndex < paramCount; paramIndex++) { if (isSpecialParameter(paramTypes[paramIndex])) { continue ; } String name = null ; for (Annotation annotation : paramAnnotations[paramIndex]) { if (annotation instanceof Param) { hasParamAnnotation = true ; name = ((Param) annotation).value(); break ; } } if (name == null ) { if (config.isUseActualParamName()) { name = getActualParamName(method, paramIndex); } if (name == null ) { name = String.valueOf(map.size()); } } map.put(paramIndex, name); } names = Collections.unmodifiableSortedMap(map); }
那么执行完毕后对于上述例子,names里面如下图所示,由于config.isUseActualParamName()
为true,所以#{0}这种写法这里并不支持,而且也不建议这种写法,无可读性. 接下来执行method.convertArgsToSqlCommandParam(args)
获取到实际输入的参数,对于上面例子我获取到的是个Map集合,如下图所示,对于单一实体例如User那么获取到的就是该实体. 再看我所用的sql写法,那么这里只能获取到name的值,sql处理时就会报错.
1 SELECT * FROM user WHERE username = #{name} AND age = #{age} AND email = #{email}
由此可见针对多参数的输入 ,最佳解决方案是用@Param
注解,其次为使用Map集合包裹参数,这样的话method.convertArgsToSqlCommandParam(args)
得到的则是该Map集合.
动态sql渲染解析 上述流程能得到所有的输入参数,那么接下来就是对sql的解析,下面把我们的sql变得复杂一些.(不要讨论sql的意义…这里只是分析参数如何解析)
1 2 3 4 5 6 7 8 9 10 User findUser (@Param("name") String name, @Param("user") User user,@Param("ids") List<Long> ids) ; <select id="findUser" resultType="cn.mrdear.users.dao.User" > SELECT * FROM user WHERE username = #{name} AND age = #{user.age} AND email = #{user.email} OR id in <foreach collection="ids" item="item" open="(" close=")" separator="," > #{item} </foreach> </select>
按照上述流程Mybatis解析出来的输入参数如下图
接下进入DefaultSqlSession
的处理中,在其中有如下方法会多参数进一步判断,可以看出对于单一参数为Collection
或者Array
时Mybatis都会给默认命名方案.(这里是在3.3.0之前的版本只会处理List)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private Object wrapCollection (final Object object) { if (object instanceof Collection) { StrictMap<Object> map = new StrictMap <Object>(); map.put("collection" , object); if (object instanceof List) { map.put("list" , object); } return map; } else if (object != null && object.getClass().isArray()) { StrictMap<Object> map = new StrictMap <Object>(); map.put("array" , object); return map; } return object; }
到了接下来转到执行器,使用DynamicContext
构造动态sql所需要的上下文,对其构造函数分析 执行到这里的话参数只有三种情况
null,无任何参数传入
Map类型,对于多参数,或者参数本身就是map再或者输入单一参数集合类型,数组类型都会转换为map
单一POJO类型. Mybatis这里要做的就是把参数的各种形式尽可能都放在ContextMap
中,该ContextMap
是绑定了Ognl的,方便Ognl直接从其中获取到值.1 2 3 4 5 6 7 8 9 10 11 12 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()); }
SqlNode SqlNode是动态Sql解析和完善ContextMap
的地方,对于我上述sql会转换为其三个子类,相关解析方法都在其内部. 解析后的sql如下图
1 2 SELECT * FROM user WHERE username = ? AND age = ? AND email = ? OR id in ( ?, ?, ?)
此时ContextMap
如下,其中有_frch_item_2
这种形式的参数,这是Mybatis对foreach解析后所生成的键,便于填充数据,具体可以看ForeachSqlNode
那么接下来要做的事情就是一一设置进去这些值.
ParameterHandler 顾名思义,其提供void setParameters(PreparedStatement ps)
对于sql参数设置的处理.分析下DefaultParameterHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Override public void setParameters (PreparedStatement ps) { ErrorContext.instance().activity("setting parameters" ).object(mappedStatement.getParameterMap().getId()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null ) { 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)) { value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null ) { value = null ; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { 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 { 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); } } } } }
那么看MetaObject
的递归获取,递归是针对参数为user.username
这样的话会先从_parameter
中找到user,然后再调用user 的getUsername()方法获取到结果.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public Object getValue (String name) { PropertyTokenizer prop = new PropertyTokenizer (name); if (prop.hasNext()) { MetaObject metaValue = metaObjectForProperty(prop.getIndexedName()); if (metaValue == SystemMetaObject.NULL_META_OBJECT) { return null ; } else { return metaValue.getValue(prop.getChildren()); } } else { return objectWrapper.get(prop); } }
那么针对上面的例子,这里先是去boundSql中的addtionParameters中获取参数,该参数一般是sql解析时动态生成的,比如foreach生成的_frch_xx,获取不到的话再去原始的ParamsObject中获取,该处的解析为递归形式了.
总结 Mybatis的SQL解析总体流程如下:
构造ParamtersMap,保存输入参数.
构造ContextMap,为OGNL解析提供数据.
读取xml.使用SqlSource与SqlNode解析xml中的sql,设置参数值到boundSql的addtionParameters中,其为ContextMap的一个副本.
根据boundSql.parameterMappings
获取到参数,从addtionParameters
与ParamtersMap
中读取参数设置到PreparedStatement
中
执行sql 本文只分析了总体流程,其中有很多细节都忽略了,如遇到问题再看也不迟.