Mybatis 执行Sql流程

前言

对于mybatis之前已经讲了mybatis 中接口注入spring源码分析mybatis 接口依赖注入源码分析。现在mybatis的接口能够放入Spring,并且能够依赖注入,这一节讲的重点就是以下代码的具体流程。

    @Autowired
    private CardMapper cardMapper;
    public void select() {
        cardMapper.selectCardById(1);
    }

重点关注类

MapperProxy:
SqlSessionTemplate:
SqlSessionUtils:
SqlSessionFactory:
Configuration:
CachingExecutor:
Executor:
SqlSession:
org.apache.ibatis.transaction.Transaction:
Connection:
RoutingStatementHandler:
StatementHandler:
Statement:
ResultSetHandler

一创建SqlSession

SqlSession用于对外执行sql,并且一般来说SqlSession使用了之后就要关闭掉,防止内存泄漏。既然说的是执行sql流程,那么SqlSession的创建就一定是避不开的。通常我们都会结合Spring一起使用mybatis,并且SqlSession的创建,关闭、提交、回滚都交给Spring管理,对于使用者来说完全无感知。就如下面这段代码,执行Sql的时候,用户完全不需要关心SqlSession的管理。这里SqlSession的创建我们具体指SqlSessionTemplateDefaultSqlSession

 @Autowired
    private CardMapper cardMapper;
    public void select() {
        cardMapper.selectCardById(1);
    }

1.1 创建SqlSessionTemplate

SqlSessionTemplateSpring用来管理SqlSession的生命周期的,并且这个类是全局唯一的,线程安全的,在项目启动的时候就会创建,创建后最后会赋值到MapperProxy中。先简单的看下SqlSessionTemplate构造函数

//SqlSessionTemplate
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    // 生成代理
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
  }
  @Override
  public <T> T selectOne(String statement) {
//    使用代理查询
    return this.sqlSessionProxy.<T> selectOne(statement);
  }

SqlSessionInterceptorSqlSessionTemplate内部类,并且实现了InvocationHandler接口,用于代理SqlSession,所以只要是sqlSessionProxy调用了SqlSession的方法都会调用SqlSessionInterceptor中的invoke方法。具体的invoke方法我会在下面讲解。这里先贴下创建SqlSessionTemplate的代码,该代码存在于mybatis.spring.boot.autoconfigure包下。

//MybatisAutoConfiguration
 @Bean
  @ConditionalOnMissingBean
  public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    ExecutorType executorType = this.properties.getExecutorType();
    if (executorType != null) {
      return new SqlSessionTemplate(sqlSessionFactory, executorType);
    } else {
      return new SqlSessionTemplate(sqlSessionFactory);
    }
  }

1.2 创建DefaultSqlSession

创建DefaultSqlSession的流程图

)
创建DefaultSqlSession的过程首先是调用接口的xxx方法(cardMapper.selectCardById(1)),接着就是MapperProxy对象的invoke方法其实在mybatis 接口依赖注入源码分析就简单的展示了MapperProxy的源码,其实Spring帮我们的接口依赖注入对象就是MapperProxy对象。当我们调用接口中的xxx方法就直接调用代理类MapperProxy中的invoke方法。

@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
   ....
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // sqlSession 就是SqlSessionTemplate,执行sqlSessionTemplate中对应的方法。
    return mapperMethod.execute(sqlSession, args);
  }

SqlSessionTemplate调用select方法的时候,就是sqlSessionProxy调用对应的方法,接着就是调用SqlSessionInterceptorinvoke方法。
在上没有展示SqlSessionInterceptorinvoke方法,这里讲下。以下段代码主要分三部分。

  1. 创建SqlSession
  2. 执行SqlSession对应的方法。
  3. 关闭SqlSession
private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);

      try {
        Object result = method.invoke(sqlSession, args);

        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
      
      } finally {
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

二、获取SQL

mybatis 解析xml流程中介绍了类似<select/>是以何种形式保存在Configuration对象中。以下是关键代码,MappedStatement表示<select/>等节点,ms.getId()是接口中的包名+类名+方法名(test.CardMapper.select)。因此现在SQL是以MappedStatement形式存在ConfigurationMap中,key为包名+类名+方法名,所以只要知道执行方法的key(包名+类名+方法名)就可以获取SQL

#Configuration
 protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("xxxxx");

public void addMappedStatement(MappedStatement ms) {
    mappedStatements.put(ms.getId(), ms);
  }

其实当你调用接口的某个方法的时候就已经知道这个key(包名+类名+方法名)是什么了,以下是mybatis组装key的代码。

 private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
        Class<?> declaringClass, Configuration configuration) {
        //组装id key    
      String statementId = mapperInterface.getName() + "." + methodName;
      if (configuration.hasStatement(statementId)) {
        return configuration.getMappedStatement(statementId);
      } else if (mapperInterface.equals(declaringClass)) {
        return null;
      }
      for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
          MappedStatement ms = resolveMappedStatement(superInterface, methodName,
              declaringClass, configuration);
          if (ms != null) {
            return ms;
          }
        }
      }
      return null;
    }
  }

以下是DefaultSqlSession执行查询代码,具体解释见代码。

@Override
  public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
    
      //从map中获取MappedStatement(<select/>),statement就是包名+类名+方法名
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 执行器进行查询
      executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

在流程图上添加操作。


image.png

三、 走缓存 or 数据库

