刚开始使用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>
上面例子中主要列出了参数的下列使用情况:
- 单个字符串;
- Bean对象;
- Map对象;
- 数组;
- 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中。
分支情况:
- 如果使用了动态SQL,一般进入分支1;使用动态SQL时,通过对ForEachSqlNode的实现可以发现,动态SQL的参数值实际上是转为了一个Map,所以通过propertyName可取得对应的参数值;
- 如果进入分支2则代表没有参数了;
- 进入分支3,说明这些类型是有已注册的(包括默认或者自定义的)TypeHandler,像JAVA常见的类型(如int、long、byte、boolean、String)等都有默认的TypeHandler(可参见org.apache.ibatis.type.TypeHandlerRegistry);也可以在Mapper.xml中自定义TypeHandler。对于有已注册的TypeHandler,则可以直接从TypeHandler中取出值;
- 对于分支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的语法把这些值拿出来来使用。