How a SQL is executed in MyBatis

Foreword

MyBatis’s core interface for executing SQL is the SqlSession interface, which provides some CURD and transaction control methods. In addition, you can first obtain an instance of the Mapper interface through SqlSession, and then execute SQL through the Mapper interface. The execution of the Mapper interface methods is ultimately delegated to Methods in SqlSession. Therefore, you can start analyzing the SQL execution process from SqlSession. Since this article contains a lot of content, interested friends can save it first and read it patiently when they have free time, or go directly to the end to view the summary.

SQL execution process analysis

SQL in MyBatis is executed by SqlSession. Since most of the SQL types used in daily work are queries, and the queries in MyBatis are also the most complex, this article uses SqlSession#selectList(String, Object, RowBounds) as the entry point for analysis. , interspersed with other API introductions of SqlSession, and important components will be listed separately in subsequent chapters.

public interface SqlSession extends Closeable {
    <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds);
}

The method definition of #selectList is as shown above. This method queries the database and then converts the results into the type required by the user. The specific meaning of each parameter is as follows.

statement: Represents the identifier of the SQL statement. When calling methods in the Mapper interface, the fully qualified name of the interface will be used. The method name corresponds to the mapper node namespace.select node id in the Mapper xml configuration.

parameter: The parameter in the MyBatis SQL statement can be a native type or a wrapper class of the native type. It can be a Map or other Object. If the Mapper interface method contains multiple parameters, it will be converted to a Map. MyBatis takes Map or Object. Replace ${paramName} in the Mapper xml file with the field value in or set the SQL parameter specified by #{paramName} to the corresponding field value.

rowBounds: paging information, including offset and limit. MyBatis paging the returned results in memory.

After understanding the function of the method, let’s look at the implementation of the method. The default implementation of SqlSession in MyBatis is DefaultSqlSession, track the source code.

public class DefaultSqlSession implements SqlSession {
    // Mybatis configuration
    private final Configuration configuration;
    // Actuator
    private final Executor executor;

    @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();
        }
    }
}

When SqlSession performs database query, it first obtains the MappedStatement representing the SQL statement from the configuration, and then uses the executor Executor to execute it. The called Executor#query method is defined as follows.

public interface Executor {
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
}

Executor has a new parameter ResultHandler when executing a query, which is used to process the Java object corresponding to each row of database records. For example, the results can be saved in a List or Map. Executor has multiple implementations as an interface. Compared with other Executors, CachingExecutor only supports statement-level caching, so we track the implementation of the BaseExecutor#query method.

public abstract class BaseExecutor implements Executor {
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
        return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
}

BaseExecutor first uses MappedStatement to obtain an instance of BoundSql, then creates a CacheKey representing the current query, and finally calls another #query method.

BoundSql: MappedStatement contains complete metadata required by MyBatis to execute SQL, such as result mapping, parameter mapping, dynamic SQL, etc. MappedStatement parses dynamic SQL and generates BoundSql. BoundSql only contains the final executed SQL and parameter information.

CacheKey: MyBatis can cache the query results of each SQL. CacheKey is used to represent the cached key value. It consists of SQL, parameters, paging, etc. When the same conditions are used to query the database next time, the query can be obtained from the cache first. result.

After understanding these parameters, look at the #query method called.

public abstract class BaseExecutor implements Executor {
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ... Omit the verification and caching processing code
        List<E> list;
        try {
            queryStack + + ;
            // Get it from cache first
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
            if (list != null) {
                // Handle stored procedure OUT parameters
                handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else {
                // There is no data in the cache, query from the database
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            queryStack--;
        }
        ... Omit cache processing related code
        return list;
    }
}

In order to focus on the main process, the above code omits some cache processing methods. The cache processing will be analyzed separately later. The #query method first obtains the query result from the cache. If it is not obtained, it will query from the database, and then look at the code of the database query.

public abstract class BaseExecutor implements Executor {

    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 {
            //Execute database query
            list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            localCache.removeObject(key);
        }
        //Cache query results
        localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
            localOutputParameterCache.putObject(key, parameter);
        }
        return list;
    }
    
    protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
        throws SQLException;
}

#queryFromDatabase calls the #doQuery method to query the database, and then caches the query results. #doQuery is an abstract method, let’s look at its implementation in the SimpleExecutor used by default.

public class SimpleExecutor extends BaseExecutor {
    @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();
            StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            //Create Statement and set SQL parameters
            stmt = prepareStatement(handler, ms.getStatementLog());
            //Use StatementHandler to execute the query
            return handler.query(stmt, resultHandler);
        } finally {
            closeStatement(stmt);
        }
    }
    // Prepare Statement
    private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        Statement stmt;
        Connection connection = getConnection(statementLog);
        //Create Statement
        stmt = handler.prepare(connection, transaction.getTimeout());
        //Set SQL parameters
        handler.parameterize(stmt);
        return stmt;
    }
}

