在使用MyBatis时,有两种使用方法。一种是使用的接口形式,另一种是通过SqlSession调用命名空间。这两种方式在传递参数时是不一样的,命名空间的方式更直接,但是多个参数时需要我们自己创建Map或者实体对象作为入参。

最近写东西发现了一个有意思的问题,以SqlSession方式在dao层我设定了接受参数类型为一个实体对象,可是传入map也可以成功解析。于是跟进看了一下源码,也在学习中,可能会出现问题,欢迎指出。


测试中我们定义了接受参数为一个Userinfo的实体类对象user,传入时使用map类型传入

在Mybatis 设置参数中

List<ParameterMapping> parameterMappings = this.boundSql.getParameterMappings();

构造xml #{xxx}的mappings

我们没有传入实体类对象而传入的map类型数据

执行DefaultParameterHandler类中setParameters方法

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);
            }
        }
    }

}

其中

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)。

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

会执行四个关键判断

if(this.boundSql.hasAdditionalParameter(propertyName))

第一个if当使用<foreach>的时候,MyBatis会自动生成额外的动态参数,如果propertyName是动态参数,就会从动态参数中取值

else if(this.parameterObject == null)

第二个if,如果参数是null,不管属性名是什么,都会返回null。

if(this.typeHandlerRegistry.hasTypeHandler(this.parameterObject.getClass()))

第三个if,如果参数是一个简单类型,或者是一个注册了typeHandler的对象类型,就会直接使用该参数作为返回值,和属性名无关

最后的else,这种情况下是复杂对象或者Map类型,通过反射方便的取值

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实现

我们在使用时可以

sqlSession = GetSqlSession.getSqlSession();
Userinfo user = (Userinfo) sqlSession.selectOne("cn.rui0.mapper.UserinfoMapper.selectByPrimaryKey", 1);

也可以用直接传对象或者map,自己设置的接收参数#{}会在给你解析map或对象后去匹配

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

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对象。

具体可以看一下代码

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继续执行

SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

之后就会进行setParameters操作

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