[框架]Mybatis中预编译sql参数替换

一、前言

在回顾@Param注解时,发现自己并不理解mepper.xml里sql语句的参数设置。之前认为的是,如果参数类型为Bean,那么语句内填写的参数名应与Bean中get方法名对应。例如:

UserMapper.xml



	insert into user values(null,#{name},#{addr},#{age})

那么按我之前的想法,User类中必须存在getName()、getAddr()、getAge()三个方法,User.java

public class User {

    private Integer id;
    private String name;
    private String addr;
    private Integer age;

    // setters and getters
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddr() {
        return addr;
    }

    public void setAddr(String addr) {
        this.addr = addr;
    }

    public Integer getAge() {
        return age;
    }

    
    public void setAge(Integer age) {
        this.age= age;
    }

}

但是在我后面的测试中,发现如果将get方法名或属性名中任意一个改成与xml中需要的参数名不对应,也能完成插入操作。也就是说,只有在get方法名与属性名都与xml中参数名不对应时,如:方法名为getAgg()、属性名为ag、xml中#{age},才会抛异常,异常信息为:

There is no getter for property named 'age' in 'class com.demo.pojo.User'

二、探究 

在发现实际运行结果与理解出了偏差后,第一时间去Mybatis官网查看了下xml中对参数的要求,其中写到:


  insert into users (id, username, password)
  values (#{id}, #{username}, #{password})

If a parameter object of type User was passed into that statement, the id, username and password property would be looked up and their values passed to a PreparedStatement parameter.

大概意思是,id、username和password三个属性将被寻找。但是没说怎么寻找,在哪寻找,也没说如果参数名不匹配会发生什么。在尝试了百度、询问老师、debug跟踪后,最后将目光锁定在DefaultParameterHandler类中的setParameters()方法上:

// DefaultParameterHandler类
public void setParameters(PreparedStatement ps) throws SQLException {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List 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;
                // 获取需要的参数名,如 #{name},propertyName = "name"
                String propertyName = parameterMapping.getProperty();
                if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                    value = boundSql.getAdditionalParameter(propertyName);
                } else if (parameterObject == null) {
                    value = null;
                } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    value = parameterObject;
                } else {
                    // 如果xml中paramType为简单Bean类,将执行此句,通过传入User的对象构建元对象
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    // 通过反射,调用get方法,获得存放在对象中的参数值
                    value = metaObject.getValue(propertyName);
                }
                TypeHandler typeHandler = parameterMapping.getTypeHandler();
                JdbcType jdbcType = parameterMapping.getJdbcType();
                if (value == null && jdbcType == null)
                    jdbcType = configuration.getJdbcTypeForNull();
                typeHandler.setParameter(ps, i + 1, value, jdbcType);
            }
        }
    }
}

当我们在xml中写到#{age}、User类中方法为getAgg()时,上述方法中的propertyName="age"。MetaObject(元对象)又是如何根据“age”执行User类中getAgg()方法呢?继续跟踪,发现MetaObject对象通过调用了Reflector对象的getGetInvoker()方法拿到get方法:

// Reflector类
public Invoker getGetInvoker(String propertyName) {
   Invoker method = getMethods.get(propertyName);
   if (method == null) {
     throw new ReflectionException("There is no getter for property named '" + propertyName + "' in '" + type + "'");
   }
   return method;
 }

 目标一下明朗起来,只要知道这个叫getMethods的map里到底存了什么东西,什么时候存了东西,问题就好解决了。

三、原因

原来,在准备连接前,Mybatis会新建多个Reflector类的对象,其中一个就是用来保存User中属性的get方法的。类中getMethods属性为HashMap,用于存放get方法的映射,键为User中所有属性名与getXXX()方法名的XXX。类中涉及到向getMethods添加的主要方法有:

// Reflector类
private void addGetMethod(String name, Method method) {
   if (isValidPropertyName(name)) {
     getMethods.put(name, new MethodInvoker(method));
     getTypes.put(name, method.getReturnType());
   }
}

private void addGetField(Field field) {
   if (isValidPropertyName(field.getName())) {
     getMethods.put(field.getName(), new GetFieldInvoker(field));
     getTypes.put(field.getName(), field.getType());
   }
}

当执行addGetField()方法时,发现属性age没有与之匹配的get方法(之前添加的是getAgg(),键为“agg”),在这Mybatis会为该属性新建get方法,存入getMethods中!这也就是为什么即便修改了User中的get方法名或属性名,只要保证其中有一项与xml中的参数名相同,就可获取User对象中的相应属性值,完成对预编译sql语句的拼接。

四、总结

对于下面这条xml中的语句,只要User类中存在与参数名对应的属性名或get方法名,Mybatis即可完成对sql语句的拼接。


	insert into user values(null,#{name},#{addr},#{age})

对于参数 #{name} 来说:

  • 只要User类中存在名叫name的属性,不存在getName()方法也可执行成功;
  • 或者User类中存在getName()方法,不存在名为name属性也可完成拼接;
  • 当User类中既没有名叫name的属性,又没有getName()方法,抛出异常。 

在Mybatis将从数据库中查询的结果封装到VO对象时,也遵循类似规则。即,数据库表字段名和VO对象的属性名、set方法其中一者存在对应关系便可完成该属性的赋值,否则对象的该属性值为null。

但是在以后的开发中,VO对象的编写还是要遵循规范。因为不仅是Mybatis在使用,Spring的依赖注入对VO的set方法名有严格要求。

 

参考:

MyBatis主流程分析之(三)-准备SQL语句和参数替换、执行

《深入理解mybatis原理》 MyBatis的架构设计以及实例分析

Mybatis参数变量替换流程

你可能感兴趣的:(框架)