4.问题最佳解决方案-@Param
可以不用 arg0 arg1 param1 param2 吗?这个 map 集合的 key 我们自定义可以吗?
当然可以,其实在配置 SQL 映射时,使用 arg0 、arg1 或 param1 、param2 这样的名称,并不利于我们对代码的阅读和理解,所以 Mybatis 框架提供了 @Param
注解,可以在抽象方法的参数列表中,在每个参数之前添加该注解,并在注解中配置参数的名称,该名称就可以在 SQL 中作为占位参数 使用。这样就增强了代码可读性。
List<User> selectByNameAndSex( @Param("userName") String name, @Param("userSex") Character sex );
<!-- 使用了 @Param 注解之后,arg0 和 arg1 失效了 但 param1 和 param2 还可以用,但其实使用了@Param 注解,再使用 param1 和 param2 也没有什么意义了 --> <select id="selectByNameAndSex" resultType="User"> SELECT * FROM `user` WHERE `name` = #{userName} and sex = #{userSex} </select>
⚠️ 需要注意的是,使用了 @Param 注解之后,arg0 和 arg1 失效了,但 param1 和 param2 还可以使用。这是为什么呢?我们再次看下 map 集合创建方法的源码:
所以最终 map 集合的键值对添加顺序为:
map.put("userName", "浪浪"); map.put("param1", "浪浪"); map.put("userSex", '女'); map.put("param2", '女');
然后解析 SQL 时,遇到 占位参数 则会以 占位参数 作为 key 去 map 集合中查询对应的 value,以替换 占位参数。
🚩 核心:@Param(“**这里填写的其实就是 map 集合的 key **”)
5.一张图解释源码
6.拓展-单参数情况
当抽象方法的参数只有 1 个时,MyBatis 会自动的使用唯一的那一个参数,所以,在配置SQL映射时,使用的 #{} 占位符中的占位参数名称根本就不重要!在开发时,推荐使用规范的名称,但是,从代码是否可以运行的角度来说,这个占位符中写什么都不重要!
我们举一个例子来说明这件事:
- Mapper 接口方法:
User selectById(Integer id);
- UserMapper.xml 映射文件配置:
<select id="selectById" resultType="User"> SELECT * FROM `user` WHERE id = #{langLang} </select>
- 测试方法:
@Test public void testSingleSelect(){ UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.selectById(2); System.out.println(user); }
很明显,我们在 UserMapper.xml 映射文件中配置的 #{}
中占位参数 langLang
是一个我们从没有见过的参数名,但执行测试方法却能够执行成功查询到 id 为 2 的 数据信息。
关于底层实现,我们依旧以 debug 的方式先来看下 创建 map 集合的 getNamedParams
方法。
// args 是一个数组,内容为我们传的实际参数:[2] public Object getNamedParams(Object[] args) { /* this.names 也是一个 map 集合,存储的键值对为: 0 - arg0 因此得到的 paramCount 就是 1,这实际上是统计的 Mapper接口方法的形参个数 */ int paramCount = this.names.size(); // 如果存在实参并且形参个数不是0 if (args != null && paramCount != 0) { // 如果形参上没有 @Param 注解 并且 形参的个数是 1,就走 if 语句,否则走 else 语句 // 显然,我们的接口方法:User selectById(Integer id); // 是没有 @Param 注解的,并且形参个数为 1,因此 if 语句成立,执行 if if (!this.hasParamAnnotation && paramCount == 1) { // 执行 // 由于 names 只有一个键值对,因此 (Integer)this.names.firstKey() 获取的第一个key 就是 0 // 那么 value = args[0] 即 value 为 2 Object value = args[(Integer)this.names.firstKey()]; /* 1. value:表示需要处理的参数值。 2. this.useActualParamName:是一个布尔值,表示是否启用使用实际参数名称。这里为 true。 3. (String)this.names.get(0):这段代码从names列表中获取第一个参数名称,并将其转换为字符串类型。 调用本类的wrapToMapIfCollection方法, 该方法是根据参数的类型和配置的实际参数名称来进行适当的参数封装,以便正确地在SQL语句中使用。 在这里,该方法返回的结果是原 value 值 2,即 getNamedParams 返回的结果是 2 */ return wrapToMapIfCollection(value, this.useActualParamName ? (String)this.names.get(0) : null); } else { // 不执行 // 省略代码... } } else { return null; } }
public static Object wrapToMapIfCollection(Object object, String actualParamName) { MapperMethod.ParamMap map; // 这里的 object 是 Integer 类型的 2 // 判断 是否为 Collection 类型 --> 不是 if (object instanceof Collection) { map = new MapperMethod.ParamMap(); map.put("collection", object); if (object instanceof List) { map.put("list", object); } Optional.ofNullable(actualParamName).ifPresent((name) -> { map.put(name, object); }); return map; // 判断是否为数组 --> 不是 } else if (object != null && object.getClass().isArray()) { map = new MapperMethod.ParamMap(); map.put("array", object); Optional.ofNullable(actualParamName).ifPresent((name) -> { map.put(name, object); }); return map; } else { // 执行 // 因此返回的就是原对象,最终 getNamedParams 返回的结果就是 2 return object; } }
还没完噢,我们再找到调用 getNamedParams
方法的位置,找到 MapperMethod
类的 execute
方法:
// execute 方法源码 public Object execute(SqlSession sqlSession, Object[] args) { Object result; Object param; switch (this.command.getType()) { case INSERT: param = this.method.convertArgsToSqlCommandParam(args); result = this.rowCountResult(sqlSession.insert(this.command.getName(), param)); break; case UPDATE: param = this.method.convertArgsToSqlCommandParam(args); result = this.rowCountResult(sqlSession.update(this.command.getName(), param)); break; case DELETE: param = this.method.convertArgsToSqlCommandParam(args); result = this.rowCountResult(sqlSession.delete(this.command.getName(), param)); break; // 我们的 UserMapper.xml 映射文件中对应 selectById 方法的标签是 <select>,因此 this.command.getType() 走到这个 case case SELECT: if (this.method.returnsVoid() && this.method.hasResultHandler()) { this.executeWithResultHandler(sqlSession, args); result = null; } else if (this.method.returnsMany()) { result = this.executeForMany(sqlSession, args); } else if (this.method.returnsMap()) { result = this.executeForMap(sqlSession, args); } else if (this.method.returnsCursor()) { result = this.executeForCursor(sqlSession, args); } else { // 执行这里 // convertArgsToSqlCommandParam 方法实际是调用 getNamedParams 方法 // 那么 param 的值就是 2 param = this.method.convertArgsToSqlCommandParam(args); // 将 2 作为 参数进行查询单条数据 // result 就是查询结果:User(id=2, name=浪浪, sex=女, age=18) result = sqlSession.selectOne(this.command.getName(), param); if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + this.command.getName()); } if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) { throw new BindingException("Mapper method '" + this.command.getName() + "' attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ")."); } else { return result; } }
那实际上,对于 sqlSession.selectOne(this.command.getName(), param);
的内部实现,由于有些复杂,我们直接看对我们而言的核心部分:
/* CachingExecutor 是 MyBatis 中的一个执行器(Executor)实现类,它用于提供查询结果的缓存功能。 在 MyBatis 中,每次执行 SQL 查询时,都会创建一个执行器来处理查询操作。 CachingExecutor是其中的一种特殊类型,它负责在执行查询时缓存结果,以提高后续相同查询的性能。 */ public class CachingExecutor implements Executor { // 传入这个方法的参数 parameterObject 就是 我们的实参 2 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // getBoundSql 是获取包含 SQL 语句和参数的 BoundSql 对象。 /* 这个 boundSql 实例化对象有两个重要参数: - sql: SELECT * FROM `user` WHERE id = ? - parameterObject: 2 */ BoundSql boundSql = ms.getBoundSql(parameterObject); CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql); return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } }
所以我们可以看出,在 MyBatis 中,当只有一个 简单参数 传递给映射语句时,MyBatis 底层不会创建一个额外的参数 Map 集合来维护参数,即是不会去执行 在多参数状况下会执行的 ParamNameResolver
类的 getNamedParams
方法的 。相反,MyBatis 将直接使用传递的参数对象作为映射语句的参数。
⚠️ 注意这里说的是 简单参数 的情况下,那指的是什么呢?比如 参数如果是 Map 集合,那就不是简单参数。
7.不同 mybatis 版本下的默认 key 变化
使用 mybatis-3.4.2
之前的版本,底层创建的 map 集合 默认使用的 key 是:
- 0、1 …
- param1、param2 …
从 mybatis-3.4.2
开始,底层创建的 map 集合 默认使用的 key 是:
- arg0、arg1 …
- param1、param2 …
8.总结
关于以上问题,其根本原因在于 在 Java 源代码文件被编译为字节码文件(.class 文件)时,局部变量的名称通常不会被保留。这意味着即使在编写 Java 抽象方法时使用了具体的参数名,这些名称在最终的 .class 文件中是不会存在的。MyBatis 使用反射和动态代理来创建和执行 SQL 查询。在运行时,MyBatis 需要根据方法参数的位置和类型来匹配具体的 SQL 查询语句。但是,由于编译过程中参数名称的丢失,所以,即使在设计抽象方法时,使用了 name、sex 这样的参数名,在最终运行的 .class 文件中,根本就没有这样的名称!
当抽象方法的参数只有 1 个时,MyBatis 会自动的使用唯一的那一个参数,所以,在配置SQL映射时,使用的 #{} 占位符中的名称根本就不重要!在开发时,推荐使用规范的名称,但是,从代码是否可以运行的角度来说,这个占位符中写什么都不重要!
在 MyBatis3.4.2及以后的版本 的处理过程中,可以根据 arg 或 param 作为前缀,按照顺序来确定参数的值,所以,使用第 1 个参数时,可以使用 arg0 或 param1 作为名称,第 2 个可以使用 arg1 或 param2 作为名称,如果有更多参数,顺序编号即可,序号可参考抽象方法的声明。
但这种方式我们并不推荐,还是建议 使用 @Param 注解的方式。
mybatis 的 map 集合即 默认key 取值,与 mybatis 的版本有关,详细看 第七小节《不同 mybatis 版本下的默认 key 变化》。当然,
@Param
这个注解无论是 mybatis3.4.2 以前还是以后都是可以使用的。
🚩 总结:在使用 MyBatis 框架,设计抽象方法时,如果参数的数量超过1个(有2个或更多个),就为每一个参数都添加 @Param 注解。