At this point, we finally see the familiar JDBC API. The #doQuery method first uses the configuration to create a StatementHandler. After using the StatementHandler to create the Statement and setting the SQL parameters, it starts calling #StatementHandler#query to execute the database query. StatementHandler is used to create StatementHandler, set parameters, and execute SQL. If not specified, it is used

PreparedStatementHandler.

Let’s first look at StatementHandler’s method of creating Statement #prepared.

public abstract class BaseStatementHandler implements StatementHandler {
    @Override
    public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
        ErrorContext.instance().sql(boundSql.getSql());
        Statement statement = null;
        try {
            // Instantiate Statement
            statement = instantiateStatement(connection);
            //Set the database parameters of Statement
            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);
        }
    }
    // Instantiate Statement
    protected abstract Statement instantiateStatement(Connection connection) throws SQLException;
}

The StatementHandler#prepared method is implemented by the base class BaseStatementHandler. The template method #instantiateStatement is first called to instantiate Statement, and then the Statement is set. The template method is implemented by a specific subclass. For example, PreparedStatementHandler will instantiate PreparedStatement.

Then trace the implementation of StatementHandler and the #parameterize method of setting parameters in PreparedStatementHandler.

public class PreparedStatementHandler extends BaseStatementHandler {
    @Override
    public void parameterize(Statement statement) throws SQLException {
        parameterHandler.setParameters((PreparedStatement) statement);
    }
}

Since PreparedStatement needs to set parameters, here PreparedStatementHandler delegates the action of setting parameters to ParameterHandler for processing.

Let’s look at the implementation of StatementHandler’s method of querying data, StatementHandler#query.

public class PreparedStatementHandler extends BaseStatementHandler {
    @Override
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        PreparedStatement ps = (PreparedStatement) statement;
        ps.execute();
        return resultSetHandler.handleResultSets(ps);
    }
}

PreparedStatementHandler first calls the PreparedStatement#execute method to execute SQL, and then uses ResultSetHandler to convert the query results into the required type. ResultSetHandler will convert database records into objects returned by the Mapper interface method based on resultMap. Due to its complicated internal implementation, we will not analyze it here.

MappedStatement

Conceptual understanding: According to the four SQL types of CURD, the nodes that operate the database in the Mapper xml file are divided into four types: insert, update, select, and delete. The annotations that can be used on the Mapper interface methods are @Insert, @Update, and @Select. , @Delete, MappedStatement represents the metadata of these four statements, and MyBatis saves it in Configuration.

Parsing storage: When MyBatis parses the Mapper xml file, it will use the metadata in the xml node to build a MappedStatement instance and then add it to the Configuration. You can also directly add the Mapper interface to the Configuration. In this case, the annotation information in the Mapper interface will be used to build the MappedStatement and then add it. to Configuration.

Source code location: see XMLMapperBuilder#buildStatementFromContext(List), MapperAnnotationBuilder#parseStatement

Executor

Conceptual understanding: The Executor interface is the executor of SQL. It operates the database based on the abstract MappedStatement and parameters of the SQL statement and returns the operation results. If a database query is performed, the results can be converted into the type expected by the user.

Configure Executor: Normally, there is no need to explicitly specify a specific Executor in MyBatis. If you need to specify it, there are two ways.

Specify the value of defaultExecutorType under the /configuration/settings node in the xml configuration file to configure the default Executor.

Create an instance of Executor through the Configuration#newExecutor(Transaction, ExecutorType) method.

Executor interface definition: The Executor interface is defined as follows.

public interface Executor {
    // Add, modify, or delete database
    int update(MappedStatement ms, Object parameter) throws SQLException;
    // Database query
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
    <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
    // Executor that supports batch processing executes SQL in batches
    List<BatchResult> flushStatements() throws SQLException;
    // transaction management
    void commit(boolean required) throws SQLException;
    void rollback(boolean required) throws SQLException;
    Transaction getTransaction();
    //Load the property value of the object from the cache, or record the property to be obtained from the cache
    void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
    // connection management
    void close(boolean forceRollback);
    boolean isClosed();
    //Set the wrapper for the current Executor
    void setExecutorWrapper(Executor executor);
}

Executor implementation: As an interface, Executor has multiple implementations in MyBatis. The class diagram is as follows.

BaseExecutor: The base class of Executor.

SimpleExecutor: The default Executor in MyBatis.

ReuseExecutor: Reuse the Executor of Statement. For the same SQL, the same Statement is used.

BatchExecutor: Executor that supports batch processing. Each time the #update method is executed, SQL will be added to the Statement until the #flushStatements method is called to start submitting it to the database for execution.

