An insert operation
Take the simple operation of saving a record to the table as an example. Follow this example to track how the MySQL statement executes. To save a user record to the table:
sqlSession.insert("x.y.insertUser", user);
First of all, we need to look at the sqlsession ා insert method, and find the specific implementation in the DefaultSqlSession implementation class. The following two code fragments are the call chains in the insert method implementation:
@Override public int insert(String statement, Object parameter) { return update(statement, parameter); }
@Override public int update(String statement, Object parameter) { try { dirty = true; MappedStatement ms = configuration.getMappedStatement(statement); return executor.update(ms, wrapCollection(parameter)); } catch (Exception e) { throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
The first code shows that the update method is invoked in the insert method, and the insertion operation is implemented through the update method. In addition, the delete method is actually the update method that is invoked to complete the removal operation.
Next, in the second section of the code, in the update method, we first obtain a MappedStatement object, which actually encapsulates the information of each insert, delete, update node in the mapper mapping file that we configured. It is parsed when mybatis loads the Configuration object.
Then the update operation is completed through an executor object. Executor is a reference of type executor held in DefaultSqlSession, which is set when initializing DefaultSqlSession. Executor is the bottom core component of mybatis. We know that SqlSession provides a set of user-oriented methods, and then the bottom layer of these methods provides the actual capabilities through executor. Executor defines a set of operation methods similar to SqlSession, which is a bit like appearance mode.
MyBatis provides the specific implementation of various executors. The following is its architecture diagram:
By default, MyBatis uses simpleexecutior, which will be mentioned in the instructions after how to use other types of executors. Therefore, we need to look at the implementation of update in SimpleExector, but there is a basic implementation in BaseExecutor, and then through the template method of doUpdate, we can call specific subclasses:
// BaseExecutor: @Override public int update(MappedStatement ms, Object parameter) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } clearLocalCache(); // Implementation of calling concrete subclass through doepdate abstract method return doUpdate(ms, parameter); }
// SimpleExecutor: @Override 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); } }
As shown above, in the doUpdate method implementation of simpleexecution, a StatementHandler is created through the configuration object. StatementHandler is another important core object in MyBatis, which is a java interface. In fact, the operations on the database are completed in StatementHandler component, and the Executor is like a distributor, which divides different operations Sent to StatementHandler object for processing, StatementHandler basically implements the whole operation process: creating Statement object from sql Statement, replacing sql parameter placeholder, query, update and processing result set, etc. these operations can be seen from StatementHandler interface definition pasted below:
public interface StatementHandler { // Creating a jdbc Statement object Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException; // Execute parameter filling of parameterized sql void parameterize(Statement statement) throws SQLException; // Batch update operation, corresponding to the addBatch operation of jdbc void batch(Statement statement) throws SQLException; int update(Statement statement) throws SQLException; <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException; <E> Cursor<E> queryCursor(Statement statement) throws SQLException; // Get the bound sql statement object BoundSql getBoundSql(); ParameterHandler getParameterHandler(); }
For PreparedStatement, CallableStatement and Statement type sql, StatementHandler provides specific implementation classes respectively, and its class extension structure is shown as follows:
The RoutingStatementHandler in the figure is different from the other three implementations. It is an implementation with distribution function. It forwards specific requests to the other three specific statementhandlers. It holds a StatementHandler of other specific types that references delegate. According to different sql types, it delegates requests to specific statementhandlers. Look at RoutingStatementHandler The code of E is clear:
// RoutingStatementHandler: public class RoutingStatementHandler implements StatementHandler { // Delegate object private final StatementHandler delegate; // Initialize the delegate object to a specific StatementHandler according to different kinds of sql 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()); } } @Override public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException { return delegate.prepare(connection, transactionTimeout); } @Override public void parameterize(Statement statement) throws SQLException { delegate.parameterize(statement); } // ... omit other StatementHandler implementations }
Now let's go back to the code snippet labeled "SimpleExecutor:" above:
First, the StatementHandler created through configuration is a RoutingStatementHandler;
The second step is to obtain a Statement object through the prepareStatement method:
// SimpleExecutor#prepareStatement: private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; // The Connection object is obtained from the data source at the bottom of the method, and the transaction commit property and transaction isolation level are set Connection connection = getConnection(statementLog); // Create a Statement object through the preparation method of the RoutingStatementHandler just mentioned; // The RoutingStatementHandler is delegated to the specific StatementHandler, // If there is no special configuration in sql mapper, it should be PreparedStatementHandler stmt = handler.prepare(connection, transaction.getTimeout()); // Last fill parameter placeholder handler.parameterize(stmt); return stmt; }
Here is the method call to replace the parameter placeholder. The PreparedStatementHandler is implemented by a ParameterHandler. The PreparedStatementHandler also has a ResultHandler, which is used to process the query result set. You can see that the Executor cooperates with StatementHandler, ParameterHandler and ResultHandler to complete the operation of the whole database.
Step 3: call handler.update(stmt). According to the above instructions, the call is delegated to the update method of PreparedStatementHandler. See the final update implementation:
// PreparedStatementHandler#update: @Override public int update(Statement statement) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; // Execution statement ps.execute(); // Get affected rows int rows = ps.getUpdateCount(); // Get the generated key below Object parameterObject = boundSql.getParameterObject(); KeyGenerator keyGenerator = mappedStatement.getKeyGenerator(); keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject); return rows; }
The whole insert database operation is completed.
Creation of Executor
As mentioned earlier, the executor reference in DefaultSqlSession sets the reference value when initializing. This is created through DefaultSqlSessionFactory. Take our usual example of creating a SqlSession object:
SqlSessionFactory sqlSessionFactory = SqlSessionFactoryBuilder.build(stream); SqlSession sqlSession = sqlSessionFactory.open();
The SqlSessionFactoryBuilder.build() method creates an instance of DefaultSqlSessionFactory type. Let's trace the process in DefaultSqlSessionFactory:
// DefaultSqlSessionFactory#openSession: public SqlSession openSession() { // Get the default executor type return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); }
// DefaultSqlSessionFactory#openSessionFromDataSource: private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); // Create an executor object final Executor executor = configuration.newExecutor(tx, execType); // Create a DefaultSqlSession object and pass the executor object to it return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
Let's look at the creation code of the Executor instance:
// Configuration#newExecutor: public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor 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 (cacheEnabled) { executor = new CachingExecutor(executor); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
In the newExecutor method, an Executor instance of a specific type is created according to the executorType parameter. In the openSession method, the executorType value sets the default value in the Configuration object. The default setting in Configuration is SIMPLE:
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
Therefore, by default, when we use openSession() or openSession(autoCommit) method, we get the simpleexecution instance internally. Of course, we can modify the default value. The Configuration class provides the setter method of defaultExecutorType, which corresponds to setting the defaultExecutorType property value to SIMPLE, REUSE or BATCH in the Configuration file. Another way is to directly use SqlSession Other heavy-duty forms of ion:
sqlSession.openSession(ExecutorType.REUSE); // Or: sqlSession.openSession(ExecutorType.REUSE, true);
Dependencies of several important classes
The following is the dependency graph of several important classes in mybatis, which is helpful to understand how mybatis realizes things control.