美文网首页
mybatis运行原理05-mapperMethod.execu

mybatis运行原理05-mapperMethod.execu

作者: 布拉德老瓜 | 来源:发表于2021-03-10 23:17 被阅读0次

背景

上一篇中讲了代理对象的方法调用,实际是通过mapperProxy.invoke(proxyObj, method, args)来完成的。在执行过程中,需要先判断方法是不是Object类定义的。

  • 若方法是Object类定义的方法,则直接执行。
  • 若不是,则需要创建对应的invoker来处理,创建逻辑由lambda表达式mapperFunction来定义:对于大多数接口方法而言,他们是非default的,为其创建PlainMethodInvoker,并存入到cachedMethod中。

在创建PlainMethodInvoker的时候,先要创建其核心对象MapperMethod,MapperMethod的包含两个重要属性:

  • sqlCommand: 用于指定方法名称和操作类型(select/update/insert...),
  • 方法签名methodSignature: 为MapperMethod指定resultHandlerIndex(用于找到resultHandler)和paramNameResolver(用于参数名解析)。

最后调用方法执行的时候,PlainMethodInvoker.invoke(proxy, method, args, sqlSession),实际上就是调用了mapperMethod.execute(sqlSession, args)来获取到结果。

mapperMethod.execute(sqlSession, args)

在execute的时候,有一些问题需要考虑:execute方法做了哪些事?参数是如何被解析到sql中的?这个execute()过程中mybatis是怎样与数据库交互的获取到结果集的?Object类型的结果集又是怎么被封装成了指定的类型的?ResultHandler是个什么东西,它又是怎样处理返回结果的?

在本文中,以查询一个List<Entity>来进行分析。创建sqlConmmand之后,就知道该sql是SELECT类型的。mapperMethod解析方法签名的时候,就已经将方法返回值类型、结果处理器和参数名解析器准备好了。所以现在可以轻松知道该方法返回结果为many,进入result = this.executeForMany(sqlSession, args)分支。

public class MapperMethod {
    private final MapperMethod.SqlCommand command;
    private final MapperMethod.MethodSignature method;

    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        Object param;
        switch(this.command.getType()) {
        case INSERT:
            //将方法的参数解析为sql语句的参数
            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;
        //对于增删改而言,方法的返回结果比较固定,就是影响的行数。
        //但对于查找来说,返回结果则复杂一点
        case SELECT:        
            //如果返回void类型且定义了ResultHandler,那么需要resultHandler去处理查找结果并返回void。
            if (this.method.returnsVoid() && this.method.hasResultHandler()) {
                this.executeWithResultHandler(sqlSession, args);
                result = null;
            //返回结果为多条记录
            } else if (this.method.returnsMany()) {
                result = this.executeForMany(sqlSession, args);
            //返回结果为Map     
            } else if (this.method.returnsMap()) {
                result = this.executeForMap(sqlSession, args);
            //返回游标位置
            } else if (this.method.returnsCursor()) {
                result = this.executeForCursor(sqlSession, args);
            } else {
            //常规查找, 先解析参数, 若结果为null或返回结果类型与方法的返回类型不一致,则返回Optional.ofNullable.(看似非空,实则为空,避免空指针异常)
                param = this.method.convertArgsToSqlCommandParam(args);
                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;
        }
    }
......
    private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
        Object param = this.method.convertArgsToSqlCommandParam(args);
        List result;
        if (this.method.hasRowBounds()) {
            RowBounds rowBounds = this.method.extractRowBounds(args);
            result = sqlSession.selectList(this.command.getName(), param, rowBounds);
        } else {
            result = sqlSession.selectList(this.command.getName(), param);
        }

        if (!this.method.getReturnType().isAssignableFrom(result.getClass())) {
            return this.method.getReturnType().isArray() ? this.convertToArray(result) : this.convertToDeclaredCollection(sqlSession.getConfiguration(), result);
        } else {
            return result;
        }
    }

