05 sql映射之入参取值

1 单参数

当我们的Mapper接口,只有一个参数的时候,mybatis不会做任何处理,都会将参数映射到sql语句中

比如:

  • mapper接口
TbUser selectById(Long id);
  • sql映射文件,通过#{id}可以将参数动态的映射到sql语句中,由于这里只有一个参数,所以大括号里面就所谓是啥,都可以
    映射到sql语句中,比如我下面随便写的#{dashjdkas},也可以将参数动态的映射到sql中
<select id="selectById" resultType="study.wyy.mybatis.sqlmapper.model.TbUser" parameterType="long">
    select
        *
    from
        tb_user
    where 
        id = #{id}
</select>

2 多个参数

2.1 多参数映射:

    TbUser selectByUsernameAndPhone(String username,String phone);

<select id="selectByUsernameAndPhone" resultType="study.wyy.mybatis.sqlmapper.model.TbUser">
    select
        *
    from
        tb_user
    where
        `username` = #{username} AND
        `phone` = #{phone}
</select>

测试:

@Before
public void getMapper() throws IOException {
    // 加载全局配置文件
    InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
    // 构建sqlSessionFactory
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    // 获取sqlSession
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 生成Mapper接口的代理对象
    tbUserMapper = sqlSession.getMapper(TbUserMapper.class);
}
@Test
public void test02() {
    TbUser tbUser = tbUserMapper.selectByUsernameAndPhone("kobe","13100001111");
    System.out.println(tbUser);
}

抛出异常:

Cause: org.apache.ibatis.binding.BindingException: Parameter 'username' not found. Available parameters are [arg1, arg0, param1, param2]

参数username没有找到,合法的参数是[arg1, arg0, param1, param2]

尝试修改一下sql映射的取值方式改为#{param1} 和 #{param2}:

<select id="selectByUsernameAndPhone" resultType="study.wyy.mybatis.sqlmapper.model.TbUser">
    select
        *
    from
        tb_user
    where
        `username` = #{param1} AND
        `phone` = #{param2}
</select>

或者使用arg0和arg1取值

<select id="selectByUsernameAndPhone" resultType="study.wyy.mybatis.sqlmapper.model.TbUser">
    select
        *
    from
        tb_user
    where
        `username` = #{arg0} AND
        `phone` = #{arg1}
</select>

再次测试,上面两种方式没有报错,并且参数成功映射到sql语句中

总结

多参数mybatis会进行处理:

  • 多个参数为封装成一个Map
  • map的key就是:param1,param2,param3 … paramN
  • map的value就是传入的参数值
  • 所以使用#{}取值的时候,就使用#{param1},#{param2}这种方式取值映射到sql语句中
  • 或者使用使用#{arg0}和#{arg1}取值

2.2 命名参数

还是期望#{参数名}这样取值,更加清晰:

<select id="selectByUsernameAndPhone" resultType="study.wyy.mybatis.sqlmapper.model.TbUser">
    select
        *
    from
        tb_user
    where
        `username` = #{username} AND
        `phone` = #{phone}
</select>

mybatis提拱了一个注解Param,来为参数命名,指定封装成Map时候使用的key

TbUser selectByUsernameAndPhone2(@Param("username") String username, @Param("phone") String phone);
<select id="selectByUsernameAndPhone2" resultType="study.wyy.mybatis.sqlmapper.model.TbUser">
    select
        *
    from
        tb_user
    where
        `username` = #{username} AND
        `phone` = #{phone}
</select>

3 参数为pojo或者Map

如果参数过多,还是封装成pojo比较好,当然mybatis也支持传递pojo

此时取值就使用#{属性名}或者#{key}


TbUser selectByPojo(TbUser tbUser);

TbUser selectByMap(Map<String,Object> map);
<select id="selectByPojo" resultType="study.wyy.mybatis.sqlmapper.model.TbUser">
    select
        *
    from
        tb_user
    where
        `username` = #{username} AND
        `phone` = #{phone}
</select>

<select id="selectByMap" resultType="study.wyy.mybatis.sqlmapper.model.TbUser">
    select
        *
    from
        tb_user
    where
        `username` = #{username} AND
        `phone` = #{phone}
</select>
    

测试:

@Test
public void test04() {
    TbUser user = new TbUser();
    user.setUsername("kobe");
    user.setPhone("13100001111");
    TbUser tbUser = tbUserMapper.selectByPojo(user);
    System.out.println(tbUser);
}
@Test
public void test05() {

    Map<String, Object> map = new HashMap<>();
    map.put("username","kobe");
    map.put("phone","13100001111");
    TbUser tbUser = tbUserMapper.selectByMap(map);
    System.out.println(tbUser);
}

