前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MyBatis的SQL执行过程

MyBatis的SQL执行过程

作者头像
码农飞哥
发布2021-08-18 10:22:54
5270
发布2021-08-18 10:22:54
举报
文章被收录于专栏:好好学习

前言

今天的分析的核心是SQL的执行过程。主要分为如下章节进行分析

  1. 代理类的生成
  2. SQL的执行过程
  3. 处理查询结果

mapper 接口的代理类的生成过程分析

首先我们来看看mapper 接口的代理类的生成过程,如下是一个MyBatis查询的调用实例。

代码语言:javascript
复制
 StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
 List<Student> studentList = mapper.selectByName("点点");

上述方法sqlSession.getMapper(StudentMapper.class) 返回的其实是StudentMapper的代理类。 接着我们来看看调用的时序图。

如上时序图我们可知,接口的代理类(MapperProxy)最终由MapperProxyFactory通过JDK动态代理生成。接着我们一步步分析下。

代码语言:javascript
复制
//DefaultSqlSession
  @Override
  public <T> T getMapper(Class<T> type) {
    //最后会去调用MapperRegistry.getMapper
    return configuration.<T>getMapper(type, this);
  }

如上,DefaultSqlSession直接请求抛给Configuration。

代码语言:javascript
复制
//Configuration
 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

同样,Configuration也是一个甩手掌柜,将请求直接抛给了MapperRegistry 这个接盘侠。 接下来我们来看看接盘侠MapperRegistry。

代码语言:javascript
复制
//*MapperRegistry
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

如上,在MapperRegistry的getMapper的方法中,首先根据配置的Mapper 获取其对应的MapperProxyFactory。接着调用newInstance方法返回MapperProxy。最后我们来看看MapperProxyFactory

代码语言:javascript
复制
//*MapperProxyFactory
  protected T newInstance(MapperProxy<T> mapperProxy) {
    //用JDK自带的动态代理生成映射器
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

如上,通过JDK自带的动态代理生成映射器,PS: JDK 动态代理需要接口。 分析完了MapperProxy的生成过程,接下来我们来分析下SQL的执行过程。

SQL的执行过程

SQL 的执行过程是从MapperProxy的invoke方法开始。按照惯例我们还是先看看相关的时序图。

如上图,在MapperProxy的invoke方法里调用了MapperMethod的execute方法,该方法是真正执行SQL,返回结果的方法。接下来我们来看看。

代码语言:javascript
复制
//*MapperProxy
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //代理以后,所有Mapper的方法调用时,都会调用这个invoke方法
    //并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行
    if (Object.class.equals(method.getDeclaringClass())) {
      try {
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
    //这里优化了,去缓存中找MapperMethod
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //执行
    return mapperMethod.execute(sqlSession, args);
  }

如上,这里MyBatis做了个优化,如果缓存中有MapperMethod,则取缓存中的,如果没有则new一个MapperMethod实例。

代码语言:javascript
复制
//*MapperProxy
  //去缓存中找MapperMethod
  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      //找不到才去new
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

我们接着来看看MapperMethod 中的execute方法,该方法主要是通过区分各种CURD操作(insert|update|delete|select),分别调用sqlSession中的4大类方法。源码如下:

代码语言:javascript
复制
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    //可以看到执行时就是4种情况,insert|update|delete|select,分别调用SqlSession的4大类方法
    if (SqlCommandType.INSERT == command.getType()) {
//      对用户传入的参数进行转换,下同
      Object param = method.convertArgsToSqlCommandParam(args);
// 执行插入操作,rowCountResult方法用于处理返回值
      result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
    }
//    根据目标返回方法的返回类型进行相应的查询操作。
    else if (SqlCommandType.SELECT == command.getType()) {
      if (method.returnsVoid() && method.hasResultHandler()) {
        /*
          如果方法返回值为void,但参数列表中包含ResultHandler,
          想通过ResultHandler的方式获取查询结果,而非通过返回值获取结果
        * */
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        //如果结果有多条记录
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        //如果结果是map
        result = executeForMap(sqlSession, args);
      } else {
        //否则就是一条记录
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
    } else {
      throw new BindingException("Unknown execution method for: " + command.getName());
    }
//    如果方法的返回值是基本类型,而返回值却为null,此种情况下应抛出异常
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

如上,代码注释比较详细。前面也说过了,不同的操作调用sqlSession中不同的方法。这里我重点分析下查询操作。查询的情况分为四种:

  1. 返回值为空
  2. 返回多条记录
  3. 返回map
  4. 返回单条记录。 返回值为空的情况下,直接返回 result 为null。其余几种情况内部都调用了sqlSession 中的selectList 方法。下面我就以返回单条记录为例进行分析。
代码语言:javascript
复制
//DefaultSqlSession
  public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    //转而去调用selectList,很简单的,如果得到0条则返回null,得到1条则返回1条,得到多条报TooManyResultsException错
    // 特别需要主要的是当没有查询到结果的时候就会返回null。因此一般建议在mapper中编写resultType的时候使用包装类型
    //而不是基本类型,比如推荐使用Integer而不是int。这样就可以避免NPE
    List<T> list = this.<T>selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null;
    }
  }

如果所示,如果selectList查询返回1条,则直接返回,如果返回多条则抛出异常,否则直接返回null。我们接着往下看.

代码语言:javascript
复制
//DefaultSqlSession
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      //根据statement id找到对应的MappedStatement
      MappedStatement ms = configuration.getMappedStatement(statement);
      //转而用执行器来查询结果,注意这里传入的ResultHandler是null
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

如上,在selectList 内部最终调用的是SimpleExecutor (执行器)的query方法来执行查询结果。我们接着往下找

enter description here 根据类图我们不难发现SimpleExecutor是BaseExecutor类的子类。在BaseExecutor 类中我们找到了query 方法。

代码语言:javascript
复制
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //得到绑定sql
    BoundSql boundSql = ms.getBoundSql(parameter);
    //创建缓存Key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    //查询
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

该方法主要有两步,

  1. 得到绑定的SQL,
  2. 调用其重载query方法。 绑定SQL的过程,我们稍后分析。我们接着来看看其重载的query方法。
代码语言:javascript
复制
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
   ····· 省略部分代码
    try {
      //加一,这样递归调用到上面的时候就不会再清局部缓存了
      queryStack++;
      //先根据cachekey从localCache去查
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        //若查到localCache缓存,处理localOutputParameterCache
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        //从数据库查
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    ···· 省略部分代码
    }
    return list;
  }