先说一下executeForMany大致的执行流程:

  1. 利用方法签名来解析参数

  2. 判断是否有RowBounds, RowBounds的主要作用是进行分页。

    • 如果有,先把分页参数提取出来,提取完进行selectList
    • 没有则直接selectList
  3. selectList完之后就已经产生结果集了,现在要判断是否可以将返回结果集赋值给方法的返回值类型。

    • 可以则判断一下返回值类型:如果是返回数组类型则将结果转数组,是Collection类型则转为对于的Collection
    • 不可以的话,直接将result返回。

每一步具体过程如下:

1. 解析参数

mapperMethod ——>方法签名 ——>paramNameResolver,一层层委派。

        public Object convertArgsToSqlCommandParam(Object[] args) {
            return this.paramNameResolver.getNamedParams(args);
        }

具体承担参数解析责任的是paramNameResolver,其解析方法如下。

public class ParamNameResolver {
    public static final String GENERIC_NAME_PREFIX = "param";
    // 以sortedMap存放参数名
    private final SortedMap<Integer, String> names;
    private boolean hasParamAnnotation;


    public Object getNamedParams(Object[] args) {
        int paramCount = this.names.size();
        if (args != null && paramCount != 0) {
            //没有@Param注解且参数名只有1个,那么直接返回参数args[map.firstkey()]
            if (!this.hasParamAnnotation && paramCount == 1) {
                return args[(Integer)this.names.firstKey()];
            } else {
                // 将参数名与参数值对应起来,存放到paramMap中。
                // 需要遍历names中的每一个节点,获取参数名paramName和该参数名在参数数组中的位置index,
                // 然后以paramName: args[index]存放到paramMap内。
                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();
                    param.put((String)entry.getValue(), args[(Integer)entry.getKey()]);
                    String genericParamName = "param" + (i + 1);
                    if (!this.names.containsValue(genericParamName)) {
                        param.put(genericParamName, args[(Integer)entry.getKey()]);
                    }
                }

                return param;
            }
        } else {
            return null;
        }

该方法就是将参数名和参数值对应起来,举例如下。

举个例子来说:在candidates表中有一个方法来筛选候选人
    public List<Candidate> getCandidatesByCondition(@Param("age") int age,
                                                    @Param("workYears") int workYears,
                                                    @Param("educationLevel") int educationLevel);
其names假设为{0: "age", 1: "workYears", 2: "educationLevel"}
调用传入参数为getCandidatesByCondition(30, 3, 2);
那么得到的param = {"age" : 30, "workYears" : 3, "educationLevel" : 2}

2. 是否分页