3 参数为list或者数组

也会进行处理,会把list或者数组封装到map中,key的取值:

  • 如果是Collection:key为collection
  • 如果是List:key为list
  • 如果是数组:key为array

比如:

    TbUser selectByIds(List<Long> id);
<select id="selectByIds" resultType="study.wyy.mybatis.sqlmapper.model.TbUser" >
    select
        *
    from
        tb_user
    where
        <!--取出第一个元素-->
        id = #{list[0]}
</select>

测试

 @Test
 public void test06() {
     List<Long> ids = new ArrayList<>();
     ids.add(1L);
     TbUser tbUser = tbUserMapper.selectByIds(ids);
     System.out.println(tbUser);
 }

同样也支持使用Param注解,来为参数命名:

TbUser selectByIds(@Param("ids") List<Long> id);
<select id="selectByIds" resultType="study.wyy.mybatis.sqlmapper.model.TbUser" >
   select
       *
   from
       tb_user
   where
       <!--取出第一个元素-->
       id = #{ids[0]}
</select>

4 组合使用场景


TbUser select1(@Param("id") Long id, String phone);

TbUser select2(Long id, TbUser tbUser);

TbUser select3(@Param("id") Long id, @Param("user") TbUser tbUser);
   <select id="select1" resultType="study.wyy.mybatis.sqlmapper.model.TbUser" >
        select
        *
        from
        tb_user
        where
        `id` = #{id} AND
        `phone` = #{param2}
    </select>

    <select id="select2" resultType="study.wyy.mybatis.sqlmapper.model.TbUser" >
        select
        *
        from
        tb_user
        where
        `id` = #{param1} AND
        `phone` = #{param2.phone}
    </select>


    <select id="select3" resultType="study.wyy.mybatis.sqlmapper.model.TbUser" >
        select
        *
        from
        tb_user
        where
        `id` = #{id} AND
        `phone` = #{user.phone}
    </select>

5 源码解读

就是用这个方法来debug,解读一下源码,这个方法一个参数使用了Param注解,一个没有使用

TbUser select1(@Param("id") Long id, String phone);
@Test
public void test07() {
   TbUser tbUser = tbUserMapper.select1(1L,"13100001111");
}
  1. org.apache.ibatis.binding.MapperProxy的 invoker方法:
