SQL Execution of Mybatis Core Working Principles
Learn the structure, working principle and main modules of Mybatis from a macro perspective, and learn the working principle and design idea of Mybatis from a micro perspective.
1. SQL Execution
Let's look at the execution of Mybatis's SQL statement.
List<User> list = mapper.selectUserList();
As we mentioned earlier, all Mappers are JDK dynamic proxy objects, so any method is to execute the invoke() method of the management class MapperProxy.
1.MapperProxy.invoke()
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // Methods such as toString hashCode equals getClass do not require walking to the process of executing SQL if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else { // Increase the efficiency of getting mapperMethod to invoke MapperMethodInvoker // The normal method goes to invoke of PlainMethodInvoker return cachedInvoker(method).invoke(proxy, method, args, sqlSession); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); }
Enter into the cachedInvoker method.
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable { try { // Map's method in Java8, gets a value based on the key, and assigns the value of the following Object to the key if the value is null // If not, create // Getting the MapperMethodInvoker object, there is only one invoke method // Remove from the methodCache based on the method Fill in with the second parameter if empty is returned return methodCache.computeIfAbsent(method, m -> { if (m.isDefault()) { // The default method of the interface (Java8), which inherits the default method of the interface whenever it is implemented, such as List.sort() try { if (privateLookupInMethod == null) { return new DefaultMethodInvoker(getMethodHandleJava8(method)); } else { return new DefaultMethodInvoker(getMethodHandleJava9(method)); } } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { throw new RuntimeException(e); } } else { // Create a MapperMethod return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); } }); } catch (RuntimeException re) { Throwable cause = re.getCause(); throw cause == null ? re : cause; } }
Default method judgments are made first, and then the invoke() method of PlainMethodInvoker is entered, which is also the entry point for true SQL execution.
@Override public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable { // The real starting point for SQL execution return mapperMethod.execute(sqlSession, args); }
2.mapperMethod.execute()
public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { // Call SqlSession's corresponding method based on the type of SQL statement case INSERT: { // Associate user-passed arguments with specified parameter names by processing args[] arrays with ParamNameResolver Object param = method.convertArgsToSqlCommandParam(args); // sqlSession.insert(command.getName(), param) calls the insert method of SqlSession // The rowCountResult method converts the results based on the return value type of the method recorded in the method field result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { // Method with empty return value and ResultSet handled by ResultHandler executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { // Method that returns a single object Object param = method.convertArgsToSqlCommandParam(args); // Execution Entry of Common select Statement >> result = sqlSession.selectOne(command.getName(), param); if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; }
Here, according to different types (INSERT, UPDATE, DELETE, SELECT) and return types:
- Call the convertArgsToSqlCommandParam() method to convert the parameters to SQL parameters.
- Call the insert(), update(), delete(), selectOne() methods of SqlSession.
Take selectOne() as an example.
3.sqlSession.selectOne()
@Override public <T> T selectOne(String statement, Object parameter) { // Going to DefaultSqlSession // Popular vote was to return null on 0 results and throw exception on too many. List<T> list = this.selectList(statement, parameter); if (list.size() == 1) { return list.get(0); } else if (list.size() > 1) { throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size()); } else { return null; } }
selectOne() also calls the selectList() method, in which you first get the MappedStatement in Configuration based on the statement ID.Variable ms has all the attributes in xml that add or delete to the check label configuration, including id, statementType, sqlSource, useCache, and so on.
@Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); // If cacheEnabled = true (default), Executor will be decorated with CachingExecutor 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(); } }
Then executor.query()
Executor was created at openSeesion, created the basic type of executor, then wrapped in a secondary cache, followed by a plug-in wrapper.
If wrapped by a plug-in, you go to the logic of the plug-in, then to the logic of the CachingExecutor, and finally to the query() method of the BaseExecutor.
4.CachingExecutor.query()
@Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // Get SQL BoundSql boundSql = ms.getBoundSql(parameterObject); // Create CacheKey: What kind of SQL is the same SQL?>> CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
What is the CacheKey of the secondary cache?The same CacheKey means that two queries are the same query.
Entering the Executor.createCacheKey() method, we can see the six elements that make up it:
@Override public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (closed) { throw new ExecutorException("Executor was closed."); } CacheKey cacheKey = new CacheKey(); //Element One cacheKey.update(ms.getId()); //Element Two cacheKey.update(rowBounds.getOffset()); //Element Three cacheKey.update(rowBounds.getLimit()); //Element Four cacheKey.update(boundSql.getSql()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); for (ParameterMapping parameterMapping : parameterMappings) { if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); if (boundSql.hasAdditionalParameter(propertyName)) { 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); } //Element Five cacheKey.update(value); } } if (configuration.getEnvironment() != null) { //Element Six cacheKey.update(configuration.getEnvironment().getId()); } return cacheKey; }
From the six elements of CacheKey, we can see that the same query is considered to be the same query if the methods are the same, the page offsets are the same, the SQL statements are the same, the parameter values are the same, and the data source environment is the same.
Let's look at the properties of CacheKey:
private static final int DEFAULT_MULTIPLIER = 37; private static final int DEFAULT_HASHCODE = 17; private final int multiplier; private int hashcode; private long checksum; private int count; private List<Object> updateList;
How do I compare two CacheKey values to be equal?It is inefficient to compare these six features six times if they are equal in turn.Mybatis calculates hash codes. Every class that inherits Object has a hashCode() method. When the CacheKey value is generated (update method), it also updates the hashCode of CacheKey, which is generated by a multiplication hash (cardinality baseHashCode=17, multiplication factor multiplier=37).
hashcode = multiplier * hashcode + baseHashCode;
When CacheKey is generated, the query() method is called.
5.BaseExecutor.query()
@Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); // Where was the cache object created?XMLMapperBuilder class xmlconfigurationElement() // Determined by <cache>tag if (cache != null) { // flushCache="true" Empty Level 1 and Level 2 caches >> flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); // Get secondary cache // Cache is managed through TransactionalCache Manager, TransactionalCache @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // Write to Level 2 Cache tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } // Go to SimpleExecutor | ReuseExecutor | BatchExecutor return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } @Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // ErrorContext for Exception Systems ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } if (queryStack == 0 && ms.isFlushCacheRequired()) { // When flushCache="true", even queries empty the first level cache clearLocalCache(); } List<E> list; try { // Prevent recursive queries from repeating cache processing queryStack++; // Query Level 1 Cache // Differences between ResultHandler and ResultSetHandler list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { // True query process 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; } private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; // Preemptive Placement localCache.putObject(key, EXECUTION_PLACEHOLDER); try { // The difference between the three Executor s, see doUpdate // Default Simple list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { // Remove placeholders localCache.removeObject(key); } // Write Level 1 Cache localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
6.SimpleExecutor.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(); // Note that you have reached StatementHandler >>the key object for SQL processing StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); // Get a Statement object stmt = prepareStatement(handler, ms.getStatementLog()); // Execute Query return handler.query(stmt, resultHandler); } finally { // Close when used up closeStatement(stmt); } }
RoutingStatementHandler is derived from the configuration.newStatementHandler() method.The RoutingStatementHandler determines the type of StatementHandler based on the statementType inside the MappedStatement.The default is PREPARED.
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { // How did StatementType come from?Add or delete statementType="PREPARED" in check label, default value PREPARED switch (ms.getStatementType()) { case STATEMENT: delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); break; case PREPARED: // What did you do when you created the StatementHandler?>> 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()); } }
The StatementHandler contains the ParameterHandler for processing parameters and the ResultSetHandler for processing result sets.
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { this.configuration = mappedStatement.getConfiguration(); this.executor = executor; this.mappedStatement = mappedStatement; this.rowBounds = rowBounds; this.typeHandlerRegistry = configuration.getTypeHandlerRegistry(); this.objectFactory = configuration.getObjectFactory(); if (boundSql == null) { // issue #435, get the key before calculating the statement generateKeys(parameterObject); boundSql = mappedStatement.getBoundSql(parameterObject); } this.boundSql = boundSql; // Created the other two big objects of the four major objects >> // What were you doing when you created these two objects? this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql); this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql); }
These three objects are one of the four objects that plug-ins can intercept, so they are wrapped in interceptors after they are created.
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); // Implant plug-in logic (return proxy object) parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler); return parameterHandler; } public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) { ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds); // Implant plug-in logic (return proxy object) resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler); return resultSetHandler; } public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); // Implant plug-in logic (return proxy object) statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; }
Create a Statement object by creating a StatementHandler.
// Get a Statement object stmt = prepareStatement(handler, ms.getStatementLog());
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; Connection connection = getConnection(statementLog); // Get Statement Object stmt = handler.prepare(connection, transaction.getTimeout()); // Set parameters for Statement handler.parameterize(stmt); return stmt; }
Perform queries and, if there is a plug-in wrapper, go to the blocked business first.
// Execute Query return handler.query(stmt, resultHandler);
Processing into PreparedStatementHandler
@Override public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; // Process to JDBC ps.execute(); // Processing result set return resultSetHandler.handleResultSets(ps); }
Execute the execute() method of PreparedStatement followed by the execution of PreparedStatement in the JDBC package.
ResultSetHandler processes the result set, and if there is a plug-in wrapper, it is processed first in the intercepted business.
2. Summary
1.Mybatis Core Objects
object | Related Objects | Effect |
---|---|---|
Configuration | MapperRegistry TypeAlisaRegistry TypeHandlerRegistry | Contains all configuration information for Mybaits |
SqlSession | SqlSessionFactory DefaultSqlSession | Encapsulate API s for add-delete change checks of operational databases and provide them to the application layer for use |
Executor | BaseExecutor SimpleExecutor BatchExecutor ReuseExecutor | Mybatis executor, the core of Mybatis scheduling, is responsible for generating SQL statements and maintaining query cache |
StatementHandler | BaseStatementHandler SimpleStatementHandler PreparedStatementHandler CallableStatementHandler | Encapsulates Statement operations in JDBC |
ParameterHandler | DefaultParameterHandler | Converts the passed parameter to the parameter required by Statement in JDBC |
ResultSetHandler | DefaultResultSetHandler | Converts the ResultSet result set object returned by JDBC to a collection of List type |
MapperProxy | MapperProxyFactory | Used to proxy Mapper interface methods |
MappedStatement | SqlSource BoundSql | MappedStatement maintains the encapsulation of a <select|update|delete|insert>node that contains an SQL SQL SQL SQL information, entry and exit information |