mybatis原理:参数解析与SQL动态组装过程

          mybatis执行sql之前, 需要经过参数解析、sql动态组装等过程,本文主要聊聊mybatis的:

(1)参数解析原理及其过程

(2)sql动态组装原理及其过程

 

一、数据准备

1.实体类,省略了set、get方法

public class User {
    private String id;
    private String username;
    private String password;
    private Integer isValid;
}

2.mapper接口UserMapper,可以看作是一个根据用户名和密码的登录接口

 User getUserByUsernameAndPassword(@Param("name") String username, @Param("pwd") String password);

3.mapper映射

    <select id="getUserByUsernameAndPassword" resultType="com.qxf.pojo.User">
        select id,username,password,is_valid as isValid from t_user
        <where>
            <if test="name != null and name != ''">
                username = #{name}
            </if>
            <if test="pwd != null and pwd != ''">
                and password = #{pwd}
            </if>
        </where>
    </select>

4.测试,mybatis-config.xml配置文件按一般配置即可,这里就不贴代码了

        //读取配置信息
        InputStream  inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        //根据配置信息,创建SqlSession工厂
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
        //SqlSession工厂创建SqlSession
        SqlSession sqlSession = factory.openSession();
        //获取接口的代理对象
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        //执行相应的接口方法
        User user = mapper.getUserByUsernameAndPassword("张三2", null);
        System.out.println(user);

下面将以这句代码为入口:

(注意,这里只是为了测试,给密码参数传递了null,正常情况不会这样传递参数的,不然结果返回一个List集合就会报错的)

        //执行相应的接口方法
        User user = mapper.getUserByUsernameAndPassword("张三2", null);

 

二、参数解析原理及其过程

首先要明白一点,返回的是mapper接口的代理对象,所以会来到MapperProxy的invoke方法

 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            // Object对象的方法,则直接执行
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            }

            if (method.isDefault()) {
                return this.invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
        // 获取mapperMethod,这里面就会进行参数解析
        MapperMethod mapperMethod = this.cachedMapperMethod(method);
        // 执行方法
        return mapperMethod.execute(this.sqlSession, args);
    }

重点关注这句:

        // 获取mapperMethod,这里面就会进行参数解析
        MapperMethod mapperMethod = this.cachedMapperMethod(method);

参数的解析可以分成两部:

(1)形参的解析

(2)实参的封装

 

(1)形成的解析

一路跟进去,最终会来到 ParamNameResolver,暂且叫做参数名称解析器吧,首先会在构造器组装参数的位置和名称的对应关系,如果我们使用了@Param注解,则会使用我们定义的名称,否则会使用arg0、arg1....依次替代,详细代码如下:

 public ParamNameResolver(Configuration config, Method method) {
        // 获取参数列表中,每一个参数的类型
        Class<?>[] paramTypes = method.getParameterTypes();
        // 获取参数注解,因为每个参数可能有多个注解,所以是二维数组
        Annotation[][] paramAnnotations = method.getParameterAnnotations();
        // 存放结果的map
        SortedMap<Integer, String> map = new TreeMap();
        // 参数个数
        int paramCount = paramAnnotations.length;

        for(int paramIndex = 0; paramIndex < paramCount; ++paramIndex) {
            if (!isSpecialParameter(paramTypes[paramIndex])) {
                // 参数名称
                String name = null;
                // 参数的注解数组
                Annotation[] var9 = paramAnnotations[paramIndex];
                // 参数注解的个数
                int var10 = var9.length;
                // 遍历每个注解,找到Param注解,拿到value作为参数名称
                for(int var11 = 0; var11 < var10; ++var11) {
                    Annotation annotation = var9[var11];
                    if (annotation instanceof Param) {
                        this.hasParamAnnotation = true;
                        name = ((Param)annotation).value();
                        break;
                    }
                }

                if (name == null) {
                    if (config.isUseActualParamName()) {
                        name = this.getActualParamName(method, paramIndex);
                    }

                    if (name == null) {
                        name = String.valueOf(map.size());
                    }
                }
                // 参数序号作为key,从0开始,参数名称作为值
                map.put(paramIndex, name);
            }
        }
        // 没有做什么,再一次封装而已
        this.names = Collections.unmodifiableSortedMap(map);
    }

