查询语句的执⾏过程
查询语句对应的方法比较多,有如下几种:
- executeWithResultHandler
- executeForMany
- executeForMap
- executeForCursor
这些方法在内部调用了 SqlSession 中的一些 select方法,比如 selectList、selectMap、 selectCursor 等。这些方法的返回值类型是不同的,因此对于每种返回类型,需要有专门的处 理方法。以 selectList 方法为例,该方法的返回值类型为 List。但如果我们的 Mapper 或 Dao 的接口方法返回值类型为数组,或者 Set,直接将 List 类型的结果返回给 Mapper/Dao 就不合 适了。execute等方法只是对 select等方法做了一层简单的封装,因此接下来我们应们应该 把目光放在这些 select方法上。
selectOne ⽅法分析
本节选择分析 selectOne 方法,而不是其他的方法,大家或许会觉得奇怪。前面提及了 selectList、selectMap、selectCursor 等方法,这里却分析一个未提及的方法。这样做并没什么 特别之处,主要原因是 selectOne 在内部会调用 selectList 方法。这里分析 selectOne 方法是 为了告知大家,selectOne 和 selectList 方法是有联系的,同时分析 selectOne 方法等同于分析 selectList 方法。如果你不信的话,那我们看源码吧,源码面前了无秘密。
// -☆- DefaultSqlSession public <T> T selectOne(String statement, Object parameter) { // 调用 selectList 获取结果 List<T> list = this.<T>selectList(statement, parameter); if (list.size() == 1) { // 返回结果 return list.get(0); } else if (list.size() > 1) { // 如果查询结果大于 1 则抛出异常,这个异常也是很常见的 throw new TooManyResultsException("……"); } else { return null; } } 复制代码
这个异常我不信大家没有碰到过,哈哈,只有读过源码才知道怎么去解决这些异常,怎么说呢 以前小六六碰到一个api不会用 我第一时间就是百度,碰到异常 也是,但是现在 对于我们经常用的框架 我会点进去看源码了,感觉慢慢的有点进步了,当然不熟悉的框框我还是百度,哈哈。
如上,selectOne 方法在内部调用 selectList 了方法,并取 selectList 返回值的第 1 个元素 作为自己的返回值。如果 selectList 返回的列表元素大于 1,则抛出异常。上面代码比较易懂, 就不多说了。下面我们来看看 selectList 方法的实现。
// -☆- DefaultSqlSession public <E> List<E> selectList(String statement, Object parameter) { // 调用重载方法 return this.selectList(statement, parameter, RowBounds.DEFAULT); } 复制代码
private final Executor executor; public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { // 获取 MappedStatement MappedStatement ms = configuration.getMappedStatement(statement); // 调用 Executor 实现类中的 query 方法 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("……"); } finally { ErrorContext.instance().reset(); } } 复制代码
如上,这里要来说说 executor 变量,该变量类型为 Executor。Executor 是一个接口,它 的实现类如下:
Executor 有这么多的实现类,大家猜一下 executor 变量对应哪个实现类。要弄清楚这个 问题,需要大家到源头去查证。这里提示一下,大家可以跟踪一下 DefaultSqlSessionFactory 的 openSession 方法,很快就能发现 executor 变量创建的踪迹。限于篇幅原因,本文就不分 析 openSession 方法的源码了。默认情况下,executor 的类型为 CachingExecutor,该类是一 个装饰器类,用于给目标 Executor 增加二级缓存功能。那目标 Executor 是谁呢?默认情况 下是 SimpleExecutor
现在大家搞清楚 executor 变量的身份了,接下来继续分析 selectOne 方法的调用栈。先 来看看 CachingExecutor 的 query 方法是怎样实现的。如下:
// -☆- CachingExecutor public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // 获取 BoundSql BoundSql boundSql = ms.getBoundSql(parameterObject); // 创建 CacheKey CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); // 调用重载方法 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } 复制代码
上面的代码用于获取 BoundSql 对象,创建 CacheKey 对象,然后再将这两个对象传给重 载方法。BoundSql 的获取过程较为复杂,我将在下一节进行分析。CacheKey 以及接下来即 将出现的一二级缓存将会独立成章分析。 上面的方法等代码和 SimpleExecutor 父类 BaseExecutor 中的实现没什么区别,有区别的 地方在于这个方法所调用的重载方法。继续往下看
// -☆- CachingExecutor public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // 从 MappedStatement 中获取缓存 Cache cache = ms.getCache(); // 若映射文件中未配置缓存或参照缓存,此时 cache = null if (cache != null) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { // 若缓存未命中,则调用被装饰类的 query 方法 list = delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } // 调用被装饰类的 query 方法 return delegate.<E>query( ms, parameterObject, rowBounds, resultHandler, key, boundSql); } 复制代码
以上代码涉及到了二级缓存,若二级缓存为空,或未命中,则调用被装饰类的 query 方 法。下面来看一下 BaseExecutor 的中签名相同的 query 方法是如何实现的。
// -☆- BaseExecutor public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { if (closed) { throw new ExecutorException("Executor was closed."); } if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List<E> list; try { queryStack++; // 从一级缓存中获取缓存项 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { // 存储过程相关处理逻辑,本文不分析存储过程,故该方法不分析了 handleLocallyCachedOutputParameters(ms,key,parameter,boundSql); } else { // 一级缓存未命中,则从数据库中查询 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { // 从一级缓存中延迟加载嵌套查询结果 for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } deferredLoads.clear(); if (configuration.getLocalCacheScope()==LocalCacheScope.STATEMENT) { clearLocalCache(); } } return list; } 复制代码
上面的方法主要用于从一级缓存中查找查询结果,若缓存未命中,再向数据库进行查询。 在上面的代码中,出现了一个新的类 DeferredLoad,这个类用于延迟加载。该类的实现并不 复杂,但是具体用途让我有点疑惑。这个我目前也未完全搞清楚,就不分析了。接下来,我 们来看一下 queryFromDatabase 方法的实现
// -☆- BaseExecutor private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; // 向缓存中存储一个占位符 localCache.putObject(key, EXECUTION_PLACEHOLDER); try { // 调用 doQuery 进行查询 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { // 移除占位符 localCache.removeObject(key); } // 缓存查询结果 localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; } 复制代码
上面的代码仍然不是 selectOne 方法调用栈的终点,抛开缓存操作,queryFromDatabase 最终还会调用 doQuery 进行查询。所以下面我们继续进行跟踪。
// -☆- SimpleExecutor public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); // 创建 StatementHandler StatementHandler handler = configuration.newStatementHandler( wrapper, ms, parameter, rowBounds, resultHandler, boundSql); // 创建 Statement stmt = prepareStatement(handler, ms.getStatementLog()); // 执行查询操作 return handler.<E>query(stmt, resultHandler); } finally { // 关闭 Statement closeStatement(stmt); } } 复制代码
doQuery 方法中仍然有不少的逻辑,完全看不到即将要到达终点的趋势,不过这离终点 又近了一步。接下来,我们先跳过 StatementHandler 和 Statement 创建过程,这两个对象的创 建过程会在后面进行说明。这里,我们以 PreparedStatementHandler 为例,看看它的 query 方 法是怎样实现的。如下:
// -☆- PreparedStatementHandler public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; // 执行 SQL ps.execute(); // 处理执行结果 return resultSetHandler.<E>handleResultSets(ps); } 复制代码
到这里似乎看到了希望,整个调用过程总算要结束了。不过先别高兴的太早,SQL 执行 结果的处理过程也很复杂,稍后将会专门拿出一节内容进行分析。 以上就是 selectOne 方法的执行过程,尽管我已经简化了代码分析,但是整个过程看起来还是很复杂的。查询过程涉及到了很多方法调用,不把这些调用方法搞清楚,很难对 MyBatis 的查询过程有深入的理解。
结尾
其实我也只是跟着大佬的书在读,很多东西也是一知半解,哈哈 但是今天我们主要讲的是一个selectOne的执行过程,这边我们来总结一下,首先就是DefaultSqlSession调用selectOne,然后其实他这个方法调用的是selectList 然后通过list.size() > 1来看是否需要抛出异常,然后selectList里面其实是调用了executor.query方法,然后就得说一下Executor是怎么来的了,他是在调用openSession的时候通过config.newExecutor 生成的 默认情况下,executor 的类型为 CachingExecutor,那么接下来就是来看CachingExecutor.query 里面有几个步骤这边就不一一分析了(涉及一二级缓存),然后他接下来调用的是 query 然后就是调用BaseExecutor queryFromDatabase 这是去调用数据库的方法 然后是doQuery 往下就是类似于 JDBC的操作了以上就是一个查询的整个过程。