Mybatis中setParameters分析
在使用MyBatis时,有两种使用方法。一种是使用的接口形式,另一种是通过SqlSession调用命名空间。这两种方式在传递参数时是不一样的,命名空间的方式更直接,但是多个参数时需要我们自己创建Map或者实体对象作为入参。
最近写东西发现了一个有意思的问题,以SqlSession方式在dao层我设定了接受参数类型为一个实体对象,可是传入map也可以成功解析。于是跟进看了一下源码,也在学习中,可能会出现问题,欢迎指出。
测试中我们定义了接受参数为一个Userinfo的实体类对象user,传入时使用map类型传入
在Mybatis 设置参数中
1 |
List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings(); |
构造xml #{xxx}的mappings
我们没有传入实体类对象而传入的map类型数据
执行DefaultParameterHandler类中setParameters方法
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 |
public void setParameters(PreparedStatement ps) throws SQLException { ErrorContext.instance().activity("setting parameters").object(this.mappedStatement.getParameterMap().getId()); List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings(); if(parameterMappings != null) { MetaObject metaObject = this.parameterObject == null?null:this.configuration.newMetaObject(this.parameterObject); for(int i = 0; i < parameterMappings.size(); ++i) { ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i); if(parameterMapping.getMode() != ParameterMode.OUT) { String propertyName = parameterMapping.getProperty(); Object value; if(this.boundSql.hasAdditionalParameter(propertyName)) { value = this.boundSql.getAdditionalParameter(propertyName); } else if(this.parameterObject == null) { value = null; } else if(this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) { value = this.parameterObject; } else { value = metaObject == null?null:metaObject.getValue(propertyName); } TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if(value == null && jdbcType == null) { jdbcType = this.configuration.getJdbcTypeForNull(); } typeHandler.setParameter(ps, i + 1, value, jdbcType); } } } } |
其中
1 |
MetaObject metaObject = this.parameterObject == null?null:this.configuration.newMetaObject(this.parameterObject); |
实例化MetaObject对象并且MetaObject类的构造方法判断了我们传入参数(user(map))的类型并指定objectWrapper的实现接口
MetaObject是MyBatis的一个反射类,可以很方便的通过getValue方法获取对象的各种属性(支持集合数组和Map,可以多级属性点.访问,如user.username,user.roles[1].rolename)。
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 |
private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory) { this.originalObject = object; this.objectFactory = objectFactory; this.objectWrapperFactory = objectWrapperFactory; if(object instanceof ObjectWrapper) { this.objectWrapper = (ObjectWrapper)object; } else if(objectWrapperFactory.hasWrapperFor(object)) { this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object); } else if(object instanceof Map) { this.objectWrapper = new MapWrapper(this, (Map)object); } else if(object instanceof Collection) { this.objectWrapper = new CollectionWrapper(this, (Collection)object); } else { this.objectWrapper = new BeanWrapper(this, object); } } |
我们接着看setParameters
会执行四个关键判断
1 |
if(this.boundSql.hasAdditionalParameter(propertyName)) |
第一个if当使用<foreach>的时候,MyBatis会自动生成额外的动态参数,如果propertyName是动态参数,就会从动态参数中取值
1 |
else if(this.parameterObject == null) |
第二个if,如果参数是null,不管属性名是什么,都会返回null。
1 |
if(this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass())) |
第三个if,如果参数是一个简单类型,或者是一个注册了typeHandler的对象类型,就会直接使用该参数作为返回值,和属性名无关
最后的else,这种情况下是复杂对象或者Map类型,通过反射方便的取值
1 |
value = metaObject == null?null:metaObject.getValue(propertyName); |
这里传入我们需要找的属性名
进入metaObject调用getValue
这里使用this.objectWrapper.get(prop);
最终调用了MapWrapper的实现接口
调用了MapWrapper下的get方法来获取map中value,其中BeanWrapper是获取传入实体的属性,他们同时都继承了BaseWrapper,之后的CollectionWrapper则会给你抛出异常告你错了,也就是说你传入的参数除了符合设定的类型,map和实体对象mybatis会加以判断 其他的如list如果不符合设置类型则会报UnsupportedOperationException异常。
这样去找传入map中key与属性名称相同的value
举个例子更好理解mabitis处理参数类型方式
比如我的dao接口中定义了一个根据id查询用户的方法Userinfo selectByPrimaryKey(Integer id);并且用xml实现
我们在使用时可以
1 2 |
sqlSession = GetSqlSession.getSqlSession(); Userinfo user = (Userinfo) sqlSession.selectOne("cn.rui0.mapper.UserinfoMapper.selectByPrimaryKey", 1); |
也可以用直接传对象或者map,自己设置的接收参数#{}会在给你解析map或对象后去匹配
1 2 3 4 |
Userinfo userinfo = new Userinfo(); userinfo.setId(1); sqlSession = GetSqlSession.getSqlSession(); Userinfo user = (Userinfo) sqlSession.selectOne("cn.rui0.mapper.UserinfoMapper.selectByPrimaryKey", userinfo); |
这样是可以成功请求的
但你用list就不行,即使你list里放个map
具体说 在set之前
mybatis把定义的xml#{xx}参数构造为mapping的形式(MappedStatement),我们知道SqlSource是一个接口类,在MappedStatement对象中是作为一个属性出现的,SqlSource接口只有一个getBoundSql(Object parameterObject)方法,返回一个BoundSql对象。而SqlSource最常用的实现类是DynamicSqlSource
1 2 3 4 5 6 7 8 9 10 11 12 |
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; } |
调用DynamicContext,在DynamicContext类中,有对传入的parameterObject对象进行“map”化处理的部分,也就是说,你传入的pojo对象,会被当作一个键值对数据来源来进行处理,读取这个pojo对象的接口,还是Map对象。
具体可以看一下代码
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
public class DynamicContext { public static final String PARAMETER_OBJECT_KEY = "_parameter"; public static final String DATABASE_ID_KEY = "_databaseId"; private final DynamicContext.ContextMap bindings; 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); this.bindings = new DynamicContext.ContextMap(metaObject); } else { this.bindings = new DynamicContext.ContextMap((MetaObject)null); } this.bindings.put("_parameter", parameterObject); this.bindings.put("_databaseId", configuration.getDatabaseId()); } public Map<String, Object> getBindings() { return this.bindings; } public void bind(String name, Object value) { this.bindings.put(name, value); } public void appendSql(String sql) { this.sqlBuilder.append(sql); this.sqlBuilder.append(" "); } public String getSql() { return this.sqlBuilder.toString().trim(); } public int getUniqueNumber() { return this.uniqueNumber++; } static { OgnlRuntime.setPropertyAccessor(DynamicContext.ContextMap.class, new DynamicContext.ContextAccessor()); } static class ContextAccessor implements PropertyAccessor { ContextAccessor() { } public Object getProperty(Map context, Object target, Object name) throws OgnlException { Map map = (Map)target; Object result = map.get(name); if(result != null) { return result; } else { Object parameterObject = map.get("_parameter"); return parameterObject instanceof Map?((Map)parameterObject).get(name):null; } } public void setProperty(Map context, Object target, Object name, Object value) throws OgnlException { Map map = (Map)target; map.put(name, value); } } static class ContextMap extends HashMap<String, Object> { private static final long serialVersionUID = 2977601501966151582L; private MetaObject parameterMetaObject; public ContextMap(MetaObject parameterMetaObject) { this.parameterMetaObject = parameterMetaObject; } public Object get(Object key) { String strKey = (String)key; if(super.containsKey(strKey)) { return super.get(strKey); } else if(this.parameterMetaObject != null) { Object object = this.parameterMetaObject.getValue(strKey); if(object != null) { super.put(strKey, object); } return object; } else { return null; } } } } |
在DynamicContext的构造函数中,可以看到,根据传入的参数对象是否为Map类型,有两个不同构造ContextMap的方式。而ContextMap作为一个继承了HashMap的对象,作用就是用于统一参数的访问方式:用Map接口方法来访问数据。
结束之后回到getBoundSql继续执行
1 2 |
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); BoundSql boundSql = sqlSource.getBoundSql(parameterObject); |
之后就会进行setParameters操作
1 |
List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings(); |
从boundSql取出mapping
继续执行后续上面所说获取其中对象或map操作
总结一下,mybatis除了标准的设置传参类型的方式,也可以传入包含相同属性的对象或者key的map,同样也是可以执行的!
参考:
https://blog.csdn.net/azengqiang/article/details/54377801
https://blog.csdn.net/isea533/article/details/44002219
近期评论