MyBatis 源码阅读笔记:SqlSession 执行 Mapper 过程

in TCEH Java

前言

Mybatis3 中 SqlSession 接口有三个实现类:DefaultSqlSession、SqlSessionManager、SqlSessionTemplate 。后两个实现最终委托给 DefaultSqlSession 执行相应的操作。沿用《MyBatis 源码阅读笔记:Mapper 接口的注册过程》中的示例,续上一篇文章继续阅读 SqlSession 执行 Mapper 过程。

insert() 方法

DefaultSqlSession#insert

  @Override
  public int insert(String statement, Object parameter) {
    return update(statement, parameter);
  }

update() 方法

DefaultSqlSession#update

  @Override
  public int update(String statement, Object parameter) {
    try {
      dirty = true;
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 由 Executor 执行。默认 CachingExecutor ,这里委托给 simpleExecutor 执行 
      return executor.update(ms, wrapCollection(parameter));
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

doUpdate() 方法

SimpleExecutor#doUpdate

  @Override
  public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      // 创建声明处理器。这里会将 SQL 中${}占位符替换为实际的参数
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
      // 获取预编译对象
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 组装发送 SQL 
      return handler.update(stmt);
    } finally {
      closeStatement(stmt);
    }
  }

prepareStatement() 方法

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    // 获取连接对象。包含连接数据库、设置自动提交等操作
    Connection connection = getConnection(statementLog);
    // 预编译处理
    stmt = handler.prepare(connection, transaction.getTimeout());
    // 由 JDBC 按参数类型及数据库要求做相应的参数转义处理
    // 非核心代码过多,就不贴啦!假设是客户端预编译,参数类型为 String 
    // 最终会跳转到 com.mysql.cj.ClientPreparedQueryBindings#setString 方法
    handler.parameterize(stmt);
    return stmt;
  }

prepare

BaseStatementHandler#prepare

  @Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
      // 实例化声明
      statement = instantiateStatement(connection);
      setStatementTimeout(statement, transactionTimeout);
      setFetchSize(statement);
      return statement;
    } catch (SQLException e) {
      closeStatement(statement);
      throw e;
    } catch (Exception e) {
      closeStatement(statement);
      throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
  }

instantiateStatement()

  @Override
  protected Statement instantiateStatement(Connection connection) throws SQLException {
    // 获取 SQL 语句
    String sql = boundSql.getSql();
    // NoKeyGenerator:默认空实现,不对主键单独处理;
    // Jdbc3KeyGenerator:主要用于数据库的自增主键,比如 MySQL、PostgreSQL 等,在 Insert 响应结果中解析出 Last_INSERT_ID ;
    // SelectKeyGenerator:主要用于数据库不支持自增主键的情况,比如 Oracle、DB2,在执行前填充主键。
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
        // 调用 JDBC 驱动,进行预编译处理。默认 ClientPreparedStatement 客户端预编译
        // 可通过参数 useServerPrepStmts=true 使用 ServerPreparedStatement 服务端预编译
        // 可通过参数 cachePrepStmts=true 缓存预编译数据,默认 false
        // 同时将 sql 按占位符 ? 进行分割
        return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
      } else {
        return connection.prepareStatement(sql, keyColumnNames);
      }
    } else if (mappedStatement.getResultSetType() != null) {
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    } else {
      return connection.prepareStatement(sql);
    }
  }

setString() 方法

