Mybatis的参数使用

时间:2022-09-23 05:14:51

刚开始使用Mybatis的时候,感觉一头雾水:由于数据库的实际操作已经给封装起来,只需要传入参数就可以,但是这些参数与SQL之间是怎样关联上的呢?是使用参数名称关联吗?如果有多个参数呢?如果使用数组或者列表呢?


下面来看个各种参数使用的例子:

// DAO的实现类:

public class UserDaoImpl extends SqlSessionDaoSupport implements UserDao {
	// 使用继承SqlSessionDaoSupport的方式,只提供了sqlSessionTemplate的setter方法,
	// 故在applicationContext.xml中配置DAO时,属性名必须固定为sqlSessionTemplate
	
	@Override
	public User getUserByNameStr1(String name) {
		return getSqlSession().selectOne("findByNameStr1", name);
	}

	@Override
	public User getUserByNameStr2(String name) {
		return getSqlSession().selectOne("findByNameStr2", name);
	}

	@Override
	public User getUserByNameStr3(String name) {
		return getSqlSession().selectOne("findByNameStr3", name);
	}

	@Override
	public User getUserByNameStr4(String keyWord) {
		return getSqlSession().selectOne("findByNameStr4", keyWord);
	}

	@Override
	public User getUserByBean(User user) {
		return getSqlSession().selectOne("findByBean", user);
	}

	@Override
	public User getUserByBean2(User user) {
		return getSqlSession().selectOne("findByBean2", user);
	}

	@Override
	public User getUserByMap(Map<String, String> map) {
		return getSqlSession().selectOne("findByMap", map);
	}

	@Override
	public List<User> getUserByMap2(Map<String, String> map) {
		return getSqlSession().selectList("findByMap2", map);
	}

	@Override
	public List<User> getUserByMap3(Map<String, String> map) {
		return getSqlSession().selectList("findByMap3", map);
	}

	@Override
	public List<User> getUserByMap4(Map<String, String> map) {
		return getSqlSession().selectList("findByMap4", map);
	}

	@Override
	public List<User> getUserByArray(String[] names) {
		return getSqlSession().selectList("findByArray", names);
	}

	@Override
	public List<User> getUserByArray2(String[] keyWords) {
		return getSqlSession().selectList("findByArray2", keyWords);
	}

	@Override
	public List<User> getUserByList(List<String> names) {
		return getSqlSession().selectList("findByList", names);
	}

	@Override
	public List<User> getUserByList2(List<User> users) {
		return getSqlSession().selectList("findByList2", users);
	}
	
	@Override
	public List<User> getUserByList3(List<String> keyWords) {
		return getSqlSession().selectList("findByList3", keyWords);
	}
}

// Mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="">
	<resultMap id="userMap" type="User">
		<id property="id" column="id" />
		<result property="name" column="name" />
		<result property="password" column="password" />
	</resultMap>
	
	<!--
		输入参数为一个字符串,参数类型由输入的实质参数来决定;
		由于字符串是有TypeHandler的,下面配置在#{}里面的名称无作用,
		可以是name,也可以是A或0,甚至是abc,因为在这种情况下并不使用
		这个名称,只看占位符#{} (如果有多个,那么就按顺序,仍然不用名称) 
	 -->
	<select id="findByNameStr1" resultMap="userMap">
		select * from users where name=#{name}
	</select>	
	<select id="findByNameStr2" resultMap="userMap">
		select * from users where name=#{A}
	</select>	
	<select id="findByNameStr3" resultMap="userMap">
		select * from users where name=#{0}
	</select>
	<!--
		输入参数为一个字符串,参数类型由输入的实质参数来决定;
		虽然字符串有 TypeHandler,但是一当使用动态SQL中的bind语句,
		则参数值会先赋值给_parameter,通过bind语句对_parameter进行处理,
		则可以给参数指定一个名称(如abc),此时在#{}内必须使用新指定的名称。
		在bind语句中,支持使用OGNL表达式,也就是说如果输入参数为bean,那么
		还可以直接使用bean的方法,如:假设输入参数为一个User,那么也可以使
		用_parameter.getName() 。
	 -->
	<select id="findByNameStr4" resultMap="userMap">
		<bind name="abc" value="'%' + _parameter + '%'" />
		select * from users where name like #{abc}
	</select>
	
	
	<!-- 
		输入参数为Bean,此时需要在#{}内使用Bean的成员变量名,且该成员变量名
		必须有对应的getter。如果没有对应的getter,则会报错,如findByBean2中
		由于没有A对应的getter,则会报错:ReflectionException: There is no getter for property named 'A' in 'class com.mybatis.demo6.User'
	 -->
	<select id="findByBean" resultMap="userMap">
		select * from users where name=#{name}
	</select>	
	<select id="findByBean2" resultMap="userMap">
		select * from users where name=#{A}
	</select>
	
	
	<!-- 
		输入参数为Map,此时需要在#{}使用Map中的key值。
		Map中可以只有一个key,如例findByMap;
		可以有多个key分别对应不同字段,如例findByMap2;
		也可以多个key都对应同一个字段,如例findByMap3;
		还可以只用一个key来进行like,如例findByMap4;
	 -->
	<select id="findByMap" resultMap="userMap">
		select * from users where name=#{keyName}
	</select>	
	<select id="findByMap2" resultMap="userMap">
		select * from users where name=#{keyName} or password=#{keyPwd}
	</select>	
	<select id="findByMap3" resultMap="userMap">
		select * from users where name=#{keyName1} or name=#{keyName2}
	</select>	
	<!-- 对于like中有%的语句,如果不使用bind动态SQL,还可以使用下面的方式 -->
	<select id="findByMap4" resultMap="userMap">
		select * from users where name like CONCAT('%',#{keyWord},'%') or password like CONCAT('%',#{keyWord},'%')
	</select>
	
	
	<!-- 
		输入参数为数组时,要操作数组中的元素,简单的办法就是使用动态SQL(还有个办法就是自定义TypeHandler);
		foreach是动态SQL的一种,在foreach中里面的SQL段会以循环的方式出现,每次循环以separator属性中所配置
		的值分隔;而collection的类型为array(表示数组),index属性没什么影响,item属性的值会替换SQL中的#{},
		即item的值必须与#{}里的名称一致。如下例中,假如传入的参数为数组,数组长度为2,那么生成的SQL语句为:
		select * from users where name = #{__frch_keyName_0} or name = #{__frch_keyName_1}
		从上面可以看到foreach中的#{keyName}被转成了#{__frch_keyName_0}和#{__frch_keyName_1},个数与数组
		的长度一致;当然数组的元素值被放到一个map中,其key为#__frch_keyName_0、__frch_keyName_1,值就是数
		组的元素值,在后面取值的时候就使用map的方式通过key来取值即可。
	 -->
	<select id="findByArray" resultMap="userMap">
		select * from users where 
		<foreach item="keyName" index="index" collection="array" open="(" separator="or" close=")">
			name = #{keyName}
		</foreach>
	</select>	
	<!-- 
		当使用bind动态SQL时,数组会放到两重map中:
			第一重map:key=_parameter,value=第二重map;
			第二重map:key=array,value=数组。
		由于bind语句可以使用OGNL表达式进行取值,故在bind语句中也可以直接取出数组指定索引的值(如下例);
		不过这得预先知道数组元素的个数或者说数组元素的个数是固定的,此方法才有应用。
	 -->
	<select id="findByArray2" resultMap="userMap">
		<bind name="keyName" value="_parameter['array'][0]" />
		<bind name="keyPwd" value=" _parameter['array'][1]" />
		select * from users where name=#{keyName} or password=#{keyPwd}
	</select>
	
	
	<!-- 
		list与数组相似,上面对数组的描述基本可以适用于list,只是有一些属性需要更换:
		如foreach中的collection属性必须换成list,其它的原理一样;
		在使用bind语句时,第二重map的key值是list,value则是list。
	 -->
	<select id="findByList" resultMap="userMap">
		select * from users where 
		<foreach item="keyName" index="index" collection="list" open="(" separator="or" close=")">
			name = #{keyName}
		</foreach>
	</select>	
	<select id="findByList2" resultMap="userMap">
		select * from users where 
		<foreach item="user" index="index" collection="list" open="(" separator="or" close=")">
			name = #{user.name}
		</foreach>
	</select>	
	<select id="findByList3" resultMap="userMap">
		<bind name="keyName" value="_parameter['list'][0]" />
		<bind name="keyPwd" value=" _parameter['list'][1]" />
		select * from users where name=#{keyName} or password=#{keyPwd}
	</select>
</mapper>  

上面例子中主要列出了参数的下列使用情况:
  1. 单个字符串;
  2. Bean对象;
  3. Map对象;
  4. 数组;
  5. List;

这几种情况的使用方式已经在上面Mapper.xml中详细说明,但是为什么要这样做呢?


上面DAO实现类中是直接使用SqlSession的方式来操作,先看一看SqlSession接口的定义:

public interface SqlSession extends Closeable {
  <T> T selectOne(String statement);
  <T> T selectOne(String statement, Object parameter);
  <E> List<E> selectList(String statement);
  <E> List<E> selectList(String statement, Object parameter);
  <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);
  // 省略。。。
}
从上面接口定义中可以看到,无论是selectOne,还是selectList,真正用于SQL语句中的参数只有一个(第一个参数是用于选择SQL语句的ID,第二个参数才是用于SQL语句的参数),也就是只接受一个参数来组装SQL语句。这样就代表着,无论是什么形式的参数,都得弄成一个对象(接口参数的类型就是Object)。如果是使用Mapper接口的方式,虽然Mapper接口可以使用多个参数,但实际上会转成一个Map的方式,其中Key为0、1、2、3……等,即用参数在参数列表中的位置来表示Key,value则是参数的值;(实际上同时还在map中加入了以param0、param1……这种key,value同样是参数值),最后Mapper的代理调用的仍然是SqlSession的接口来进行数据库处理。

