Mybatis参数处理

现象演示

我们在日常的开发中,可能会遇到下方所示的异常:

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

场景重现
创建mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <properties>
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&amp;characterEncoding=utf-8&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
    </properties>

    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

    <environments default="default">
        <environment id="default">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/ParametersMapper.xml" />
    </mappers>

</configuration>
创建ParametersMapper.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="com.ys.mybatis.mapper.ParametersMapper">

    <select id="getEmployeeByParameters" resultType="com.ys.mybatis.DO.EmployeeDO">
        select * from employee where id = #{id} and age = #{age}
    </select>

</mapper>
创建ParametersMapper
public interface ParametersMapper {

    EmployeeDO getEmployeeByParameters(Integer id, Integer age);
}
创建ParametersTest 
public class ParametersTest {
    private SqlSessionFactory sqlSessionFactory;

    @BeforeEach
    public void before() {
        InputStream inputStream = ConfigurationTest.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }

    @Test
    public void testMultipleParameters() {
        // 获取sqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 获取ParametersMapper对象
        ParametersMapper parametersMapper = sqlSession.getMapper(ParametersMapper.class);

        EmployeeDO employee = parametersMapper.getEmployeeByParameters(1, 18);

        System.out.println(employee);
    }
执行测试方法

解决方案

1.添加@Param注解
EmployeeDO getEmployeeByParameters(@Param("id") Integer id, @Param("age") Integer age);

2.使用-parameters
2.1 本地环境使用

2.2 修改maven配置
2.2.1 maven编译插件版本 < 3.6.2
<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.6.0</version>
        <configuration>
            <compilerArgs>
                 <arg>-parameters</arg>
            </compilerArgs>
        </configuration>
    </plugin>
</plugins>
2.2.2 maven编译插件版本 >= 3.6.2
<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.6.2</version>
        <configuration>
            <parameters>true</parameters>
        </configuration>
    </plugin>
</plugins>

参考链接 : Apache Maven Compiler Plugin – compiler:compile

PS : Springboot 2.0 以后的版本,默认启用此选项

SpringBoot 1.x 最后一个RELEASE版本,没有配置-parameters

源码解析

Mybatis与数据库的交互过程中,一般由MapperMethod的execute方法进行转发。MapperMethod的类结构如下:

public class MapperMethod {

  private final SqlCommand command;
  private final MethodSignature method;

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }
}
  • SqlCommand :记录本次本次sql指令的名称和类型(INSERT | UPDATE | DELETE | SELECT | FLUSH)
  • MethodSignature : 记录本次sql指令的返回值类型,以及参数名称解析器(ParamNameResolver)的实例化过程