该方法核心的步骤是,首先根据cacheKey 从localCache 中去查,如果不为空的话则直接取缓存的,否则查询数据库。我们主要看看查询数据库的queryFromDatabase方法。

代码语言:javascript
复制
//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);
    //如果是存储过程,OUT参数也加入缓存
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }
    //query-->queryFromDatabase-->doQuery
  protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException;

此处doQuery方法是抽象方法,定义了模板供子类实现。此处用到了模板模式。 首先,此方法首先调用doQuery方法执行查询,然后将查询的结果放入缓存中。 接着我们再来看看SimpleExcutor中的doQuery方法。

代码语言:javascript
复制
//*SimpleExcutor
  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
      //这里看到ResultHandler传入了
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      //准备语句
      stmt = prepareStatement(handler, ms.getStatementLog());
      //StatementHandler.query(实际调用的是PreparedStatementHandler)
      return handler.<E>query(stmt, resultHandler);
    } finally {
//      关闭statement
      closeStatement(stmt);
    }
  }

如上,该方法主要有三步:

  1. 新建一个StatementHandler
  2. 获取Statement
  3. StatementHandler.query(实际调用的是PreparedStatementHandler)获取查询结果。 第一步比较简单,我们首先来看看第二步
代码语言:javascript
复制
//*SimpleExcutor
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
//   获取数据库连接
    Connection connection = getConnection(statementLog);
    //创建Statement
    stmt = handler.prepare(connection);
    //为Statement设置IN参数
    handler.parameterize(stmt);
    return stmt;
  }

对于prepareStatement方法里的相关步骤,相信大家都不会陌生。获取数据库连接,创建Statement; 为Statement设置IN参数。都是我们非常熟悉的。我们接着看看第三步。

代码语言:javascript
复制
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
//    执行SQL
    ps.execute();
//    处理执行结果
    return resultSetHandler.<E> handleResultSets(ps);
  }

这一步到了最终的执行链。还是先执行SQL,然后处理执行结果。限于篇幅,在此不展开分析了。

总结

本文通过两个时序图,为主线来展开分析了Mapper接口代理类的生成过程,以及SQL的执行过程。希望对大家有所帮助。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-05-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农飞哥 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • mapper 接口的代理类的生成过程分析
  • SQL的执行过程
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档