/****
	Object proxy: mybatis根据Mapper接口生成的动态代理类
	Method method: 要执行的方法,比如这里就是select1方法
	Object[] args:就是当前执行的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
  	// 判断当前要执行的方法是不是Object类的方法(比如toString等Object
  	//类中声明的方法),如果是那么就直接执行
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    } else {
    	// 如果不是Object类的方法,那么就执行cachedInvoker(method)
    	// 显然我们接口中声明的方法都会执行这块逻辑
      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}
  1. cachedInvoker方法内部:
    MapperMethodInvoker:两个实现: DefaultMethodInvoker和PlainMethodInvoker
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
 try {
 	// computeIfAbsent:首先会从缓存中获取MapperMethodInvoker对象, 如果缓存中没有那么就是构建MapperMethodInvoker并放入到methodCache(就是个Map)缓存中
   return methodCache.computeIfAbsent(method, m -> {
   	// 当前方法是不是Mapper接口的default方法,如果是default方法,就构造DefaultMethodInvoker对象返回
     if (m.isDefault()) {
       try {
         if (privateLookupInMethod == null) {
         	
           return new DefaultMethodInvoker(getMethodHandleJava8(method));
         } else {
           return new DefaultMethodInvoker(getMethodHandleJava9(method));
         }
       } catch (IllegalAccessException | InstantiationException | InvocationTargetException
           | NoSuchMethodException e) {
         throw new RuntimeException(e);
       }
     } else {
     	// 不是接口的默认方法,就构造MapperMethodInvoker的另一个实现:PlainMethodInvoker
     	// 先去构造MapperMethod
     	// mapperInterface: mapper接口的class对象
     	// method: 当前要执行的mapper接口中的方法
     	// sqlSession.getConfiguration(): 获取mybaits的全局配置
       return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
     }
   });
 } catch (RuntimeException re) {
   Throwable cause = re.getCause();
   throw cause == null ? re : cause;
 }
}
  1. 构造MapperMethod,来到构造方法
  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
  	// 构造SqlCommand: 解析mapper接口,封装statementId,以及操作类型(Select,update,delete。。),不多介绍
    this.command = new SqlCommand(config, mapperInterface, method);
    // 方法签名,
    this.method = new MethodSignature(config, mapperInterface, method);
  }

SqlCommand: 解析mapper接口,封装statementId,以及操作类型(Select,update,delete。。)这里不多介绍:
在这里插入图片描述

  1. 构建MethodSignature:
 public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
 		// 这些先不关心,解析当前执行的Mapper接口的方法的返回值,这里就是返回TbUser
      Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
      if (resolvedReturnType instanceof Class<?>) {
        this.returnType = (Class<?>) resolvedReturnType;
      } else if (resolvedReturnType instanceof ParameterizedType) {
        this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
      } else {
        this.returnType = method.getReturnType();
      }
      this.returnsVoid = void.class.equals(this.returnType);
      this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
      this.returnsCursor = Cursor.class.equals(this.returnType);
      this.returnsOptional = Optional.class.equals(this.returnType);
      this.mapKey = getMapKey(method);
      this.returnsMap = this.mapKey != null;
      this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
      this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
      // 核心在这构建paramNameResolver:参数名字解析器
      this.paramNameResolver = new ParamNameResolver(configuration, method);
    }
  1. 核心在这构建paramNameResolver:参数名字解析器
public ParamNameResolver(Configuration config, Method method) {
	// 1 获取参数类型,比如当前执行的这个select1方法的类型就是Long.class, String.class
    final Class<?>[] paramTypes = method.getParameterTypes();
      // TbUser select1(@Param("id") Long id, String phone);
    // 2 获取方法形参上的注解,id这个参数存在有一个注解Param,phone参数没有注解
    // 解释一下返回值二维数组:
    // 比如这里有两个参数id 和 phone,二维数组的长度就是2
    // 每个参数可以有多个注解,所以就是二维数组
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    
    final SortedMap<Integer, String> map = new TreeMap<>();
    int paramCount = paramAnnotations.length;
    // 遍历这个二维数组
    // get names from @Param annotations 遍历获取从@Param注解获取name
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
    	// 判断每一个参数类型是不是特殊的类型:RowBounds.class 或者ResultHandler.class
      if (isSpecialParameter(paramTypes[paramIndex])) {
        // skip special parameters:如果是特殊的就跳过
        continue;
      }
      String name = null;
      // 遍历每个参数上的每个注解
      for (Annotation annotation : paramAnnotations[paramIndex]) {
        if (annotation instanceof Param) {
       	// 当前参数的当前注解是Param注解,标记hasParamAnnotation设置为true
          hasParamAnnotation = true;
          // name 就是Param注解的value值
          name = ((Param) annotation).value();
          // 跳出当前循环
          break;
        }
      }
      // 如果name是null
      if (name == null) {
        // isUseActualParamName是全局配置文件的一个配置项,默认是true
        // protected boolean useActualParamName = true;
        if (config.isUseActualParamName()) {
        	// 没有使用Param注解指定参数名称,就调用这个方法生成name
          name = getActualParamName(method, paramIndex);
        }
        if (name == null) {
          // use the parameter index as the name ("0", "1", ...)
          // gcode issue #71
          name = String.valueOf(map.size());
        }
      }
      // 记录到前面定义的map,key是参数的索引,value就是name
      map.put(paramIndex, name);
    }
    // private final SortedMap<Integer, String> names; names属性就赋值为这个map
    names = Collections.unmodifiableSortedMap(map);
  }
  • 关于paramAnnotations二维数组图解:
    在这里插入图片描述
  • 加了Param注解的参数处理:
    在这里插入图片描述
  • 没有使用Param注解的参数处理:
    在这里插入图片描述
  • names属性就赋值为这个map
    在这里插入图片描述
  1. getActualParamName: 生成参数名称的逻辑
private String getActualParamName(Method method, int paramIndex) {
// 生成list:[arg0,arg1], 
  return ParamNameUtil.getParamNames(method).get(paramIndex);
}
// ParamNameUtil
public static List<String> getParamNames(Method method) {
  return getParameterNames(method);
}
private static List<String> getParameterNames(Executable executable) {
 	// 生成list:[arg0,arg1] 为啥是arg0,java编译之后,所有的形参都会编译成 arg0,arg0...,按照参数定义顺序排下去
  return Arrays.stream(executable.getParameters()).map(Parameter::getName).collect(Collectors.toList());
}
  1. 到此为止,MapperMethodInvoker就构造完成,就回到了这里,执行MapperMethodInvoker的invoke方法
 @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
      	// 执行MapperMethodInvoker的invoke
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }
  @Override
  public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
  	// 实际就是执行MapperMethod属性的execute方法
    return mapperMethod.execute(sqlSession, args);
  }
// MapperMethod 封装了SqlCommand和MethodSignature
// args: 当前的实参
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    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:
      	// method 方法签名的狗仔,前面已经提过了,判断当前方法的返回值类型
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
          // 还是集合
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
        	// convertArgsToSqlCommandParam:转换java参数到sql命令参数
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.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;
  }
  1. convertArgsToSqlCommandParam:转换java参数到sql命令参数

 public Object convertArgsToSqlCommandParam(Object[] args) {
 	// 就是调用paramNameResolver参数解析的getNamedParams方法
   return paramNameResolver.getNamedParams(args);
 }
  1. paramNameResolver参数解析的getNamedParams方法, 终于到了最核心的地方
// args: 实参
 public Object getNamedParams(Object[] args) {
 	// names是个Map,key是参数索引值,value是参数名称,上面已经分析这个map的生成
    final int paramCount = names.size();
    // 如果接口方法没有形参,或者参数个数是0,就直接返回
    if (args == null || paramCount == 0) {
      return null;
      // 如果hasParamAnnotation这个标记是false并且,形参个数只有一个,那么就直接返回names的第一个key
      // 所以这就是一开始说的,如果只有一个参数,#{}使用任何名称都可以取到参数,映射到sql中
    } else if (!hasParamAnnotation && paramCount == 1) {
      return args[names.firstKey()];
    } else {
    // 如果参数不只一个的时候,
    // param就是前面说的mybatis多参数的时候会把参数封装到一个map,现在就开始构造这个map
    // 首先这个map的value就是参数的值,主要看key的生成, 也就是使用#{}取值时,{}里面要写的是啥
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
      // 遍历names
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
      	// param的key就是参数名称,value是参数的值
      	// args是实参数组,names的key就是参数索引
        param.put(entry.getValue(), args[entry.getKey()]);
        // add generic param names (param1, param2, ...)
        // public static final String GENERIC_NAME_PREFIX = "param";
        // 拼接上param,这就是多参数的时候使用的#{paramN}来取参数的值
        final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
        // ensure not to overwrite parameter named with @Param
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      // 返回这个map
      return param;
    }
  }

在这里插入图片描述

6 扩展:useActualParamName

回头看一个东西:isUseActualParamName,是全局配置文件的一个配置项,默认是true

// 如果name是null
if (name == null) {
  // isUseActualParamName是全局配置文件的一个配置项,默认是true
  // protected boolean useActualParamName = true;
  if (config.isUseActualParamName()) {
  	// 没有使用Param注解指定参数名称,就调用这个方法生成name
    name = getActualParamName(method, paramIndex);
  }
  // 如果这个isUseActualParamNamefalse,并且没有使用Param注解使用指定参数名称,
  // 那么就不能使用#{arg0}方式取值,而是#{参数索引}取值,或者#{param0}这种方式
  if (name == null) {
    // use the parameter index as the name ("0", "1", ...)
    // gcode issue #71
    name = String.valueOf(map.size());
  }
}

看一下官网对useActualParamName这个配置的介绍:

允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。(新增于 3.4.1)

意思是可以直接通过形参的名称取值,但是两个前提条件

  1. useActualParamName为true:默认就是true,如果想要配置该如何配置:
<settings>
    <setting name="useActualParamName" value="true"/>
</settings>
  1. 使用java8编译,并且加上-parameters编写选项,可以使用maven编译插件配置
 <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                	<!--使用1.8-->
                    <target>1.8</target>
                    <source>1.8</source>
                    <encoding>utf-8</encoding>
                    <compilerArgs>
                    	<!--添加编译参数-->
                        <arg>-parameters</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>

测试一下:

// 没有使用注解指定参数名
TbUser select4(Long id,String phone);
<select id="select4" resultType="study.wyy.mybatis.sqlmapper.model.TbUser" >
    select
    *
    from
    tb_user
    where
    <!--这里也没使用#{param1}这种方式取值-->
    `id` = #{id} AND
    `phone` = #{phone}
</select>

可以用idea看一下编译后的class:
- TbUser select4(Long id, String phone);
之前:

  • TbUser select4(Long var1, String var2);

所以:就不再支持#{arg0}这种取值了,所以啊不要用#{arg0}这种方式取值,存在兼容

    <select id="select4" resultType="study.wyy.mybatis.sqlmapper.model.TbUser" >
        select
        *
        from
        tb_user
        where
        `id` = #{arg0} AND
        `phone` = #{arg1}
    </select>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值