现象演示
我们在日常的开发中,可能会遇到下方所示的异常:
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&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&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>
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 ]
- useActualParamName属性为true
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
- 如果参数类型是collection : 实例化一个ParamMap对象,将key为 names[0],value为 args[0] 的entry对象put到paramMap中,并额外put一个key为 "collection",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的过程中抛出异常