Deep Understanding of MyBatis (III) - MyBatis Update Operational Execution Process

Deep Understanding of MyBatis (III) - MyBatis Update Operational Execution Process

MyBatis is initialized by parsing the configuration file and generating the Configuration object. After initialization, SqlSession Factory can be obtained for dynamic operation.

The insert operation, delete operation and update operation in MyBatis are essentially called update at the bottom. This paper takes insert operation as an example to analyze the specific execution process of update.

Personal Home Page: tuzhenyu's page
Original address: Deep Understanding of MyBatis (III) - MyBatis Update Operational Execution Process

(1) Session Creation

  • Create session objects by initializing the obtained Session Factory
SqlSession session = sessionFactory.openSession();
  • Call the openSessionFromDataSource() method to execute the creation logic
public SqlSession openSession() {
    return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
}
  • Create an Executor instance in the openSessionFromDataSource() method, and the specific SQL operations are executed through the executor

    • First, we get the Encironment object from Configuration, which contains the configuration of the data source and the corresponding transaction.

    • Create a transaction factory transactionFactory based on the Encironment object, and then create a transaction object

    • The executor executor object is created according to the generated transaction object, which is used to execute specific SQL operations.

    • Encapsulate executor into DefaultSqlSession object and return

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;

    DefaultSqlSession var8;
    try {
        Environment e = this.configuration.getEnvironment();
        TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(e);
        tx = transactionFactory.newTransaction(e.getDataSource(), level, autoCommit);
        Executor executor = this.configuration.newExecutor(tx, execType);
        var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
    } catch (Exception var12) {
        this.closeTransaction(tx);
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);
    } finally {
        ErrorContext.instance().reset();
    }

    return var8;
}
  • An executor instance is created based on the incoming transaction instance, and DefaultSqlSession hands over the main operation to the executor for execution.

    • There are three types of executor s: BatchExecutor, ReuseExecutor and simpleExecutor; BatchExecutor is specifically used for batch processing; ReuseExecutor reuses state to perform sqi operations; SimpleExecutor simply executes sql statement s; the default is simpleExecutor;

    • If the catch cache is turned on, a Caching Executor is created using the decorator pattern, and the Caching Executor queries the cache before querying the database.

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null?this.defaultExecutorType:executorType;
    executorType = executorType == null?ExecutorType.SIMPLE:executorType;
    Object executor;
    if(ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if(ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }

    if(this.cacheEnabled) {
        executor = new CachingExecutor((Executor)executor);
    }

    Executor executor1 = (Executor)this.interceptorChain.pluginAll(executor);
    return executor1;
}

(2) Insert method execution process

1. Create Executor

  • An instance of SqlSession, DefaultSqlSession, is created by openSession() method as the entrance of specific SQL execution. The DefaultSqlSession object encapsulates attributes such as Configuration, Executor, autoCommit, etc.
public int insert(String statement) {
    return this.insert(statement, (Object)null);
}
public int insert(String statement, Object parameter) {
    return this.update(statement, parameter);
}
  • insert operation is related logic by calling update statement, delete operation is also concrete logic by calling update.

  • Get the corresponding MappedStatement object from the configuration according to the state (parsed and loaded into the configuration in initialization)

  • Processing the set in the parameter parameter parameter.

public int update(String statement, Object parameter) {
    int var4;
    try {
        this.dirty = true;
        MappedStatement e = this.configuration.getMappedStatement(statement);
        var4 = this.executor.update(e, this.wrapCollection(parameter));
    } catch (Exception var8) {
        throw ExceptionFactory.wrapException("Error updating database.  Cause: " + var8, var8);
    } finally {
        ErrorContext.instance().reset();
    }

    return var4;
}
  • Executor ultimately delegates the execution of Mapper Statement to Statement Handler; specific execution process:

    • Get a connection database connection;

    • Call the StatementHandler.prepare() method to get a statement

    • Call the StatementHandler.parameterize method to set the parameters required for sql execution

    • Call the StatementHandler.update() method to execute specific sql instructions

public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;

    int var6;
    try {
        Configuration configuration = ms.getConfiguration();
        StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, (ResultHandler)null, (BoundSql)null);
        stmt = this.prepareStatement(handler, ms.getStatementLog());
        var6 = handler.update(stmt);
    } finally {
        this.closeStatement(stmt);
    }

    return var6;
}
  • Call the prepareStatement() method to get the statement, similar to JDBC's conn.getStatement();
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Connection connection = this.getConnection(statementLog);
    Statement stmt = handler.prepare(connection, this.transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
}

2. Getting Connection Database Connections

  • MyBatis maintains a simple database connection pool at the bottom to manage database connections and reuse database connections to avoid wasting resources caused by repeated connection creation.

  • The getConnection() method in SimpleExecutor is finally implemented by calling openConnection(), which calls the getConnection() method in dataSource to obtain the connection.