有了SQL之后,接下来是考虑走缓存还是走数据库。首先mybatis有一级缓存PerpetualCache以及二级缓存。

dd
。这里一级缓存的存活时间与SqlSession的何时关闭有关,而SqlSession的关闭与事务关联。如果外层没有事务,则一次查询后,缓存随着SqlSession的关闭而被清除。如果外层有事务的话,SqlSession查询一次后不会关闭,mybatis会继续将session保留在SessionHolder上,并且存活时间延迟到外层事务完成后关闭。

  SqlSessionUtils
  
  public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
    notNull(session, NO_SQL_SESSION_SPECIFIED);
    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);

    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
    if ((holder != null) && (holder.getSqlSession() == session)) {
        // 持有减一
        holder.released();
    } else {
      //关闭session
      session.close();
    }
  }
SqlSessionUtils#SqlSessionSynchronization
事务完成后关闭
 public void afterCompletion(int status) {
      if (this.holderActive) {
        // afterCompletion may have been called from a different thread
        // so avoid failing if there is nothing in this one

        TransactionSynchronizationManager.unbindResourceIfPossible(sessionFactory);
        this.holderActive = false;
        this.holder.getSqlSession().close();
      }
      this.holder.reset();
    }

一级缓存是内存中的缓存,对象是PerpetualCache,在调用Executor构造函数的时候就会初始化一级缓存。

## BaseExecutor
  protected BaseExecutor(Configuration configuration, Transaction transaction) {
  ...
    //一级缓存
    this.localCache = new PerpetualCache("LocalCache");
  ...
  }
image.png

接下来贴一段代码查询代码。

## BaseExecutor
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, 
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 相当于<select flushCache=true/>
    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();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

继续添加代码流程。<select flushCache=true/>这个清除缓存(一级&二级缓存)

image.png

四 走数据库

走数据库就要涉及到Connection的管理以及参数解析。

4.1 Connection

mybatis中是由org.apache.ibatis.transaction.Transaction管理Connection的生命周期(creation, preparation, commit/rollback and close),如果你整合了Spirng,那么具体的类是org.mybatis.spring.transaction.SpringManagedTransaction,但是SpringManagedTransaction本身不会直接负责Connection的创建和关闭,他向外提供Connection的创建和关闭功能都是由javax.sql.DataSource负责,比如创建连接、维护空闲连接池、活跃连接池等等都是由DateSource管理。由于这个DateSource是一个顶级接口,因此这提供了一个很好的扩展性,你可以选择自己喜欢的DateSource(c3po,druid等),由它来管理你的Connection
Executor主要就是依靠Transaction来进行Connection的管理的,Executor的构造函数中由一个参数就是Transaction,那么这个Transaction是来源哪里?
答案是从Environment中获取,以下是Environmment的三个属性。在org.mybatis.spring.SqlSessionFactoryBean创建Environmment的时候,如果没有TransactionFactory就默认给一个SpringManagedTransactionFactory对象并将他传给Environmment的构造函数

#org.mybatis.spring.SqlSessionFactoryBean
 if (this.transactionFactory == null) {
      this.transactionFactory = new SpringManagedTransactionFactory();
    }

    configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));

SpringManagedTransactionFactory生产的实例对象就是SpringManagedTransaction。因此ExecutorTransaction是来源自Environmment的属性对象TransactionFactory

##Environment
  private final String id;
  // 用于创建Transaction
  private final TransactionFactory transactionFactory;
  private final DataSource dataSource;

4.2 参数解析

mybatis解析参数主要是通过ParameterHandlerParameterHandler是一个接口,只是定义了解析参数的两个方法getParameterObject以及setParametersmybatis默认提供了DefaultParameterHandlerDefaultParameterHandler将入参一一对应到sql上,以下是参数解析的主要的代码。

##DefaultParameterHandler
  // 这里保存了<JavaType, TypeHandler>的对应关系
  private final TypeHandlerRegistry typeHandlerRegistry;
  // <select/>
  private final MappedStatement mappedStatement;
  // xxxMapper.select()的入参对象
  private final Object parameterObject;
  //
  private final BoundSql boundSql;
  private final Configuration configuration;
 @Override
  public void setParameters(PreparedStatement ps) {
    // 入参都会按照sql的参数顺序一一的放入parameterMappings 里面。
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
           // 获取参数的类型处理器,比如string 对应StringTypeHandler
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            // 设置参数
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException e) {

          } catch (SQLException e) {

          }
        }
      }
    }
  }

五、执行SQL

执行SQL是通过StatementHandler执行,当然这是个接口,具体的操作由子类去实现。

image.png

RoutingStatementHandler: 仅仅是选择让哪个StatementHandler去执行SQL,下面是代码是RoutingStatementHandler的构造函数,里面根据StatementType选择不同的StatementHandler,而StatementType在写<select statementType="PREPARED"/>类似的语句的时候就已经决定了是什么类型的,默认是PREPARED

#RoutingStatementHandler
  private final StatementHandler delegate;
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, 
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }
  }

总结

两条路:
SqlSession->获取SQL->走缓存
SqlSession->获取SQL->走数据库->管理连接->参数解析->执行SQL

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,874评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,102评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,676评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,911评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,937评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,935评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,860评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,660评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,113评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,363评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,506评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,238评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,861评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,486评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,674评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,513评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,426评论 2 352

推荐阅读更多精彩内容