ParamNameResolver的实例化过程
public ParamNameResolver(Configuration config, Method method) {
    // 默认值为true
    this.useActualParamName = config.isUseActualParamName();
    // 获取参数的类型
    final Class<?>[] paramTypes = method.getParameterTypes();
    // 获取参数上的注解
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<>();
    int paramCount = paramAnnotations.length;
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
        // 如果参数类型是RowBounds、ResultHandler则不处理
        if (isSpecialParameter(paramTypes[paramIndex])) {
            continue;
        }
        String name = null;
        for (Annotation annotation : paramAnnotations[paramIndex]) {
            // 如果存在@Param注解,则注解的value值就是name
            if (annotation instanceof Param) {
                hasParamAnnotation = true;
                name = ((Param) annotation).value();
                break;
            }
        }
        if (name == null) {
            if (useActualParamName) {
                // 如果设置了-parameters,name的值为coding的内容,否则就是arg0、arg1....这种形式
                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.put(paramIndex, name);
    }
    names = Collections.unmodifiableSortedMap(map);
}

根据是否存在@Param注解,以及useActualParamName属性值 ( 默认为true ),以测试方法为例,names大概有以下几种组合:

  • 存在@Param注解 :[ id,age ]
  • 不存在@Param注解
    • useActualParamName属性为true
      • 未设置-parameters: names:[ arg0,arg1 ]
      • 已设置-parameters: names:[ id,age ]
    • useActualParamName属性为false :[ 0,1 ]
convertArgsToSqlCommandParam方法

不管是增删改查,MapperMethod的execute方法都会调用MethodSignature的 convertArgsToSqlCommandParam 方法(内部调用ParamNameResolver的getNamedParams方法)。该方法根据names的个数和传入参数,分为以下几种情况进行处理:

  • names个数为0或传入参数为null : 返回 null
  • names个数为1且不存在@Param注解
    • 传入参数是集合
      • 如果参数类型是collection : 实例化一个ParamMap对象,将key为 names[0],value为 args[0] 的entry对象put到paramMap中,并额外put一个key为 "collection",value为 args[0] 的entry对象到paramMap中,返回 paramMap
        • 如果参数类型是list : 在参数类型是collection的基础上,额外put一个key为"list",value为 args[0] 的entry对象到paramMap中,返回paramMap
      • 如果参数类型是array : 实例化一个ParamMap对象,将key为 names[0],value为  args[0] 的entry对象put到paramMap中,并额外put一个key为"array",value为 args[0] 的entry对象到paramMap中,返回paramMap
    • 传入参数不是集合:返回args[0]
  • 其它 : 实例化一个ParamMap对象,遍历names对象,将key为 names[index],value为 args[index] 的entry对象put到paramMap中。每次循环会额外put一个key为"param" + index,value为 args[index] 的entry对象到paramMap中,返回paramMap

PS :不处理参数类型为RowBounds、ResultHandler参数。假如一个方法的传入类型分别是自定义实体类、RowBounds、ResultHandler,则有效参数个数为1

可能返回的结果
存在存在@Param注解或useActualParamName属性值为true并且设置了-parameters
{
    "id": "传入参数1",
    "age": "传入参数2",
    "param1": "传入参数1",
    "param2": "传入参数2"
}
useActualParamName属性值为true,未设置-parameters
{
    "arg0": "传入参数1",
    "arg1": "传入参数2",
    "param1": "传入参数1",
    "param2": "传入参数2"
}
useActualParamName属性值为false
{
    "0": "传入参数1",
    "1": "传入参数2",
    "param1": "传入参数1",
    "param2": "传入参数2"
}

所以对于测试案例,我们也可以将xml改成下方所示的形式:

<select id="getEmployeeByParameters" resultType="com.ys.mybatis.DO.EmployeeDO">
    select * from employee where id = #{param1} and age = #{param2}
</select>
演示有且仅有一个List类型的参数

根据上文的分析,如果有且仅有一个List类型的参数,且不存在@Param注解,在xml中如果使用了foreach标签,collection属性可以为"collection"、"list"、自定义名称。案例演示如下:

<select id="listEmployeeByIds" resultType="com.ys.mybatis.DO.EmployeeDO">
    select * from employee where id in
    <foreach collection="collection" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>
List<EmployeeDO> listEmployeeByIds(List<Integer> ids);
@Test
public void testSingleList() {
    // 获取sqlSession
    SqlSession sqlSession = sqlSessionFactory.openSession();

    // 获取ParametersMapper对象
    ParametersMapper parametersMapper = sqlSession.getMapper(ParametersMapper.class);

    List<EmployeeDO> list = parametersMapper.listEmployeeByIds(Arrays.asList(1, 2, 3));

    System.out.println(list);
}

上述案例中foreach标签的collection属性使用了"collection",这里还可以使用"list"、"ids"。

错误产生的原因

如果xml中存在占位符"#{param}",并且convertArgsToSqlCommandParam方法的返回值中,不存在一个name为param的key(返回值为paramMap)或 一个name为param的属性(返回值为实体类),则在增删改操作的sql填充或者查询操作创建cacheKey的过程中抛出异常

  • 26
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值