protected void openConnection() throws SQLException {

    if (log.isDebugEnabled()) {

      log.debug("Opening JDBC Connection");

    }

    connection = dataSource.getConnection();

    if (level != null) {

      connection.setTransactionIsolation(level.getLevel());

    }

    setDesiredAutoCommit(autoCommmit);

}
  • Call the popConnection() method in datasource to get the connection from the database connection pool
return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
  • MyBatis maintains a database connection pool. The main classes of MyBatis are Pool Data Source and Pool Connection under the datasource package, which try to get connections from the database connection pool by calling popConnection().

  • idleConnections are used to store idle database connections in the database connection pool, and c is used to store the database connections that can be used to complete the creation.

  • Calling the popConnection () method will attempt to obtain idle connections from idleConnections first. If idleConnections are empty, it will determine whether the created database connection exceeds the maximum number of connections in the connection pool, if not, create a new connection and put it into activeConnections; if it exceeds, it will take the first decision from the activeConnections list to determine whether or not the timeout occurs, and if it exceeds the maximum number of connections in the connection pool. The specific operation is rolled back, and a new data connection is created and put into active Connections; if it does not time out, the current thread await() waits for wake-up;

private PooledConnection popConnection(String username, String password) throws SQLException {
  boolean countedWait = false;
  PooledConnection conn = null;
  long t = System.currentTimeMillis();
  int localBadConnectionCount = 0;

  while (conn == null) {
    synchronized (state) {
      if (!state.idleConnections.isEmpty()) {
        // Pool has available connection
        conn = state.idleConnections.remove(0);
        if (log.isDebugEnabled()) {
          log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
        }
      } else {
        // Pool does not have available connection
        if (state.activeConnections.size() < poolMaximumActiveConnections) {
          // Can create new connection
          conn = new PooledConnection(dataSource.getConnection(), this);
          if (log.isDebugEnabled()) {
            log.debug("Created connection " + conn.getRealHashCode() + ".");
          }
        } else {
          // Cannot create new connection
          PooledConnection oldestActiveConnection = state.activeConnections.get(0);
          long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
          if (longestCheckoutTime > poolMaximumCheckoutTime) {
            // Can claim overdue connection
            state.claimedOverdueConnectionCount++;
            state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
            state.accumulatedCheckoutTime += longestCheckoutTime;
            state.activeConnections.remove(oldestActiveConnection);
            if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
              try {
                oldestActiveConnection.getRealConnection().rollback();
              } catch (SQLException e) {
                log.debug("Bad connection. Could not roll back");
              }  
            }
            conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
            conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
            conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
            oldestActiveConnection.invalidate();
            if (log.isDebugEnabled()) {
              log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
            }
          } else {
            // Must wait
            try {
              if (!countedWait) {
                state.hadToWaitCount++;
                countedWait = true;
              }
              if (log.isDebugEnabled()) {
                log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
              }
              long wt = System.currentTimeMillis();
              state.wait(poolTimeToWait);
              state.accumulatedWaitTime += System.currentTimeMillis() - wt;
            } catch (InterruptedException e) {
              break;
            }
          }
        }
      }
      if (conn != null) {
        if (conn.isValid()) {
          if (!conn.getRealConnection().getAutoCommit()) {
            conn.getRealConnection().rollback();
          }
          conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
          conn.setCheckoutTimestamp(System.currentTimeMillis());
          conn.setLastUsedTimestamp(System.currentTimeMillis());
          state.activeConnections.add(conn);
          state.requestCount++;
          state.accumulatedRequestTime += System.currentTimeMillis() - t;
        } else {
          if (log.isDebugEnabled()) {
            log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
          }
          state.badConnectionCount++;
          localBadConnectionCount++;
          conn = null;
          if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) {
            if (log.isDebugEnabled()) {
              log.debug("PooledDataSource: Could not get a good connection to the database.");
            }
            throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
          }
        }
      }
    }

  }

  if (conn == null) {
    if (log.isDebugEnabled()) {
      log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    }
    throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
  }

  return conn;
}
  • If the idleConnections list is empty and the length of the active Connections list is less than the maximum number of connections in the connection pool, a new PoolConnection object is created. The specific creation logic of the database connection is in the doGetConnection() method, which extracts the database configuration information from the dataSource to create the corresponding database connection.
private Connection doGetConnection(String username, String password) throws SQLException {
  Properties props = new Properties();
  if (driverProperties != null) {
    props.putAll(driverProperties);
  }
  if (username != null) {
    props.setProperty("user", username);
  }
  if (password != null) {
    props.setProperty("password", password);
  }
  return doGetConnection(props);
}

private Connection doGetConnection(Properties properties) throws SQLException {
  initializeDriver();
  Connection connection = DriverManager.getConnection(url, properties);
  configureConnection(connection);
  return connection;
}

3. Generating proxy objects for Connection

  • Create a new PoolConnection object to generate a connection agent
