mybatis多参数传递报错问题分析+硬核mybatis底层源码分析+@Param注解+图文实战环境分析【4500字详解打通,没有比这更详细的了!】(二)

简介: mybatis多参数传递报错问题分析+硬核mybatis底层源码分析+@Param注解+图文实战环境分析【4500字详解打通,没有比这更详细的了!】

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 注解。

相关文章
|
2月前
|
SQL Java 数据库连接
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
MyBatis-Plus是一个MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。本文讲解了最新版MP的使用教程,包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段等核心功能。
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
|
2月前
|
SQL 缓存 Java
MyBatis如何关闭一级缓存(分注解和xml两种方式)
MyBatis如何关闭一级缓存(分注解和xml两种方式)
91 5
|
2月前
|
Java 数据库连接 mybatis
Mybatis使用注解方式实现批量更新、批量新增
Mybatis使用注解方式实现批量更新、批量新增
62 3
|
2月前
|
SQL 存储 数据库
深入理解@TableField注解的使用-MybatisPlus教程
`@TableField`注解在MyBatis-Plus中是一个非常灵活和强大的工具,能够帮助开发者精细控制实体类与数据库表字段之间的映射关系。通过合理使用 `@TableField`注解,可以实现字段名称映射、自动填充、条件查询以及自定义类型处理等高级功能。这些功能在实际开发中,可以显著提高代码的可读性和维护性。如果需要进一步优化和管理你的MyBatis-Plus应用程
209 3
|
2月前
|
Java 数据库连接 mybatis
Mybatis使用注解方式实现批量更新、批量新增
Mybatis使用注解方式实现批量更新、批量新增
184 1
|
4月前
|
SQL XML Java
mybatis复习02,简单的增删改查,@Param注解多个参数,resultType与resultMap的区别,#{}预编译参数
文章介绍了MyBatis的简单增删改查操作,包括创建数据表、实体类、配置文件、Mapper接口及其XML文件,并解释了`#{}`预编译参数和`@Param`注解的使用。同时,还涵盖了resultType与resultMap的区别,并提供了完整的代码实例和测试用例。
mybatis复习02,简单的增删改查,@Param注解多个参数,resultType与resultMap的区别,#{}预编译参数
|
3月前
|
Java 数据库连接 mybatis
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
该文档详细介绍了如何在Springboot Web项目中整合Mybatis,包括添加依赖、使用`@MapperScan`注解配置包扫描路径等步骤。若未使用`@MapperScan`,系统会自动扫描加了`@Mapper`注解的接口;若使用了`@MapperScan`,则按指定路径扫描。文档还深入分析了相关源码,解释了不同情况下的扫描逻辑与优先级,帮助理解Mybatis在Springboot项目中的自动配置机制。
204 0
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
|
3月前
|
Java 数据库连接 Maven
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
169 2
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
|
3月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
103 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
3月前
|
前端开发 Java Apache
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
本文详细讲解了如何整合Apache Shiro与Spring Boot项目,包括数据库准备、项目配置、实体类、Mapper、Service、Controller的创建和配置,以及Shiro的配置和使用。
717 1
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个