com.mysql.cj.ClientPreparedQueryBindings#setString

    @Override
    public void setString(int parameterIndex, String x) {
        if (x == null) {
            setNull(parameterIndex);
        } else {
            int stringLength = x.length();
            // 不需要反斜杠转义? MySql 需要转义,不然会自动删除反斜杠
            if (this.session.getServerSession().isNoBackslashEscapesSet()) {
                // Scan for any nasty chars
                // isEscapeNeededForString 包含 “\n”、“\r”、“\\”、“\'”、‘ ” ’、“\032” 的 char 需要转义
                boolean needsHexEscape = isEscapeNeededForString(x, stringLength);
                // 参数不需要转义
                if (!needsHexEscape) {
                    StringBuilder quotedString = new StringBuilder(x.length() + 2);
                    quotedString.append('\'');
                    quotedString.append(x);
                    quotedString.append('\'');
                    // 生成 byte 数组
                    byte[] parameterAsBytes = this.isLoadDataQuery ? StringUtils.getBytes(quotedString.toString())
                            : StringUtils.getBytes(quotedString.toString(), this.charEncoding);
                    // 保存 byte 数组
                    setValue(parameterIndex, parameterAsBytes, MysqlType.VARCHAR);

                } else {
                    byte[] parameterAsBytes = this.isLoadDataQuery ? StringUtils.getBytes(x) : StringUtils.getBytes(x, this.charEncoding);
                    setBytes(parameterIndex, parameterAsBytes);
                }

                return;
            }

            String parameterAsString = x;
            boolean needsQuoted = true;
            // isEscapeNeededForString 包含 “\n”、“\r”、“\\”、“\'”、‘ ” ’、“\032” 的 char 需要转义
            if (this.isLoadDataQuery || isEscapeNeededForString(x, stringLength)) {
                needsQuoted = false; // saves an allocation later

                StringBuilder buf = new StringBuilder((int) (x.length() * 1.1));

                buf.append('\'');

                //
                // Note: buf.append(char) is _faster_ than appending in blocks, because the block append requires a System.arraycopy().... go figure...
                //
                // 对特殊字符进行转义
                for (int i = 0; i < stringLength; ++i) {
                    char c = x.charAt(i);

                    switch (c) {
                        case 0: /* Must be escaped for 'mysql' */
                            buf.append('\\');
                            buf.append('0');
                            break;
                        case '\n': /* Must be escaped for logs */
                            buf.append('\\');
                            buf.append('n');
                            break;
                        case '\r':
                            buf.append('\\');
                            buf.append('r');
                            break;
                        case '\\':
                            buf.append('\\');
                            buf.append('\\');
                            break;
                        case '\'':
                            buf.append('\'');
                            buf.append('\'');
                            break;
                        case '"': /* Better safe than sorry */
                            if (this.session.getServerSession().useAnsiQuotedIdentifiers()) {
                                buf.append('\\');
                            }
                            buf.append('"');
                            break;
                        case '\032': /* This gives problems on Win32 */
                            buf.append('\\');
                            buf.append('Z');
                            break;
                        case '\u00a5':
                        case '\u20a9':
                            // escape characters interpreted as backslash by mysql
                            if (this.charsetEncoder != null) {
                                CharBuffer cbuf = CharBuffer.allocate(1);
                                ByteBuffer bbuf = ByteBuffer.allocate(1);
                                cbuf.put(c);
                                cbuf.position(0);
                                this.charsetEncoder.encode(cbuf, bbuf, true);
                                if (bbuf.get(0) == '\\') {
                                    buf.append('\\');
                                }
                            }
                            buf.append(c);
                            break;

                        default:
                            buf.append(c);
                    }
                }

                buf.append('\'');

                parameterAsString = buf.toString();
            }
            byte[] parameterAsBytes = this.isLoadDataQuery ? StringUtils.getBytes(parameterAsString)
                    : (needsQuoted ? StringUtils.getBytesWrapped(parameterAsString, '\'', '\'', this.charEncoding)
                            : StringUtils.getBytes(parameterAsString, this.charEncoding));

            setValue(parameterIndex, parameterAsBytes, MysqlType.VARCHAR);
        }
    }

update()