        // MethodSignature.hasRowBounds()
        public boolean hasRowBounds() {
            return this.rowBoundsIndex != null;
        }
      // 若分页,则根据rowBoundsIndex从参数数组中提取出分页的参数。

3. sqlSession.selectList()查询结果集。

绕了一大圈,又回到了sqlSession中。(想想绕了一圈框架做了什么?我们得到了什么?搞了个代理对象,创建了InvocationHandler(即mapperProxy对象),然后又创建了MapperMethod对象,最后就将sql语句和接口方法名对应起来了(sqlCommand.name<-->MappedStatements {name: MappedStatement}),方法参数也解析了,返回结果类型也已经很明确了。)

但是呢,有些事情还没有做,比如sql语句中还有些占位符没搞定(现在的sql还是 select ... from table where xx = ? and zz = ?, 存放在sqlSource中,如下面的代码块),而我们想要的是先创建preparedStatement,然后再由jdbc去完成数据的查询。现在还差"?"对应的实际参数呢。数据库连接也没创建,语句还没执行,那事务也还没开启。前面准备工作都做好了,现在就要开始着手处理这些问题了。

还是以筛选候选人方法对应的Mappedstatement来说:
它在Mappedstatement中的key是namespace + 标签的id : "org.orange.dao.CandidateDao.getCandidatesByCondition"
其sqlSource的属性是:
{
    sql: "select * from candidates where work_years >= ? and education_level >= ? and age <= ?"
    parameterMappings:[
      {property: "workYears", JavaType:{...}, ...},
      {property: "educationLevel", JavaType:{...}, ...},
      {property: "age", JavaType:{...}, ...}
    ],
  ...
}
1. 通过sqlCommand的name就可以查到这个Mappedstatement了
2. 这条sql语句后面应该再添上参数列表才成了preparedStatement的样子,根据parameterMappings和pareameterMap
   可以将参数对应起来,从而产生参数列表。
具体做法请看 3.1

selectList流程如下:

3.1. 根据sqlCommand的name获取mappedStatement.

之前sql语句的信息都存放到了configuration对象的MappedStatements中,其key为xxMapper.xml中sql语句的namespace+id。现在我们不是在sqlCommand中存放了方法名(类名+方法名)吗,根据它和sql语句的对应关系就可以取出MappedStatement。

从这里就可以理解为什么mapper接口的方法不可以重载也不可以有不同返回值了:因为要确定一个方法的两个属性是方法名和方法签名。而mybatis中方法与sql语句的映射仅仅是通过方法名和sql的namespace+id一一对应的。

同时,也可以理解sql语句中的占位符应该怎样被替换成方法参数:在查询的时候还需要将原来的占位符替换成参数列表中的值。参数列表在参数解析之后就存到了paramMap中,key是参数名,它和xml中sql语句里#{}中的字符串是对应的。现在#{}中的字符串存放在了mappedStatement中的parameterMappings数组内。只需要在遇到第i个"?"的时候去statementparamMap中查它的property, 将其作为key,然后查paramMap中该key对应的value,最后将value放到字符串最后参数对相应位置就完成了参数的替换,即生成了preparedStatement对象。

如图:mapper接口方法与mapper.xml文件终于走到了一起(撒花)


接口方法与sql, mapperMethod与mappedStatement
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        List var5;
        try {
            //根据sqlCommand的name属性取出sql语句的信息MappedStatement
            MappedStatement ms = this.configuration.getMappedStatement(statement);
            //executor完成数据库操作
            var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception var9) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var9, var9);
        } finally {
            ErrorContext.instance().reset();
        }

        return var5;
    }

3.2.激动人心的时刻来了,是的,我们开始查数据了。

executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

不过先得搞明白两件事情:

  • 在调用前先对参数parameter做了一次包装,这次包装做了什么事
  • executor是哪个对象

第一个问题: wrapCollection的逻辑很简单,创建一个map,判断参数是什么类型的:容器、List、数组还是Object?
如果是前三个,则以类型为key,当前object为value存到map中并返回该map,如果是Object类的,就直接返回该object。

    private Object wrapCollection(Object object) {
        DefaultSqlSession.StrictMap map;
        if (object instanceof Collection) {
            map = new DefaultSqlSession.StrictMap();
            map.put("collection", object);
            if (object instanceof List) {
                map.put("list", object);
            }

            return map;
        } else if (object != null && object.getClass().isArray()) {
            map = new DefaultSqlSession.StrictMap();
            map.put("array", object);
            return map;
        } else {
            return object;
        }
    }

第二个问题:在看query方法的执行逻辑之前,先了解一下Executor接口,毕竟人家是核心接口。它的UML结构如下


Executor接口继承关系图

一开始我们在创建sqlSession的时候,使用的是SimpleExecutor, 由于开启了缓存,因此在executor创建完成之后,为其包装了一个CachingExecutor.因此sqlSession中的executor是CachingExecutor,它具有原来executor的全部功能。

        // newExecutor(tx, exeType), exeType=Simple
        //...
        if (this.cacheEnabled) {
            executor = new CachingExecutor((Executor)executor);
        }

    //CachingExecutor构造方法
    public CachingExecutor(Executor delegate) {
        this.delegate = delegate;
        delegate.setExecutorWrapper(this);
    }

    //CachingExecutor的delegate 就是传过来的SimpleExecutor
    public void setExecutorWrapper(Executor wrapper) {
        this.wrapper = wrapper;
    }

现在我们内存中有两个Executor,分别是cachingExecutor和baseExecutor,他们的关系是你中有我,我中有你。