public PooledConnection(Connection connection, PooledDataSource dataSource) {
  this.hashCode = connection.hashCode();
  this.realConnection = connection;
  this.dataSource = dataSource;
  this.createdTimestamp = System.currentTimeMillis();
  this.lastUsedTimestamp = System.currentTimeMillis();
  this.valid = true;
  this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}
  • To generate JDK dynamic proxy for Connection, PooledConnection itself implements the invocation Handler interface. When invoking the method of Connection, it directly invokes the method of the generated proxy class by invoke().

  • In the proxy database connection class, if the calling method is close(), pushConnection() will be called to proxy the original close() method.

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  String methodName = method.getName();
  if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
    dataSource.pushConnection(this);
    return null;
  } else {
    try {
      if (!Object.class.equals(method.getDeclaringClass())) {
        // issue #579 toString() should never fail
        // throw an SQLException instead of a Runtime
        checkConnection();
      }
      return method.invoke(realConnection, args);
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }
}
  • The pushConnection() method mainly deals with the closure of database connections. When the database connections are exhausted, the connection is removed from the active Connections list and the number of connections in idleConnections is judged to be less than the maximum number of connections, if less than the number of connections placed in idleConnections, and if more than the maximum number of idle connections, the close() side of the original connection is called. Method to make the connection invalid;
protected void pushConnection(PooledConnection conn) throws SQLException {

  synchronized (state) {
    state.activeConnections.remove(conn);
    if (conn.isValid()) {
      if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
        state.accumulatedCheckoutTime += conn.getCheckoutTime();
        if (!conn.getRealConnection().getAutoCommit()) {
          conn.getRealConnection().rollback();
        }
        PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
        state.idleConnections.add(newConn);
        newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
        newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
        conn.invalidate();
        if (log.isDebugEnabled()) {
          log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
        }
        state.notifyAll();
      } else {
        state.accumulatedCheckoutTime += conn.getCheckoutTime();
        if (!conn.getRealConnection().getAutoCommit()) {
          conn.getRealConnection().rollback();
        }
        conn.getRealConnection().close();
        if (log.isDebugEnabled()) {
          log.debug("Closed connection " + conn.getRealHashCode() + ".");
        }
        conn.invalidate();
      }
    } else {
      if (log.isDebugEnabled()) {
        log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
      }
      state.badConnectionCount++;
    }
  }
}

4. Create a statement with the generated Connection object

  • The Statement instance object is created according to the database connection generated above.
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;
  Connection connection = getConnection(statementLog);
  stmt = handler.prepare(connection, transaction.getTimeout());
  handler.parameterize(stmt);
  return stmt;
}
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
  return delegate.prepare(connection, transactionTimeout);
}
  • The prepare() method calls the instantiateStatement() method to get the Statement instance from connection and bind the timeout
@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);
  }
}
  • Get the SQL to be executed from boundSql and call connection.preparement(sql) to generate the Statement object
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
  String sql = boundSql.getSql();
  if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
    String[] keyColumnNames = mappedStatement.getKeyColumns();
    if (keyColumnNames == null) {
      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);
  }
}

5. Setting parameters for the generated Statement instance

  • Parameter settings are required after obtaining state through connection instance
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;
  Connection connection = getConnection(statementLog);
  stmt = handler.prepare(connection, transaction.getTimeout());
  handler.parameterize(stmt);
  return stmt;
}
@Override
public void parameterize(Statement statement) throws SQLException {
  delegate.parameterize(statement);
}
@Override
public void parameterize(Statement statement) throws SQLException {
  parameterHandler.setParameters((PreparedStatement) statement);
}
  • Get the parameter list parameterMapping from boundSql and call the setParameter() method to set the parameters.
@Override
public void setParameters(PreparedStatement ps) {
  ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
  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);
        }
        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) {
          throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
        } catch (SQLException e) {
          throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
        }
      }
    }
  }
}

6. Perform update operations

  • Complete the state acquisition and parameter settings, and then execute the statement.
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {

    Statement stmt = null;

    try {

      Configuration configuration = ms.getConfiguration();

      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);

      stmt = prepareStatement(handler, ms.getStatementLog());

      return handler.update(stmt);

    } finally {

      closeStatement(stmt);

    }

}
@Override
public int update(Statement statement) throws SQLException {
  return delegate.update(statement);
}
  • Call the statement.execute() method to perform database operations, and call the processAfter() method to process the post-processing operations.
@Override
public int update(Statement statement) throws SQLException {
  PreparedStatement ps = (PreparedStatement) statement;
  ps.execute();
  int rows = ps.getUpdateCount();
  Object parameterObject = boundSql.getParameterObject();
  KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
  keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
  return rows;
}

(3) summary

MyBatis specifically executes the process of SQL: 1. Get the corresponding mappedStatement from the configuration according to the state string; 2. Create the corresponding Statement instance according to the obtained mappedStatement; 3. Set the parameters of the state instance according to the incoming parameters; 4. Execute the state and perform the post-operation;

Keywords: Database SQL Mybatis Session

Added by delorian on Sun, 26 May 2019 20:55:36 +0300