这里只分析带有入参的方法。
UserInfo selectByPrimaryKey(String id);
List<UserInfo> getByOpenIdAndUsername2(@Param("openid") String openId, @Param("username") String username);
List<UserInfo> getByOpenIdAndUsername3(UserInfo userInfo);
List<UserInfo> getByOpenIdAndUsername(Map<String, Object> params);
还是以之前的一个例子来进入我们今天的正题。
@Test
// 快速入门
public void quickStart() throws IOException {
//--------------------第二阶段---------------------------
// 2.获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3.获取对应mapper
UserInfoMapper mapper = sqlSession.getMapper(UserInfoMapper.class);
//--------------------第三阶段---------------------------
// 4.执行查询语句并返回单条数据
UserInfo user = mapper.selectByPrimaryKey("1");
System.out.println(user);
}
当我们执行到这一行时,
UserInfoMapper mapper = sqlSession.getMapper(UserInfoMapper.class);
通过调试我们可以看到,这个mapper其实是通过MapperProxy代理执行的。我们拿到的其实就是个动态代理对象。如下图:
当我们执行查询时,进入MapperProxy动态代理过程。
最终交由MapperMethod类的execute()方法执行,源代码如下:
//三步翻译在此完成
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//第一步 根据sql语句类型以及接口返回的参数选择调用不同的方法
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {//返回值为void
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {//返回值为集合或者数组
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {//返回值为map
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {//返回值为游标
result = executeForCursor(sqlSession, args);
} else {//处理返回为单一对象的情况
//通过参数解析器解析解析参数
Object param = method.convertArgsToSqlCommandParam(args);//第三步翻译,将入参转化成Map
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional() &&
(result == null || !method.getReturnType().equals(result.getClass()))) {
result = OptionalUtil.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
所以,本章参数解析的过程,我们重点关注这个方法即可,这个方法就是convertArgsToSqlCommandParam。它其实是方法的参数解析器ParamNameResolver的getNamedParams()方法完成的。
而这个ParamNameResolver则是在MapperProxy获取mapperMethod时(先不说从cache中取)进行初始化的,
而ParamNameResolver实例化时,主要工作就是进行初步的映射关系存储,其字段names是一个SortedMap,存储了参数名的顺序映射。
/**
*
* The key is the index and the value is the name of the parameter.
* The name is obtained from {@link Param} if specified. When {@link Param} is not specified,
* the parameter index is used. Note that this index could be different from the actual index
* when the method has special parameters (i.e. {@link RowBounds} or {@link ResultHandler}).
*
*
* - aMethod(@Param("M") int a, @Param("N") int b) -> {{0, "M"}, {1, "N"}}
* - aMethod(int a, int b) -> {{0, "0"}, {1, "1"}}
* - aMethod(int a, RowBounds rb, int b) -> {{0, "0"}, {2, "1"}}
*
*/
private final SortedMap<Integer, String> names;
继续看getNamedParams()方法。
//将多个参数封装成MAP
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
} else {
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
本例子中,由于无@Param注解,所以在第二个else if那里就返回了。
以下面的代码为例,其他形式的入参大同小异。
@Test
public void testManyParamQuery() {
// 2.获取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3.获取对应mapper
UserInfoMapper mapper = sqlSession.getMapper(UserInfoMapper.class);
String username = "zyx";
String openId = "zyxelva";
// 第一种方式使用map
Map<String, Object> params = new HashMap<>();
params.put("username", username);
params.put("openid", openId);
List<UserInfo> list1 = mapper.getByUsernameAndOpenId(params);
System.out.println(list1);
}
对应的mapper.xml方法
<select id="getByUsernameAndOpenId" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from user_info
where username=#{username} and openid=#{openid}
select>
我们从方法
List<UserInfo> list1 = mapper.getByUsernameAndOpenId(params);
开始断点调试。我们看到,进入到了MapperProxy的动态代理过程。直接进入mapperMethod.execute(sqlSession, args);
来到MapperMethod中的execute方法中。由于我们的例子是查询操作,故进入Select。又例子的返回类型是List,故进入第二个if语句中。
进入方法executeForMany(). 没有分页,所以进入else语句中。而convertArgsToSqlCommandParam方法我们在二中已经分析了,这里不再具体梳理。
进入DefaultSQLSession的selectList方法中。可以看下statement实际形式是namespace+id,就可以从MappedStatement中获取。
而MappedStatement也有很多信息,主要是标签以及该节点其他重要的属性。
而sql的实际执行者,则为Executor. 代理对象执行方法被拦截器拦截,执行以下方法。
先看看getBoundSql干了啥:
进入RawSqlSource,因例子的sql无${},无动态SQL节点。
先看看RawSqlSource的构造方法,看看#{}是怎么替换成?的,发现主要是SqlSourceBuilder在干活儿。
主要看GenericTokenParser.parse().该方法主要完成占位符的定位工作,然后把占位符的替换工作交给与其关联的 TokenHandler 处理.
TokenHandler有四个小弟,
BindingTokenParser:该对象的handleToken方法会取出占位符中的变量,然后使用该变量作为键去上下文环境中寻找对应的值。之后,会用找到的值替换占位符。因此,该对象可以完成占位符的替换工作;
DynamicCheckerTokenParser:该对象的 handleToken 方法会置位成员属性isDynamic。因此该对象可以记录自身是否遇到过占位符。
ParameterMappingTokenHandler:将DynamicSqlSource和RawSqlSource中的“#{}”符号替换掉,从而将他们转化为StaticSqlSource;
VariableTokenHandler:handleToken方法中传入输入参数后,该方法会以输入参数为键尝试从variables属性中寻找对应的值返回。
到此BoundSql的解析过程基本结束。getBoundSql方法执行完后,我们再看看一级缓存的key生成策略。
可以看出,cacheKey由namespace的id,分页参数,sql语句,入参以及节点的信息组成。
回到查询过程,实际执行的方法为
而首次查询不会进入if语句,调用BaseExecutor的方法:
本地缓存没有结果,故需要查询数据库,进入queryFromDatabase().
doQuery()则调用的为SimpleExecutor的方法。
看看newStatementHandler()。
由于我们的例子当中sql带有#{},故进入PREPARED,生成PreparedStatementHandler。
执行完语句后,放入一级缓存。
后续就是结果映射执行过程,这里不再赘述,后续跟上。
本章主要讲述了mybatis参数解析的过程,重点跟踪了执行sql时,变量占位符${}以及参数占位符#{}的替换和解析过程。实际开发过程中,要注意这两者的区别,能用#{}的地方尽量用#。而${}主要用于order by语句、原生jdbc、表名作参数。