那SqlSession是怎样使用一个参数来满足多样化参数的情况呢?先来看一下org.apache.ibatis.scripting.defaults.DefaultParameterHandler:

public class DefaultParameterHandler implements ParameterHandler {
  // 省略……
  public void setParameters(PreparedStatement ps) throws SQLException {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject);
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
         // 分支1
         if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) { // 分支2
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { // 分支3
            value = parameterObject;
          } else { // 分支4
            value = metaObject == null ? null : metaObject.getValue(propertyName);
          }
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          if (typeHandler == null) {
            throw new ExecutorException("There was no TypeHandler found for parameter " + propertyName + " of statement " + mappedStatement.getId());
          }
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull();
          typeHandler.setParameter(ps, i + 1, value, jdbcType);
        }
      }
    }
  }
}
上面的代码就是一个根据参数的不同情况来决定使用不同的TypeHandler来设置SQL的值。其中parameterMappings就是封装了SQL句中各个占位符信息(一个#{}代表一个占位符)的对象;而parameterObject则是当前参数的封装对象,里面包含了参数的所有值信息(当然形式可能是字符串、Bean、Map、List等);typeHandler用于对PreparedStatement进行设置值;中间那4个if/else分支则决定当前次设置哪个值到PreparedStatement中。

分支情况:

  1. 如果使用了动态SQL,一般进入分支1;使用动态SQL时,通过对ForEachSqlNode的实现可以发现,动态SQL的参数值实际上是转为了一个Map,所以通过propertyName可取得对应的参数值;
  2. 如果进入分支2则代表没有参数了;
  3. 进入分支3,说明这些类型是有已注册的(包括默认或者自定义的)TypeHandler,像JAVA常见的类型(如int、long、byte、boolean、String)等都有默认的TypeHandler(可参见org.apache.ibatis.type.TypeHandlerRegistry);也可以在Mapper.xml中自定义TypeHandler。对于有已注册的TypeHandler,则可以直接从TypeHandler中取出值;
  4. 对于分支4,一般用于Map和Bean的类型。通过查看org.apache.ibatis.reflection.MetaObject可知道,对于Bean参数则封装到BeanWrapper中,对于Map参数则封装到MapWrapper中,而这些Wrapper都有共同的接口、实现不一样(Bean的通过getter获取属性值,Map通过get获取属性值),故可通过统一接口获取到值。

对于数组和list的处理更复杂一些:

1、首先在org.apache.ibatis.session.defaults.DefaultSqlSession中先放到map中

  private Object wrapCollection(final Object object) {
    if (object instanceof List) {
      StrictMap<Object> map = new StrictMap<Object>();
      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;
  }
2、在org.apache.ibatis.scripting.xmltags.DynamicContext中再塞到另外一个Map中

  public static final String PARAMETER_OBJECT_KEY = "_parameter";

  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());
  }
所以实际上数组和list的参数值就已经放到了两重map中。

3、在SqlNode的实现中再通过OGNL拿出来变成封装成简单的Map:

Object value = OgnlCache.getValue(expression, parameterObject);
故在SQL语句也就可以根据OGNL的语法把这些值拿出来来使用。