1.问题描述
❗操作 mybatis 时报错:org.apache.ibatis.binding.BindingException: Parameter ‘tableName’ not found. Available parameters are [arg1, arg0, param1, param2]
2.问题场景模拟再现
2.1 场景环境
- Maven
- MySQL 8.0.30
2.2 数据库与表创建
在本机 MySQL 中执行:
create database mybatis_demo; use mybatis_demo; create table `user`( id int primary key auto_increment comment '主键', `name` varchar(20) not null comment '姓名', sex char(1) not null comment '性别:男或女', age tinyint unsigned not null comment '年龄' ); insert into `user`(`name`,sex,age) values ('狐狸半面添','男',20), ('浪浪','女',18), ('浪浪','女',20);
2.3 Maven环境搭建
🍀 pom.xml导入依赖
<!-- junit单元测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.31</version> </dependency> <!-- MyBatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.11</version> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.24</version> </dependency>
🍀 jdbc.properties
在 resources
目录下新建 jdbc.properties
配置文件。
db.driver=com.mysql.cj.jdbc.Driver db.url=jdbc:mysql://localhost:3306/mybatis_demo?serverTimezone=GMT%2B8 db.username=root db.password=123456
🍀 mybatis-config.xml
在 resources
目录下新建 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 标签使用 resource 属性引入 jdbc.properties 配置文件 --> <properties resource="jdbc.properties"/> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <!--设置驱动类的全类名--> <property name="driver" value="${db.driver}"/> <!--设置连接数据库的连接地址--> <property name="url" value="${db.url}"/> <!--设置连接数据库的用户名--> <property name="username" value="${db.username}"/> <!--设置连接数据库的密码--> <property name="password" value="${db.password}"/> </dataSource> </environment> </environments> <!--引入映射文件--> <mappers> <mapper resource="mapper/UserMapper.xml"/> </mappers> </configuration>
🍀 User实体类
在 java
目录下新建 User
类。
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * @author 狐狸半面添 * @create 2023-07-02 1:24 */ @Data @NoArgsConstructor @AllArgsConstructor public class User { /** * 主键 id */ private Integer id; /** * 姓名 */ private String name; /** * 性别:男或女 */ private Character sex; /** * 年龄 */ private Integer age; }
🍀 Mapper 接口
在 java
目录下新建 UserMapper
接口。
import java.util.List; /** * @author 狐狸半面添 * @create 2023-07-02 1:27 */ public interface UserMapper { /** * 通过 姓名 与 性别 查询所有匹配的用户信息 * * @param name 姓名 * @param sex 性别 * @return 用户信息列表 */ List<User> selectByNameAndSex(String name, Character sex); }
🍀 UserMapper.xml 映射文件
在 resources
目录下创建 mapper
目录,再在 mapper 目录下 新建 UserMapper.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"> <!-- namespace:指定该映射文件对应的持久层接口路径,绑定 mapper.xml 与对应的mapper接口的关系 --> <mapper namespace="UserMapper"> <select id="selectByNameAndSex" resultType="User"> SELECT * FROM `user` WHERE `name` = #{name} and sex = #{sex} </select> </mapper>
2.4 测试报错
我们在 test / java
目录下创建 UserMapperTest.class
测试类。
import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.Test; import java.io.IOException; import java.io.InputStream; import java.util.List; import static org.apache.ibatis.io.Resources.getResourceAsStream; /** * @author 狐狸半面添 * @create 2023-07-02 1:35 */ public class UserMapperTest { private final static SqlSession sqlSession; static { // 1.读取 MyBatis 的核心配置文件 InputStream is = null; try { is = getResourceAsStream("mybatis-config.xml"); } catch (IOException e) { throw new RuntimeException(e); } SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is); sqlSession = sqlSessionFactory.openSession(true); } @Test public void testSelect() { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); List<User> users = userMapper.selectByNameAndSex("浪浪",'女'); users.forEach(System.out::println); } }
执行 testSelect
测试方法,则会报错:
3.问题分析
3.1 SQL解析原理
List<Student> selectByNameAndSex(String name,Character sex);
共有两个参数:name 与 sex。这是 多参数 的参数传递,那么 mybatis 框架底层怎么做的呢?
实际上,在不给形参加 @param 注解的情况下,每次调用该 selectByNameAndSex 方法时,mybatis 框架都会自动创建一个 map 集合用于存储实际参数,而每个实参都会用 两个键值对进行存储,例如我们传的 name 是 “浪浪”,sex 是 ‘女’,则该 map 集合存储内容为:
map.put("arg0", "浪浪"); map.put("param1", "浪浪"); map.put("arg1", '女'); map.put("param2", '女');
然后 mybatis 在解析我们编写的 SQL 语句时,会从 map 集合中根据 key 去取出数据,替换 SQL 中的 #{...}
或 ${...}
占位符。
例如对于我们在 UserMapper.xml
中写的 SQL 语句:
SELECT * FROM `user` WHERE `name` = #{name} and sex = #{sex}
mybatis 会先从 map 集合中找 key 为 name 的键值对,然而肯定是不存在的,因为只有 key 为 arg0、arg1、param1、param2 。因此从集合中获取到的就是 null
。mybatis 发现了查询为 null,则报错:
org.apache.ibatis.binding.BindingException: Parameter ‘name’ not found. Available parameters are [arg1, arg0, param1, param2]
翻译过来就是:参数 name 没有找到,只有参数为 arg0、arg1、param1、param2 的。
所以我们将映射文件中的 SQL 修改为:
SELECT * FROM `user` WHERE `name` = #{arg0} and sex = #{param2}
这时候就能够顺利查询成功了:
🚩 实现原理总结:实际上在 mybatis 底层会创建一个 map 集合,以 arg0/param1 为key,以方法上的参数为value 。
3.2 mybatis底层源码追踪之map集合创建
前提:本次源码追踪是建立在 第二小节 《问题场景模拟再现》 的基础上,即为 多参数传递 。
我们需要找到创建 map 结合的方法,先通过 ctrl + N
快捷键进行全局类搜索,输入 ParamNameResolver
:
找到 ParamNameResolver
类的 getNamedParams
方法,此方法就是用于创建 map 集合,并将此集合作为方法的返回值。
我们在此方法中标记一个断点,对我们的测试方法 testSelect()
进行 debug 调试,便会进入 getNamedParams
方法。
// args 是一个数组,内容为我们传的实际参数:["浪浪", '女'] public Object getNamedParams(Object[] args) { /* this.names 也是一个 map 集合,存储的键值对为: 1 - arg0 2 - arg1 因此得到的 paramCount 就是 2,这实际上是统计的 Mapper接口方法的形参个数 */ int paramCount = this.names.size(); // 如果存在实参并且形参个数不是0 if (args != null && paramCount != 0) { // 如果形参上没有 @Param 注解 并且 形参的个数是 1,就走 if 语句,否则走 else 语句 // 显然,我们的接口方法:List<User> selectByNameAndSex(String name,Character sex); // 是没有 @Param 注解的,但是形参个数为 2,因此 if 语句不成立,执行 else if (!this.hasParamAnnotation && paramCount == 1) { // 不执行 Object value = args[(Integer)this.names.firstKey()]; return wrapToMapIfCollection(value, this.useActualParamName ? (String)this.names.get(0) : null); } else { // 执行 // 创建一个 map 集合,最终返回该 map 集合作为 SQL 解析的占位参数的真实数据来源 Map<String, Object> param = new MapperMethod.ParamMap(); int i = 0; // 迭代器遍历 this.names 这个 map 集合的键值对,我们以第一次循环为例。 for(Iterator var5 = this.names.entrySet().iterator(); var5.hasNext(); ++i) { // 获取一个 键值对,例如第一次循环中,entry.key = 0,entry.value = "arg0" Map.Entry<Integer, String> entry = (Map.Entry)var5.next(); /* (String)entry.getValue() 结果是 "arg0" args[(Integer)entry.getKey()] 即 args[0] ,结果是 "浪浪" 因此存入 param 的键值对就是 param.put("arg0", "浪浪") */ param.put((String)entry.getValue(), args[(Integer)entry.getKey()]); // 设置 genericParamName 为 "param1" String genericParamName = "param" + (i + 1); // 查询 names 中是否包含 value值 为 param1 的键值对,在这里当然是不包含的,因此 if 成立 if (!this.names.containsValue(genericParamName)) { /* genericParamName 结果是 "param1" args[(Integer)entry.getKey()] 即 arg[0] ,结果是 "浪浪" 因此存入 param 的键值对就是 param.put("param1", "浪浪") */ param.put(genericParamName, args[(Integer)entry.getKey()] 即 arg[0] ,结果是 "浪浪"); } } /* 以上的 for 循环遍历完毕后,param 集合中存储的键值对为: "arg0" - "浪浪" "param1" - "浪浪" "arg1" - '女' "param2" - '女' */ // 返回这个 map 集合 return param; } } else { return null; } }