结果是这样的:符合我们的预期的

(2)实参的封装

 然后会来到getNamedParams方法对参数进一步的封装:

  public Object getNamedParams(Object[] args) {
        // 参数个数,这个names就是上面解析后的map,key是从0开始的参数序号,value是参数名称
        int paramCount = this.names.size();
        // 这里的args便是实参列表
        // 实参不为空,形参个数不为0
        if (args != null && paramCount != 0) {
            if (!this.hasParamAnnotation && paramCount == 1) {
                // 没有使用@Param注解,并且只有一个参数
                return args[(Integer)this.names.firstKey()];
            } else {
                // 将参数封装成一个map
                Map<String, Object> param = new ParamMap();
                int i = 0;
                // 对形参循环迭代
                for(Iterator var5 = this.names.entrySet().iterator(); var5.hasNext(); ++i) {
                    Entry<Integer, String> entry = (Entry)var5.next();
                    // names中的参数名称为key,值为实参值
                    param.put((String)entry.getValue(), args[(Integer)entry.getKey()]);
                    // 并添加key为param1、param2之类的通用参数
                    String genericParamName = "param" + String.valueOf(i + 1);
                    if (!this.names.containsValue(genericParamName)) {
                        param.put(genericParamName, args[(Integer)entry.getKey()]);
                    }
                }

                return param;
            }
        } else {
            return null;
        }
    }

通过源码可以发现,

(1)如果只有一个参数,并且没有使用@Param注解,就直接返回第一个参数

(2)有多个参数,则封装成一个map,key为参数参数名称,使用了@Param注解,名称就是注解中的值,否则key为arg0、arg1这种类型,同时,一定含有key为param1、param2的参数,值就是传入的值

封装后的结果如下:

这样就完成了参数的解析过程,总结一下:

(1)解析形参,判断是否使用了@Param注解

(2)封装实参,如果只有一个,并且没有使用@Param注解,就直接返回第一个参数值,否则封装成map

 

三、动态组装sql原理及其过程

来到CachingExecutor的如下方法,作为入口:

    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        // 获取组装完成的sql
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        // 创建缓存key
        CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
        // 执行查询
        return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

重点看这句:

        // 获取组装完成的sql
        BoundSql boundSql = ms.getBoundSql(parameterObject);

一路跟进去,来到DynamicSqlSource的getBoundSql方法:

   public BoundSql getBoundSql(Object parameterObject) {
        // 将参数封装成动态上下文,DynamicContext中sqlBuilder就是最后组装的sql
        DynamicContext context = new DynamicContext(this.configuration, parameterObject);
        // 根据条件,动态组装sql
        this.rootSqlNode.apply(context);
        SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(this.configuration);
        Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
        // 将#{参数}替换为?
        SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        Map var10000 = context.getBindings();
        Objects.requireNonNull(boundSql);
        var10000.forEach(boundSql::setAdditionalParameter);
        return boundSql;
    }

我们先看下这句:

        // 根据条件,动态组装sql
        this.rootSqlNode.apply(context);

对于我们的sql:

    <select id="getUserByUsernameAndPassword" resultType="com.qxf.pojo.User">
        select id,username,password,is_valid as isValid from t_user
        <where>
            <if test="name != null and name != ''">
                username = #{name}
            </if>
            <if test="pwd != null and pwd != ''">
                and password = #{pwd}
            </if>
        </where>
    </select>

每个标签都有对应的SqlNode来处理,比如if标签,就由IfSqlNode来处理,where标签,则会通过TrimSqlNode来处理,SqlNode的具体实现类如下:

 

 这里以IfSqlNode处理if标签为例:

这是就是两步:

(1)判断表达式的值是否为真,这里最终使用的是Ognl来判断

(2)如果表达式的为真,就将标签内容追加到sql中去

处理结果如下:

因为密码的参数传入为null,所以不会拼接密码查询条件,只拼接了用户名查询条件

 

然后是将#{参数}替换为?进行占位:

        // 将#{参数}替换为?
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());

这个就比较简单了,可以自行看源码,最终是这样的:

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值