CachingExecutor: As a wrapper for other Executors, it supports Statement-level caching. Other Executors only support Session-level caching.

ResultHandler

ResultHandler is used to process the Java object that MyBatis converts a certain database record into. It usually saves the conversion result inside it and retrieves it when used. Its interface is defined as follows.

public interface ResultHandler<T> {
     // Process the value corresponding to each row
    void handleResult(ResultContext<? extends T> resultContext);
}

There is only one method in the interface, which processes the result according to the context of the result (that is, the Java object corresponding to the single row record in the database). There are two implementations of ResultHandler in MyBatis, as follows.

DefaultResultHandler: Stores the results in the internal List, used when the return type of the Mapper interface method is List.

DefaultMapResultHandler: Stores the result in the internal Map. It is used when the return type of the Mapper interface method is Map. In this case, you need to use the @MapKey annotation on the Mapper method to specify which attribute of the result the key uses.

StatementHandler

StatementHandler represents the processor of Statement in JDBC and is used to create Statement, set SQL parameters in Statement, and execute SQL. The interface is defined as follows.

public interface StatementHandler {
    //Create Statement and set database related parameters
    Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;
    //Set SQL parameters in Statement
    void parameterize(Statement statement) throws SQLException;
    //Add SQL to the batch execution list
    void batch(Statement statement) throws SQLException;
    //Execute add, update, delete SQL
    int update(Statement statement) throws SQLException;
    //Execute query
    <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;
    <E> Cursor<E> queryCursor(Statement statement) throws SQLException;
    // Get SQL information
    BoundSql getBoundSql();
    // Get parameter handler
    ParameterHandler getParameterHandler();
}

According to different Statement types, StatementHandler has different implementations, and its design is very similar to Executor, as detailed below.

BaseStatementHandler: Abstract StatementHandler base class, providing common implementation for subclasses.

SimpleStatementHandler: Simple StatementHandler, handles ordinary Statements.

PreparedStatementHandler: StatementHandler that supports preprocessing and handles PreparedStatement.

CalableStatementHandler: StatementHandler that supports stored procedures and handles CallableStatement.

RoutingStatementHandler: The decorator of other StatementHandlers, which delegates specific processing to other StatementHandlers based on the type of Statement.

ParameterHandler

ParameterHandler is a handler for SQL parameters and is used to set parameters in SQL. Its definition is relatively simple, as detailed below. It has only one default implementation, DefaultParameterHandler.

public interface ParameterHandler {
    // Get the parameter object, which contains the parameters available in SQL
    Object getParameterObject();
    
    //Set SQL parameters
    void setParameters(PreparedStatement ps) throws SQLException;
}
ResultSetHandler

ResultSetHandler is the processor of ResultSet. After Statement executes SQL, ResultSetHandler will be used to process the generated ResultSet. ResultHandler will convert the database record into the return value type of the Mapper interface method based on the resultMap or resultType defined in the Mapper xml file. For each row, the Java object converted to it is processed using ResultHandler. The default implementation of this interface is DefaultResultSetHandler, which is defined as follows.

public interface ResultSetHandler {
    // Process ResultSet into List
    <E> List<E> handleResultSets(Statement stmt) throws SQLException;
    // Process ResultSet as Cursor
    <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
    // Handle stored procedure OUT parameters
    void handleOutputParameters(CallableStatement cs) throws SQLException;
}
Summary

Previously, we used the SqlSession method as the entry point to analyze the code executed by SQL within MyBatis, and introduced the important APIs. Here we use text to summarize the entire process.

First we will use SqlSessionFactoryBuilder to build SqlSessionFactory. During this period, MyBatis will parse the Mapper xml file or annotation, generate the SQL statement metadata MappedStatement, and save it to the Configuration.

Use SqlSessionFactory to obtain SqlSession. The default SqlSession is DefaultSqlSession.

Use SqlSession to obtain the Mapper interface implementation, or directly execute other methods of SqlSession to operate the database.

SqlSession obtains the MappedStatement from the Configuration based on the identity of the statement, and then entrusts the Executor to operate the database.

Executor first obtains data from the cache. If it is not in the cache, it will execute the database query. For update operations, the cache will be refreshed first.

Executor uses Configuration to create StatementHandler, the handler of Statement, and entrusts StatementHandler to perform database operations.

StatementHandler first instantiates Statement, then uses ParameterHandler to set SQL parameters, and finally executes Statement.

StatementHandler delegates ResultSetHandler to process the result set.

ResultSetHandler processes the result set, converts each row of database records into Java objects according to resultMap or resultType, then hands the Java objects to ResultHandler for processing, and finally converts them into the return type of the Mapper interface method.