【MyBatis源码学习】参数解析


一、几种入参形式

这里只分析带有入参的方法。

1.单个入参

UserInfo selectByPrimaryKey(String id);

2.多个入参

List<UserInfo> getByOpenIdAndUsername2(@Param("openid") String openId, @Param("username") String username);

3.入参为实体对象

List<UserInfo> getByOpenIdAndUsername3(UserInfo userInfo);

4.入参为Map

List<UserInfo> getByOpenIdAndUsername(Map<String, Object> params);

二、mybatis执行入口

还是以之前的一个例子来进入我们今天的正题。

@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,存储了参数名的顺序映射。
/**
   * <p>
   * The key is the index and the value is the name of the parameter.<br />
   * 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}).
   * </p>
   * <ul>
   * <li>aMethod(@Param("M") int a, @Param("N") int b) -&gt; {{0, "M"}, {1, "N"}}</li>
   * <li>aMethod(int a, int b) -&gt; {{0, "0"}, {1, "1"}}</li>
   * <li>aMethod(int a, RowBounds rb, int b) -&gt; {{0, "0"}, {2, "1"}}</li>
   * </ul>
   */
  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也有很多信息,主要是

可以看出,cacheKey由namespace的id,分页参数,sql语句,入参以及节点的信息组成。
回到查询过程,实际执行的方法为

""

而首次查询不会进入if语句,调用BaseExecutor的方法:
""

本地缓存没有结果,故需要查询数据库,进入queryFromDatabase().
""

doQuery()则调用的为SimpleExecutor的方法。
""

看看newStatementHandler()。
""

""

由于我们的例子当中sql带有#{},故进入PREPARED,生成PreparedStatementHandler。
执行完语句后,放入一级缓存。
""

后续就是结果映射执行过程,这里不再赘述,后续跟上。

四、总结

本章主要讲述了mybatis参数解析的过程,重点跟踪了执行sql时,变量占位符${}以及参数占位符#{}的替换和解析过程。实际开发过程中,要注意这两者的区别,能用#{}的地方尽量用#。而${}主要用于order by语句、原生jdbc、表名作参数。


文章作者: Kezade
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Kezade !
评论
  目录