Mybatis creates statement and result set generation
Previous: Mybatis(3) executes sql procedure
statementHandlerIn the Configuration class of Mybatis, there are the following three methods. If we want to generate a statement, we need to use a statement processor
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); 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); 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); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; }
In the following method of generating statement, we can see that there are parameterHandler and resultSetHandler in statement
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; this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql); this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql); }
The function of parameterHandler is very simple. First, it contains the types of jdbc and java. In MappedStatement, it stores the basic information of sql and the parameter information to be replaced
When generating the final sql, you need this parameterHandler to work with you
resultSetHandler is used to process the result when the query of sql returns
In the prepareStatement method, we can see that the handler is used to generate a statement, and then the input parameter is set in the parameterize method
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; }
public void setParameters(PreparedStatement ps) { ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); // Get the input parameter name from boundSql, as in sqlSource in the screenshot above 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; // Get the input parameter name from ParameterMapping 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 { // parameterObject is the input parameter that we write in mapper. If @ Param annotation exists // Then the key will be the value of @ Param MetaObject metaObject = configuration.newMetaObject(parameterObject); // Use propertyName as key to get value in the parameter map value = metaObject.getValue(propertyName); } // Get the java type of propertyName TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) { jdbcType = configuration.getJdbcTypeForNull(); } try { // Replace the placeholder in sql with the value obtained typeHandler.setParameter(ps, i + 1, value, jdbcType); } catch (TypeException | SQLException e) { throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); } } } } }
The getValue method of mateObject can bind parameters through the parameter name obtained above
public Object getValue(String name) { PropertyTokenizer prop = new PropertyTokenizer(name); if (prop.hasNext()) { MetaObject metaValue = metaObjectForProperty(prop.getIndexedName()); if (metaValue == SystemMetaObject.NULL_META_OBJECT) { return null; } else { return metaValue.getValue(prop.getChildren()); } } else { return objectWrapper.get(prop); } }
The typeHandler obtained earlier is object. Here, use getClass of parameter(value) to get the real type
@Override public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { TypeHandler handler = resolveTypeHandler(parameter, jdbcType); // Insert data with key I and value parameter into columnMap of ps handler.setParameter(ps, i, parameter, jdbcType); }
Finally, this setParameter will call the setNString method of PreparedStatement, and this method will be finally executed by the agent is the setString method of HikariProxyPreparedStatement, but I can't download the source code here So we have to guess
// Replace * * NOT SPECIFIED * * at var1 location with var2 public void setString(int var1, String var2) throws SQLException { try { ((PreparedStatement)super.delegate).setString(var1, var2); } catch (SQLException var4) { throw this.checkException(var4); } }Result set processing
In the previous step, we finished processing the sql statement. After submitting it to the database, the database will return the result set. We need to process the result set and put the result into the javabean
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); // Got a statement stmt = prepareStatement(handler, ms.getStatementLog()); // Start to query with statement's processor return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } }
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; // Here, a request is initiated to submit the sql to the database for execution ps.execute(); // Start processing results after the above execution return resultSetHandler.handleResultSets(ps); }
Let's first look at the original data returned by the database in the results of delegate
You can see that what my sql statement returns is indeed all the contents of my table. The second figure is the field name, and their value s are stored in the form of byte [] in the first figure
So it is clear here that our subsequent processing operations are to create a key value according to the contents of these two graphs or directly put it into our javabean as the return value of the method
public List<Object> handleResultSets(Statement stmt) throws SQLException { ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); final List<Object> multipleResults = new ArrayList<>(); int resultSetCount = 0; // The content screenshot of rsw is below. According to the screenshot, we extract our key and put it in columnName ResultSetWrapper rsw = getFirstResultSet(stmt); // Get the resultMap, which is our method return value. What I define here is UserDTO // The type in the type of resultMap is the return type we defined List<ResultMap> resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); validateResultMapsCount(rsw, resultMapCount); while (rsw != null && resultMapCount > resultSetCount) { ResultMap resultMap = resultMaps.get(resultSetCount); // Process result set // Whether it's selectOne or not, it's a list handleResultSet(rsw, resultMap, multipleResults, null); rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } String[] resultSets = mappedStatement.getResultSets(); if (resultSets != null) { while (rsw != null && resultSetCount < resultSets.length) { ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); if (parentMapping != null) { String nestedResultMapId = parentMapping.getNestedResultMapId(); ResultMap resultMap = configuration.getResultMap(nestedResultMapId); handleResultSet(rsw, resultMap, null, parentMapping); } rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } } return collapseSingleResultList(multipleResults); }
How to deal with result set
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException { final ResultLoaderMap lazyLoader = new ResultLoaderMap(); // An Object is generated here. The content is UserDTO Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix); if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { // Generate a metaObject for subsequent operations, and put the UserDTO into the metaObject final MetaObject metaObject = configuration.newMetaObject(rowValue); boolean foundValues = this.useConstructorMappings; // This is to determine whether hump mapping is enabled. I have enabled it here if (shouldApplyAutomaticMappings(resultMap, false)) { // After enabling hump mapping, get the mapping relationship and put the value into UserDTO foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues; } foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues; foundValues = lazyLoader.size() > 0 || foundValues; rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null; } // The user dto after all values are injected is returned here return rowValue; }
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException { List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix); boolean foundValues = false; if (!autoMapping.isEmpty()) { for (UnMappedColumnAutoMapping mapping : autoMapping) { // Get the value of the key column in rsw's resultSet // As I said above, get byte [] in delegate and return after conversion final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column); if (value != null) { foundValues = true; } if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) { // gcode issue #377, call setter on nulls (value is not 'found') // Here, we inject the value into our user dto with the key after hump mapping metaObject.setValue(mapping.property, value); } } } return foundValues; }
Finally, let's look at the entry queryFromDatabase method, which opens the first level cache of mybatis,
My understanding of L1 cache is that in the case of high concurrency, there may be many same requests. We only need to actually request the database once, without wasting resources, and the results of this database query can be used for new query requests generated in this query
So we can see that in this method, a cache is generated before doQuery, and the data obtained is injected into the cache after doQuery. Obviously, when a data becomes hot data, this cache can be used to reduce the number of database queries
But when I actually use it, I send multiple requests continuously without triggering the cache, because this localCache is not the same localCache, because the cache life cycle exists in a SqlSession
After querying the data, we found that if you want to use the first level cache, you need to open the Transaction and use the annotation @ Transaction
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 { // Here is the list received, so when we process the result set, we get a list 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; }
Finally, the list returned by the queryFromDatabase method will go back to selectList, and the selectList method is invoked in the selectOne method. In the end, it is judged whether the size of the list is 1. If not, the but found x abnormal information of selectOne is thrown, if so, the result is returned.
PostscriptIt's almost the end of learning the source code of mybatis. It's found that mybatis has poor scalability, because after reading so many methods, there is almost no interface oriented programming