当前系统中cachingExecurot与simpleExecutor的关系

再来看query方法,当前query方法的实际调用者是CachingExecutor。


sqlSession.execute(...)调用cachingExecutor的query方法

cachingExecutor.query()方法:

  • 首先完成了参数绑定,将sql语句信息存放到boundSql中,
  • 然后创建缓存key
  • 最后执行query(ms, parameterObject, rowBounds, resultHandler, key, boundSql)
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
        return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

3.2.1. getBoundSql方法

在前面我们通过sqlCommand.name取出了mappedStatements中的mappedStatement对象,关于sql语句的信息存放在了该对象中,例如sql语句和参数映射存放在了sqlSource属性内。还是以前面的图来说明。


image.png

现在要做的事情就是将sql和参数进行绑定,也就是将参数放到sql语句后面,创建一个列表,第i个"?"对应的参数就是在paramMap中以第i个property为key查出来的参数值。
所以boundSql对象相比较sqlSource而言,就是parameterObject不为null, 它内部就是paramMap。根据sql, paramMap和paramMappings三个对象就可以构建出preparedStatement了。

    public BoundSql getBoundSql(Object parameterObject) {
        //sqlSource存储的是原始的sql语句,其sql属性就是<select>标签下的字符串。
        //首先sqlSource来完成boundSql 创建,将parameterMappings传入boundSql.
        BoundSql boundSql = this.sqlSource.getBoundSql(parameterObject);
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

        //根据参数映射和参数对象,完成参数的绑定
        if (parameterMappings == null || parameterMappings.isEmpty()) {
            boundSql = new BoundSql(this.configuration, boundSql.getSql(), this.parameterMap.getParameterMappings(), parameterObject);
        }

        Iterator var4 = boundSql.getParameterMappings().iterator();
        // 判断参数映射是否有嵌套的resultMap并将结果写回到mappedStatement对象中
        while(var4.hasNext()) {
            ParameterMapping pm = (ParameterMapping)var4.next();
            String rmId = pm.getResultMapId();
            if (rmId != null) {
                ResultMap rm = this.configuration.getResultMap(rmId);
                if (rm != null) {
                    this.hasNestedResultMaps |= rm.hasNestedResultMaps();
                }
            }
        }

        return boundSql;
    }

3.2.2. 创建cacheKey

        CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);

找到缓存key,如果使用缓存,先查缓存。这点暂时先放着。

3.2.3. query(..., boundSql)

如果使用缓存,先查缓存,查不到的话查数据库,然后将结果写缓存。
缓存查不到或者不允许使用缓存,使用simpleExecutor的query查数据库(实际就是调用其父类BaseExecutor的query方法)。

    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        Cache cache = ms.getCache();
        if (cache != null) {
            this.flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                this.ensureNoOutParams(ms, boundSql);
                List<E> list = (List)this.tcm.getObject(cache, key);
                if (list == null) {
                    list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    this.tcm.putObject(cache, key, list);
                }

                return list;
            }
        }

        return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

有点累了,BaseExecutor.query(..., boundSql)下一篇再发吧
做一下总结,mapperMethod.execute()方法中做了这样一些事情:

  • 根据mapperMethod的sqlCommand和methodSignature两个属性,确定执行sql的类型增删改查,查询又分返回多个结果、单个结果、返回map和返回cursor.
  • 解析方法参数,根据是否有@param注解和参数个数决定怎么解析
    • 生成paramMap,将参数名: 参数值存入到paramMap。 或
    • 没有@Param注解且参数名只有1个,那么直接返回第一个参数
  • 判断是否分页, 分页参数处理
  • 执行sqlSession.select/update
    • 根据sqlCommand的name获取mappedStatement
    • query()
      • 参数绑定
      • cacheKey
      • 继续调query(..., boundSql):先尝试从缓存获取,没有缓存、获取不到、不允许使用缓存时,从数据库中查

相关文章

网友评论

      本文标题:mybatis运行原理05-mapperMethod.execu

      本文链接:https://www.haomeiwen.com/subject/amugqltx.html