PreparedStatementHandler#update

  @Override
  public int update(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    // 组装发送 SQL 语句。最终跳转到 com.mysql.cj.jdbc.ClientPreparedStatement#execute()
    ps.execute();
    // 获取影响的行数
    int rows = ps.getUpdateCount();
    // 自增长 id 处理
    Object parameterObject = boundSql.getParameterObject();
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    return rows;

execute()

com.mysql.cj.jdbc.ClientPreparedStatement#execute()

    @Override
    public boolean execute() throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            // 预编译的时候已经连接过了
            JdbcConnection locallyScopedConn = this.connection;

            if (!this.doPingInstead && !checkReadOnlySafeStatement()) {
                throw SQLError.createSQLException(Messages.getString("PreparedStatement.20") + Messages.getString("PreparedStatement.21"),
                        MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, this.exceptionInterceptor);
            }

            ResultSetInternalMethods rs = null;

            this.lastQueryIsOnDupKeyUpdate = false;

            if (this.retrieveGeneratedKeys) {
                this.lastQueryIsOnDupKeyUpdate = containsOnDuplicateKeyUpdateInSQL();
            }

            this.batchedGeneratedKeys = null;

            resetCancelledState();

            implicitlyCloseAllOpenResults();

            clearWarnings();

            if (this.doPingInstead) {
                doPingInstead();

                return true;
            }

            setupStreamingTimeout(locallyScopedConn);
            // 填充参数,组装数据包
            Message sendPacket = ((PreparedQuery<?>) this.query).fillSendPacket();

            String oldDb = null;

            if (!locallyScopedConn.getDatabase().equals(this.getCurrentDatabase())) {
                oldDb = locallyScopedConn.getDatabase();
                locallyScopedConn.setDatabase(this.getCurrentDatabase());
            }

            //
            // Check if we have cached metadata for this query...
            //
            CachedResultSetMetaData cachedMetadata = null;
            // 是否缓存查询结果
            boolean cacheResultSetMetadata = locallyScopedConn.getPropertySet().getBooleanProperty(PropertyKey.cacheResultSetMetadata).getValue();
            if (cacheResultSetMetadata) {
                cachedMetadata = locallyScopedConn.getCachedMetaData(((PreparedQuery<?>) this.query).getOriginalSql());
            }

            //
            // Only apply max_rows to selects
            //
            locallyScopedConn.setSessionMaxRows(((PreparedQuery<?>) this.query).getParseInfo().getFirstStmtChar() == 'S' ? this.maxRows : -1);
            // 获取返回结果
            rs = executeInternal(this.maxRows, sendPacket, createStreamingResultSet(),
                    (((PreparedQuery<?>) this.query).getParseInfo().getFirstStmtChar() == 'S'), cachedMetadata, false);
            // 数据缓存处理
            if (cachedMetadata != null) {
                locallyScopedConn.initializeResultsMetadataFromCache(((PreparedQuery<?>) this.query).getOriginalSql(), cachedMetadata, rs);
            } else {
                if (rs.hasRows() && cacheResultSetMetadata) {
                    locallyScopedConn.initializeResultsMetadataFromCache(((PreparedQuery<?>) this.query).getOriginalSql(), null /* will be created */, rs);
                }
            }
            // 是否需要自增长主键
            if (this.retrieveGeneratedKeys) {
                // 取 sql 类型的第一个字符。如 Insert 取 I,Update 取 U
                rs.setFirstCharOfQuery(((PreparedQuery<?>) this.query).getParseInfo().getFirstStmtChar());
            }

            if (oldDb != null) {
                locallyScopedConn.setDatabase(oldDb);
            }

            if (rs != null) {
                // 获取自增长主键 id。批处理返回第一条数据的 id
                this.lastInsertId = rs.getUpdateID();

                this.results = rs;
            }

            return ((rs != null) && rs.hasRows());
        }
    }

到这里,SqlSession 执行 Insert 操作差不多就结束啦!
接下来阅读 DefaultSqlSession#selectList 方法,主要是为了了解MyBatis 的缓存机制。

selectList

DefaultSqlSession#selectList

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 由执行器执行查询
      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();
    }
  }

query()

CachingExecutor#query

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 创建缓存 Key
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

query()

CachingExecutor#query

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    // 二级缓存处理。默认不开启,需要在配置文件中设置 cacheEnabled=true 
    // 或在 Mapper 接口上使用注解 @CacheNamespace(blocking = true)
    Cache cache = ms.getCache();
    if (cache != null) {
      // flushCacheRequired 在 select 中默认为 false
      flushCacheIfRequired(ms);
      // useCache 默认为 true
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        // 从缓存中获取数据
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          // 缓存中没有的话,从数据库查
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // 将查询到的结果放到缓存里
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

query()

BaseExecutor#query

  @Override
  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.");
    }
    // flushCacheRequired 在 select 中默认为 false 。也就是默认会进行一级缓存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      // localCache 一级缓存。当 SqlSession 结束后,缓存会被清空
      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;
  }

queryFromDatabase() 方法

  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 {
      // 执行 SQL 查询
      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;
  }

doQuery() 方法

  @Override
  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();
      // 创建声明处理器。这里会将 SQL 中${}占位符替换为实际的参数
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 获取预编译对象
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 组装发送 SQL 
      // 如 Insert 一样最终会执行到 com.mysql.cj.jdbc.ClientPreparedStatement#execute()
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

总结