diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexDefaultResultSetHandler.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexDefaultResultSetHandler.java
new file mode 100644
index 00000000..1ed8774a
--- /dev/null
+++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexDefaultResultSetHandler.java
@@ -0,0 +1,1251 @@
+/*
+ * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.mybatisflex.core.mybatis;
+
+import org.apache.ibatis.annotations.AutomapConstructor;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.binding.MapperMethod.ParamMap;
+import org.apache.ibatis.cache.CacheKey;
+import org.apache.ibatis.cursor.Cursor;
+import org.apache.ibatis.cursor.defaults.DefaultCursor;
+import org.apache.ibatis.executor.ErrorContext;
+import org.apache.ibatis.executor.Executor;
+import org.apache.ibatis.executor.ExecutorException;
+import org.apache.ibatis.executor.loader.ResultLoader;
+import org.apache.ibatis.executor.loader.ResultLoaderMap;
+import org.apache.ibatis.executor.parameter.ParameterHandler;
+import org.apache.ibatis.executor.result.DefaultResultContext;
+import org.apache.ibatis.executor.result.DefaultResultHandler;
+import org.apache.ibatis.executor.result.ResultMapException;
+import org.apache.ibatis.executor.resultset.DefaultResultSetHandler;
+import org.apache.ibatis.executor.resultset.ResultSetWrapper;
+import org.apache.ibatis.mapping.*;
+import org.apache.ibatis.reflection.MetaClass;
+import org.apache.ibatis.reflection.MetaObject;
+import org.apache.ibatis.reflection.ReflectorFactory;
+import org.apache.ibatis.reflection.factory.ObjectFactory;
+import org.apache.ibatis.session.*;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.TypeHandler;
+import org.apache.ibatis.type.TypeHandlerRegistry;
+import org.apache.ibatis.util.MapUtil;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Parameter;
+import java.sql.CallableStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.text.MessageFormat;
+import java.util.*;
+
+/**
+ * 复制于 DefaultResultSetHandler,并开放若干方法,方便子类重写
+ * @author Clinton Begin
+ * @author Eduardo Macarron
+ * @author Iwao AVE!
+ * @author Kazuki Shimizu
+ * @author Michael
+ */
+public class FlexDefaultResultSetHandler extends DefaultResultSetHandler {
+
+ private static final Object DEFERRED = new Object();
+
+ private final Executor executor;
+ private final Configuration configuration;
+ private final MappedStatement mappedStatement;
+ private final RowBounds rowBounds;
+ private final ParameterHandler parameterHandler;
+ private final ResultHandler> resultHandler;
+ private final BoundSql boundSql;
+ private final TypeHandlerRegistry typeHandlerRegistry;
+ private final ObjectFactory objectFactory;
+ private final ReflectorFactory reflectorFactory;
+
+ // nested resultmaps
+ private final Map nestedResultObjects = new HashMap<>();
+ private final Map ancestorObjects = new HashMap<>();
+ private Object previousRowValue;
+
+ // multiple resultsets
+ private final Map nextResultMaps = new HashMap<>();
+ private final Map> pendingRelations = new HashMap<>();
+
+ // Cached Automappings
+ private final Map> autoMappingsCache = new HashMap<>();
+ private final Map> constructorAutoMappingColumns = new HashMap<>();
+
+ // temporary marking flag that indicate using constructor mapping (use field to reduce memory usage)
+ private boolean useConstructorMappings;
+
+ private static class PendingRelation {
+ public MetaObject metaObject;
+ public ResultMapping propertyMapping;
+ }
+
+ private static class UnMappedColumnAutoMapping {
+ private final String column;
+ private final String property;
+ private final TypeHandler> typeHandler;
+ private final boolean primitive;
+
+ public UnMappedColumnAutoMapping(String column, String property, TypeHandler> typeHandler, boolean primitive) {
+ this.column = column;
+ this.property = property;
+ this.typeHandler = typeHandler;
+ this.primitive = primitive;
+ }
+ }
+
+ public FlexDefaultResultSetHandler(Executor executor, MappedStatement mappedStatement, ParameterHandler parameterHandler,
+ ResultHandler> resultHandler, BoundSql boundSql, RowBounds rowBounds) {
+ super(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
+ this.executor = executor;
+ this.configuration = mappedStatement.getConfiguration();
+ this.mappedStatement = mappedStatement;
+ this.rowBounds = rowBounds;
+ this.parameterHandler = parameterHandler;
+ this.boundSql = boundSql;
+ this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
+ this.objectFactory = configuration.getObjectFactory();
+ this.reflectorFactory = configuration.getReflectorFactory();
+ this.resultHandler = resultHandler;
+ }
+
+ //
+ // HANDLE OUTPUT PARAMETER
+ //
+
+ @Override
+ public void handleOutputParameters(CallableStatement cs) throws SQLException {
+ final Object parameterObject = parameterHandler.getParameterObject();
+ final MetaObject metaParam = configuration.newMetaObject(parameterObject);
+ final List parameterMappings = boundSql.getParameterMappings();
+ for (int i = 0; i < parameterMappings.size(); i++) {
+ final ParameterMapping parameterMapping = parameterMappings.get(i);
+ if (parameterMapping.getMode() == ParameterMode.OUT || parameterMapping.getMode() == ParameterMode.INOUT) {
+ if (ResultSet.class.equals(parameterMapping.getJavaType())) {
+ handleRefCursorOutputParameter((ResultSet) cs.getObject(i + 1), parameterMapping, metaParam);
+ } else {
+ final TypeHandler> typeHandler = parameterMapping.getTypeHandler();
+ metaParam.setValue(parameterMapping.getProperty(), typeHandler.getResult(cs, i + 1));
+ }
+ }
+ }
+ }
+
+ private void handleRefCursorOutputParameter(ResultSet rs, ParameterMapping parameterMapping, MetaObject metaParam)
+ throws SQLException {
+ if (rs == null) {
+ return;
+ }
+ try {
+ final String resultMapId = parameterMapping.getResultMapId();
+ final ResultMap resultMap = configuration.getResultMap(resultMapId);
+ final ResultSetWrapper rsw = new ResultSetWrapper(rs, configuration);
+ if (this.resultHandler == null) {
+ final DefaultResultHandler resultHandler = new DefaultResultHandler(objectFactory);
+ handleRowValues(rsw, resultMap, resultHandler, new RowBounds(), null);
+ metaParam.setValue(parameterMapping.getProperty(), resultHandler.getResultList());
+ } else {
+ handleRowValues(rsw, resultMap, resultHandler, new RowBounds(), null);
+ }
+ } finally {
+ // issue #228 (close resultsets)
+ closeResultSet(rs);
+ }
+ }
+
+ //
+ // HANDLE RESULT SETS
+ //
+ @Override
+ public List handleResultSets(Statement stmt) throws SQLException {
+ ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
+
+ final List multipleResults = new ArrayList<>();
+
+ int resultSetCount = 0;
+ ResultSetWrapper rsw = getFirstResultSet(stmt);
+
+ List resultMaps = mappedStatement.getResultMaps();
+ int resultMapCount = resultMaps.size();
+ validateResultMapsCount(rsw, resultMapCount);
+ while (rsw != null && resultMapCount > resultSetCount) {
+ ResultMap resultMap = resultMaps.get(resultSetCount);
+ 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);
+ }
+
+ @Override
+ public Cursor handleCursorResultSets(Statement stmt) throws SQLException {
+ ErrorContext.instance().activity("handling cursor results").object(mappedStatement.getId());
+
+ ResultSetWrapper rsw = getFirstResultSet(stmt);
+
+ List resultMaps = mappedStatement.getResultMaps();
+
+ int resultMapCount = resultMaps.size();
+ validateResultMapsCount(rsw, resultMapCount);
+ if (resultMapCount != 1) {
+ throw new ExecutorException("Cursor results cannot be mapped to multiple resultMaps");
+ }
+
+ ResultMap resultMap = resultMaps.get(0);
+ return new DefaultCursor<>(this, resultMap, rsw, rowBounds);
+ }
+
+ private ResultSetWrapper getFirstResultSet(Statement stmt) throws SQLException {
+ ResultSet rs = stmt.getResultSet();
+ while (rs == null) {
+ // move forward to get the first resultset in case the driver
+ // doesn't return the resultset as the first result (HSQLDB 2.1)
+ if (stmt.getMoreResults()) {
+ rs = stmt.getResultSet();
+ } else if (stmt.getUpdateCount() == -1) {
+ // no more results. Must be no resultset
+ break;
+ }
+ }
+ return rs != null ? new ResultSetWrapper(rs, configuration) : null;
+ }
+
+ private ResultSetWrapper getNextResultSet(Statement stmt) {
+ // Making this method tolerant of bad JDBC drivers
+ try {
+ // Crazy Standard JDBC way of determining if there are more results
+ if (stmt.getConnection().getMetaData().supportsMultipleResultSets()
+ && (stmt.getMoreResults() || (stmt.getUpdateCount() != -1))) {
+ ResultSet rs = stmt.getResultSet();
+ if (rs == null) {
+ return getNextResultSet(stmt);
+ }
+ return new ResultSetWrapper(rs, configuration);
+ }
+ } catch (Exception e) {
+ // Intentionally ignored.
+ }
+ return null;
+ }
+
+ private void closeResultSet(ResultSet rs) {
+ try {
+ if (rs != null) {
+ rs.close();
+ }
+ } catch (SQLException e) {
+ // ignore
+ }
+ }
+
+ private void cleanUpAfterHandlingResultSet() {
+ nestedResultObjects.clear();
+ }
+
+ private void validateResultMapsCount(ResultSetWrapper rsw, int resultMapCount) {
+ if (rsw != null && resultMapCount < 1) {
+ throw new ExecutorException(
+ "A query was run and no Result Maps were found for the Mapped Statement '" + mappedStatement.getId()
+ + "'. 'resultType' or 'resultMap' must be specified when there is no corresponding method.");
+ }
+ }
+
+ private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List multipleResults,
+ ResultMapping parentMapping) throws SQLException {
+ try {
+ if (parentMapping != null) {
+ handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
+ } else if (resultHandler == null) {
+ DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
+ handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
+ multipleResults.add(defaultResultHandler.getResultList());
+ } else {
+ handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
+ }
+ } finally {
+ // issue #228 (close resultsets)
+ closeResultSet(rsw.getResultSet());
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private List collapseSingleResultList(List multipleResults) {
+ return multipleResults.size() == 1 ? (List) multipleResults.get(0) : multipleResults;
+ }
+
+ //
+ // HANDLE ROWS FOR SIMPLE RESULTMAP
+ //
+
+ @Override
+ public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler> resultHandler,
+ RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
+ if (resultMap.hasNestedResultMaps()) {
+ ensureNoRowBounds();
+ checkResultHandler();
+ handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
+ } else {
+ handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
+ }
+ }
+
+ private void ensureNoRowBounds() {
+ if (configuration.isSafeRowBoundsEnabled() && rowBounds != null
+ && (rowBounds.getLimit() < RowBounds.NO_ROW_LIMIT || rowBounds.getOffset() > RowBounds.NO_ROW_OFFSET)) {
+ throw new ExecutorException(
+ "Mapped Statements with nested result mappings cannot be safely constrained by RowBounds. "
+ + "Use safeRowBoundsEnabled=false setting to bypass this check.");
+ }
+ }
+
+ @Override
+ protected void checkResultHandler() {
+ if (resultHandler != null && configuration.isSafeResultHandlerEnabled() && !mappedStatement.isResultOrdered()) {
+ throw new ExecutorException(
+ "Mapped Statements with nested result mappings cannot be safely used with a custom ResultHandler. "
+ + "Use safeResultHandlerEnabled=false setting to bypass this check "
+ + "or ensure your statement returns ordered data and set resultOrdered=true on it.");
+ }
+ }
+
+ private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap,
+ ResultHandler> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
+ DefaultResultContext resultContext = new DefaultResultContext<>();
+ ResultSet resultSet = rsw.getResultSet();
+ skipRows(resultSet, rowBounds);
+ while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
+ ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
+ Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
+ storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
+ }
+ }
+
+ private void storeObject(ResultHandler> resultHandler, DefaultResultContext resultContext, Object rowValue,
+ ResultMapping parentMapping, ResultSet rs) throws SQLException {
+ if (parentMapping != null) {
+ linkToParents(rs, parentMapping, rowValue);
+ } else {
+ callResultHandler(resultHandler, resultContext, rowValue);
+ }
+ }
+
+ @SuppressWarnings("unchecked" /* because ResultHandler> is always ResultHandler */)
+ private void callResultHandler(ResultHandler> resultHandler, DefaultResultContext resultContext,
+ Object rowValue) {
+ resultContext.nextResultObject(rowValue);
+ ((ResultHandler) resultHandler).handleResult(resultContext);
+ }
+
+ private boolean shouldProcessMoreRows(ResultContext> context, RowBounds rowBounds) {
+ return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
+ }
+
+ private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
+ if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
+ if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
+ rs.absolute(rowBounds.getOffset());
+ }
+ } else {
+ for (int i = 0; i < rowBounds.getOffset(); i++) {
+ if (!rs.next()) {
+ break;
+ }
+ }
+ }
+ }
+
+ //
+ // GET VALUE FROM ROW FOR SIMPLE RESULT MAP
+ //
+
+ private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
+ final ResultLoaderMap lazyLoader = new ResultLoaderMap();
+ Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
+ if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
+ final MetaObject metaObject = configuration.newMetaObject(rowValue);
+ boolean foundValues = this.useConstructorMappings;
+ if (shouldApplyAutomaticMappings(resultMap, false)) {
+ 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;
+ }
+ return rowValue;
+ }
+
+ //
+ // GET VALUE FROM ROW FOR NESTED RESULT MAP
+ //
+
+ private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix,
+ Object partialObject) throws SQLException {
+ final String resultMapId = resultMap.getId();
+ Object rowValue = partialObject;
+ if (rowValue != null) {
+ final MetaObject metaObject = configuration.newMetaObject(rowValue);
+ putAncestor(rowValue, resultMapId);
+ applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);
+ ancestorObjects.remove(resultMapId);
+ } else {
+ final ResultLoaderMap lazyLoader = new ResultLoaderMap();
+ rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
+ if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
+ final MetaObject metaObject = configuration.newMetaObject(rowValue);
+ boolean foundValues = this.useConstructorMappings;
+ if (shouldApplyAutomaticMappings(resultMap, true)) {
+ foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
+ }
+ foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
+ putAncestor(rowValue, resultMapId);
+ foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true)
+ || foundValues;
+ ancestorObjects.remove(resultMapId);
+ foundValues = lazyLoader.size() > 0 || foundValues;
+ rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
+ }
+ if (combinedKey != CacheKey.NULL_CACHE_KEY) {
+ nestedResultObjects.put(combinedKey, rowValue);
+ }
+ }
+ return rowValue;
+ }
+
+ private void putAncestor(Object resultObject, String resultMapId) {
+ ancestorObjects.put(resultMapId, resultObject);
+ }
+
+ private boolean shouldApplyAutomaticMappings(ResultMap resultMap, boolean isNested) {
+ if (resultMap.getAutoMapping() != null) {
+ return resultMap.getAutoMapping();
+ }
+ if (isNested) {
+ return AutoMappingBehavior.FULL == configuration.getAutoMappingBehavior();
+ } else {
+ return AutoMappingBehavior.NONE != configuration.getAutoMappingBehavior();
+ }
+ }
+
+ //
+ // PROPERTY MAPPINGS
+ //
+
+ private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
+ ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
+ final List mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
+ boolean foundValues = false;
+ final List propertyMappings = resultMap.getPropertyResultMappings();
+ for (ResultMapping propertyMapping : propertyMappings) {
+ String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
+ if (propertyMapping.getNestedResultMapId() != null) {
+ // the user added a column attribute to a nested result map, ignore it
+ column = null;
+ }
+ if (propertyMapping.isCompositeResult()
+ || column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))
+ || propertyMapping.getResultSet() != null) {
+ Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader,
+ columnPrefix);
+ // issue #541 make property optional
+ final String property = propertyMapping.getProperty();
+ if (property == null) {
+ continue;
+ }
+ if (value == DEFERRED) {
+ foundValues = true;
+ continue;
+ }
+ if (value != null) {
+ foundValues = true;
+ }
+ if (value != null
+ || configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive()) {
+ // gcode issue #377, call setter on nulls (value is not 'found')
+ metaObject.setValue(property, value);
+ }
+ }
+ }
+ return foundValues;
+ }
+
+ private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,
+ ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
+ if (propertyMapping.getNestedQueryId() != null) {
+ return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
+ }
+ if (propertyMapping.getResultSet() != null) {
+ addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK?
+ return DEFERRED;
+ } else {
+ final TypeHandler> typeHandler = propertyMapping.getTypeHandler();
+ final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
+ return typeHandler.getResult(rs, column);
+ }
+ }
+
+ private List createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap,
+ MetaObject metaObject, String columnPrefix) throws SQLException {
+ final String mapKey = resultMap.getId() + ":" + columnPrefix;
+ List autoMapping = autoMappingsCache.get(mapKey);
+ if (autoMapping == null) {
+ autoMapping = new ArrayList<>();
+ final List unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
+ // Remove the entry to release the memory
+ List mappedInConstructorAutoMapping = constructorAutoMappingColumns.remove(mapKey);
+ if (mappedInConstructorAutoMapping != null) {
+ unmappedColumnNames.removeAll(mappedInConstructorAutoMapping);
+ }
+ for (String columnName : unmappedColumnNames) {
+ String propertyName = columnName;
+ if (columnPrefix != null && !columnPrefix.isEmpty()) {
+ // When columnPrefix is specified,
+ // ignore columns without the prefix.
+ if (!columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
+ continue;
+ }
+ propertyName = columnName.substring(columnPrefix.length());
+ }
+ final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());
+ if (property != null && metaObject.hasSetter(property)) {
+ if (resultMap.getMappedProperties().contains(property)) {
+ continue;
+ }
+ final Class> propertyType = metaObject.getSetterType(property);
+ if (typeHandlerRegistry.hasTypeHandler(propertyType, rsw.getJdbcType(columnName))) {
+ final TypeHandler> typeHandler = rsw.getTypeHandler(propertyType, columnName);
+ autoMapping
+ .add(new UnMappedColumnAutoMapping(columnName, property, typeHandler, propertyType.isPrimitive()));
+ } else {
+ configuration.getAutoMappingUnknownColumnBehavior().doAction(mappedStatement, columnName, property,
+ propertyType);
+ }
+ } else {
+ configuration.getAutoMappingUnknownColumnBehavior().doAction(mappedStatement, columnName,
+ property != null ? property : propertyName, null);
+ }
+ }
+ autoMappingsCache.put(mapKey, autoMapping);
+ }
+ return autoMapping;
+ }
+
+ private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
+ String columnPrefix) throws SQLException {
+ List autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
+ boolean foundValues = false;
+ if (!autoMapping.isEmpty()) {
+ for (UnMappedColumnAutoMapping mapping : autoMapping) {
+ 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')
+ metaObject.setValue(mapping.property, value);
+ }
+ }
+ }
+ return foundValues;
+ }
+
+ // MULTIPLE RESULT SETS
+
+ private void linkToParents(ResultSet rs, ResultMapping parentMapping, Object rowValue) throws SQLException {
+ CacheKey parentKey = createKeyForMultipleResults(rs, parentMapping, parentMapping.getColumn(),
+ parentMapping.getForeignColumn());
+ List parents = pendingRelations.get(parentKey);
+ if (parents != null) {
+ for (PendingRelation parent : parents) {
+ if (parent != null && rowValue != null) {
+ linkObjects(parent.metaObject, parent.propertyMapping, rowValue);
+ }
+ }
+ }
+ }
+
+ private void addPendingChildRelation(ResultSet rs, MetaObject metaResultObject, ResultMapping parentMapping)
+ throws SQLException {
+ CacheKey cacheKey = createKeyForMultipleResults(rs, parentMapping, parentMapping.getColumn(),
+ parentMapping.getColumn());
+ PendingRelation deferLoad = new PendingRelation();
+ deferLoad.metaObject = metaResultObject;
+ deferLoad.propertyMapping = parentMapping;
+ List relations = MapUtil.computeIfAbsent(pendingRelations, cacheKey, k -> new ArrayList<>());
+ // issue #255
+ relations.add(deferLoad);
+ ResultMapping previous = nextResultMaps.get(parentMapping.getResultSet());
+ if (previous == null) {
+ nextResultMaps.put(parentMapping.getResultSet(), parentMapping);
+ } else if (!previous.equals(parentMapping)) {
+ throw new ExecutorException("Two different properties are mapped to the same resultSet");
+ }
+ }
+
+ private CacheKey createKeyForMultipleResults(ResultSet rs, ResultMapping resultMapping, String names, String columns)
+ throws SQLException {
+ CacheKey cacheKey = new CacheKey();
+ cacheKey.update(resultMapping);
+ if (columns != null && names != null) {
+ String[] columnsArray = columns.split(",");
+ String[] namesArray = names.split(",");
+ for (int i = 0; i < columnsArray.length; i++) {
+ Object value = rs.getString(columnsArray[i]);
+ if (value != null) {
+ cacheKey.update(namesArray[i]);
+ cacheKey.update(value);
+ }
+ }
+ }
+ return cacheKey;
+ }
+
+ //
+ // INSTANTIATION & CONSTRUCTOR MAPPING
+ //
+
+ private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader,
+ String columnPrefix) throws SQLException {
+ this.useConstructorMappings = false; // reset previous mapping result
+ final List> constructorArgTypes = new ArrayList<>();
+ final List constructorArgs = new ArrayList<>();
+ Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
+ if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
+ final List propertyMappings = resultMap.getPropertyResultMappings();
+ for (ResultMapping propertyMapping : propertyMappings) {
+ // issue gcode #109 && issue #149
+ if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
+ resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration,
+ objectFactory, constructorArgTypes, constructorArgs);
+ break;
+ }
+ }
+ }
+ this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
+ return resultObject;
+ }
+
+ private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List> constructorArgTypes,
+ List constructorArgs, String columnPrefix) throws SQLException {
+ final Class> resultType = resultMap.getType();
+ final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
+ final List constructorMappings = resultMap.getConstructorResultMappings();
+ if (hasTypeHandlerForResultObject(rsw, resultType)) {
+ return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
+ }
+ if (!constructorMappings.isEmpty()) {
+ return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs,
+ columnPrefix);
+ } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
+ return objectFactory.create(resultType);
+ } else if (shouldApplyAutomaticMappings(resultMap, false)) {
+ return createByConstructorSignature(rsw, resultMap, columnPrefix, resultType, constructorArgTypes,
+ constructorArgs);
+ }
+ throw new ExecutorException("Do not know how to create an instance of " + resultType);
+ }
+
+ Object createParameterizedResultObject(ResultSetWrapper rsw, Class> resultType,
+ List constructorMappings, List> constructorArgTypes, List constructorArgs,
+ String columnPrefix) {
+ boolean foundValues = false;
+ for (ResultMapping constructorMapping : constructorMappings) {
+ final Class> parameterType = constructorMapping.getJavaType();
+ final String column = constructorMapping.getColumn();
+ final Object value;
+ try {
+ if (constructorMapping.getNestedQueryId() != null) {
+ value = getNestedQueryConstructorValue(rsw.getResultSet(), constructorMapping, columnPrefix);
+ } else if (constructorMapping.getNestedResultMapId() != null) {
+ final ResultMap resultMap = configuration.getResultMap(constructorMapping.getNestedResultMapId());
+ value = getRowValue(rsw, resultMap, getColumnPrefix(columnPrefix, constructorMapping));
+ } else {
+ final TypeHandler> typeHandler = constructorMapping.getTypeHandler();
+ value = typeHandler.getResult(rsw.getResultSet(), prependPrefix(column, columnPrefix));
+ }
+ } catch (ResultMapException | SQLException e) {
+ throw new ExecutorException("Could not process result for mapping: " + constructorMapping, e);
+ }
+ constructorArgTypes.add(parameterType);
+ constructorArgs.add(value);
+ foundValues = value != null || foundValues;
+ }
+ return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
+ }
+
+ private Object createByConstructorSignature(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix,
+ Class> resultType, List> constructorArgTypes, List constructorArgs) throws SQLException {
+ return applyConstructorAutomapping(rsw, resultMap, columnPrefix, resultType, constructorArgTypes, constructorArgs,
+ findConstructorForAutomapping(resultType, rsw).orElseThrow(() -> new ExecutorException(
+ "No constructor found in " + resultType.getName() + " matching " + rsw.getClassNames())));
+ }
+
+ private Optional> findConstructorForAutomapping(final Class> resultType, ResultSetWrapper rsw) {
+ Constructor>[] constructors = resultType.getDeclaredConstructors();
+ if (constructors.length == 1) {
+ return Optional.of(constructors[0]);
+ }
+ Optional> annotated = Arrays.stream(constructors)
+ .filter(x -> x.isAnnotationPresent(AutomapConstructor.class)).reduce((x, y) -> {
+ throw new ExecutorException("@AutomapConstructor should be used in only one constructor.");
+ });
+ if (annotated.isPresent()) {
+ return annotated;
+ }
+ if (configuration.isArgNameBasedConstructorAutoMapping()) {
+ // Finding-best-match type implementation is possible,
+ // but using @AutomapConstructor seems sufficient.
+ throw new ExecutorException(MessageFormat.format(
+ "'argNameBasedConstructorAutoMapping' is enabled and the class ''{0}'' has multiple constructors, so @AutomapConstructor must be added to one of the constructors.",
+ resultType.getName()));
+ } else {
+ return Arrays.stream(constructors).filter(x -> findUsableConstructorByArgTypes(x, rsw.getJdbcTypes())).findAny();
+ }
+ }
+
+ private boolean findUsableConstructorByArgTypes(final Constructor> constructor, final List jdbcTypes) {
+ final Class>[] parameterTypes = constructor.getParameterTypes();
+ if (parameterTypes.length != jdbcTypes.size()) {
+ return false;
+ }
+ for (int i = 0; i < parameterTypes.length; i++) {
+ if (!typeHandlerRegistry.hasTypeHandler(parameterTypes[i], jdbcTypes.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private Object applyConstructorAutomapping(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix,
+ Class> resultType, List> constructorArgTypes, List constructorArgs, Constructor> constructor)
+ throws SQLException {
+ boolean foundValues = false;
+ if (configuration.isArgNameBasedConstructorAutoMapping()) {
+ foundValues = applyArgNameBasedConstructorAutoMapping(rsw, resultMap, columnPrefix, constructorArgTypes,
+ constructorArgs, constructor, foundValues);
+ } else {
+ foundValues = applyColumnOrderBasedConstructorAutomapping(rsw, constructorArgTypes, constructorArgs, constructor,
+ foundValues);
+ }
+ return foundValues || configuration.isReturnInstanceForEmptyRow()
+ ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
+ }
+
+ private boolean applyColumnOrderBasedConstructorAutomapping(ResultSetWrapper rsw, List> constructorArgTypes,
+ List constructorArgs, Constructor> constructor, boolean foundValues) throws SQLException {
+ for (int i = 0; i < constructor.getParameterTypes().length; i++) {
+ Class> parameterType = constructor.getParameterTypes()[i];
+ String columnName = rsw.getColumnNames().get(i);
+ TypeHandler> typeHandler = rsw.getTypeHandler(parameterType, columnName);
+ Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
+ constructorArgTypes.add(parameterType);
+ constructorArgs.add(value);
+ foundValues = value != null || foundValues;
+ }
+ return foundValues;
+ }
+
+ private boolean applyArgNameBasedConstructorAutoMapping(ResultSetWrapper rsw, ResultMap resultMap,
+ String columnPrefix, List> constructorArgTypes, List constructorArgs, Constructor> constructor,
+ boolean foundValues) throws SQLException {
+ List missingArgs = null;
+ Parameter[] params = constructor.getParameters();
+ for (Parameter param : params) {
+ boolean columnNotFound = true;
+ Param paramAnno = param.getAnnotation(Param.class);
+ String paramName = paramAnno == null ? param.getName() : paramAnno.value();
+ for (String columnName : rsw.getColumnNames()) {
+ if (columnMatchesParam(columnName, paramName, columnPrefix)) {
+ Class> paramType = param.getType();
+ TypeHandler> typeHandler = rsw.getTypeHandler(paramType, columnName);
+ Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
+ constructorArgTypes.add(paramType);
+ constructorArgs.add(value);
+ final String mapKey = resultMap.getId() + ":" + columnPrefix;
+ if (!autoMappingsCache.containsKey(mapKey)) {
+ MapUtil.computeIfAbsent(constructorAutoMappingColumns, mapKey, k -> new ArrayList<>()).add(columnName);
+ }
+ columnNotFound = false;
+ foundValues = value != null || foundValues;
+ }
+ }
+ if (columnNotFound) {
+ if (missingArgs == null) {
+ missingArgs = new ArrayList<>();
+ }
+ missingArgs.add(paramName);
+ }
+ }
+ if (foundValues && constructorArgs.size() < params.length) {
+ throw new ExecutorException(MessageFormat.format(
+ "Constructor auto-mapping of ''{1}'' failed " + "because ''{0}'' were not found in the result set; "
+ + "Available columns are ''{2}'' and mapUnderscoreToCamelCase is ''{3}''.",
+ missingArgs, constructor, rsw.getColumnNames(), configuration.isMapUnderscoreToCamelCase()));
+ }
+ return foundValues;
+ }
+
+ private boolean columnMatchesParam(String columnName, String paramName, String columnPrefix) {
+ if (columnPrefix != null) {
+ if (!columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
+ return false;
+ }
+ columnName = columnName.substring(columnPrefix.length());
+ }
+ return paramName
+ .equalsIgnoreCase(configuration.isMapUnderscoreToCamelCase() ? columnName.replace("_", "") : columnName);
+ }
+
+ protected Object createPrimitiveResultObject(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix)
+ throws SQLException {
+ final Class> resultType = resultMap.getType();
+ final String columnName;
+ if (!resultMap.getResultMappings().isEmpty()) {
+ final List resultMappingList = resultMap.getResultMappings();
+ final ResultMapping mapping = resultMappingList.get(0);
+ columnName = prependPrefix(mapping.getColumn(), columnPrefix);
+ } else {
+ columnName = rsw.getColumnNames().get(0);
+ }
+ final TypeHandler> typeHandler = rsw.getTypeHandler(resultType, columnName);
+ return typeHandler.getResult(rsw.getResultSet(), columnName);
+ }
+
+ //
+ // NESTED QUERY
+ //
+
+ private Object getNestedQueryConstructorValue(ResultSet rs, ResultMapping constructorMapping, String columnPrefix)
+ throws SQLException {
+ final String nestedQueryId = constructorMapping.getNestedQueryId();
+ final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
+ final Class> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
+ final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, constructorMapping,
+ nestedQueryParameterType, columnPrefix);
+ Object value = null;
+ if (nestedQueryParameterObject != null) {
+ final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
+ final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT,
+ nestedBoundSql);
+ final Class> targetType = constructorMapping.getJavaType();
+ final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery,
+ nestedQueryParameterObject, targetType, key, nestedBoundSql);
+ value = resultLoader.loadResult();
+ }
+ return value;
+ }
+
+ private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,
+ ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
+ final String nestedQueryId = propertyMapping.getNestedQueryId();
+ final String property = propertyMapping.getProperty();
+ final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
+ final Class> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
+ final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping,
+ nestedQueryParameterType, columnPrefix);
+ Object value = null;
+ if (nestedQueryParameterObject != null) {
+ final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
+ final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT,
+ nestedBoundSql);
+ final Class> targetType = propertyMapping.getJavaType();
+ if (executor.isCached(nestedQuery, key)) {
+ executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
+ value = DEFERRED;
+ } else {
+ final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery,
+ nestedQueryParameterObject, targetType, key, nestedBoundSql);
+ if (propertyMapping.isLazy()) {
+ lazyLoader.addLoader(property, metaResultObject, resultLoader);
+ value = DEFERRED;
+ } else {
+ value = resultLoader.loadResult();
+ }
+ }
+ }
+ return value;
+ }
+
+ private Object prepareParameterForNestedQuery(ResultSet rs, ResultMapping resultMapping, Class> parameterType,
+ String columnPrefix) throws SQLException {
+ if (resultMapping.isCompositeResult()) {
+ return prepareCompositeKeyParameter(rs, resultMapping, parameterType, columnPrefix);
+ }
+ return prepareSimpleKeyParameter(rs, resultMapping, parameterType, columnPrefix);
+ }
+
+ private Object prepareSimpleKeyParameter(ResultSet rs, ResultMapping resultMapping, Class> parameterType,
+ String columnPrefix) throws SQLException {
+ final TypeHandler> typeHandler;
+ if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
+ typeHandler = typeHandlerRegistry.getTypeHandler(parameterType);
+ } else {
+ typeHandler = typeHandlerRegistry.getUnknownTypeHandler();
+ }
+ return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
+ }
+
+ private Object prepareCompositeKeyParameter(ResultSet rs, ResultMapping resultMapping, Class> parameterType,
+ String columnPrefix) throws SQLException {
+ final Object parameterObject = instantiateParameterObject(parameterType);
+ final MetaObject metaObject = configuration.newMetaObject(parameterObject);
+ boolean foundValues = false;
+ for (ResultMapping innerResultMapping : resultMapping.getComposites()) {
+ final Class> propType = metaObject.getSetterType(innerResultMapping.getProperty());
+ final TypeHandler> typeHandler = typeHandlerRegistry.getTypeHandler(propType);
+ final Object propValue = typeHandler.getResult(rs, prependPrefix(innerResultMapping.getColumn(), columnPrefix));
+ // issue #353 & #560 do not execute nested query if key is null
+ if (propValue != null) {
+ metaObject.setValue(innerResultMapping.getProperty(), propValue);
+ foundValues = true;
+ }
+ }
+ return foundValues ? parameterObject : null;
+ }
+
+ private Object instantiateParameterObject(Class> parameterType) {
+ if (parameterType == null) {
+ return new HashMap<>();
+ }
+ if (ParamMap.class.equals(parameterType)) {
+ return new HashMap<>(); // issue #649
+ } else {
+ return objectFactory.create(parameterType);
+ }
+ }
+
+ //
+ // DISCRIMINATOR
+ //
+
+ @Override
+ public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix)
+ throws SQLException {
+ Set pastDiscriminators = new HashSet<>();
+ Discriminator discriminator = resultMap.getDiscriminator();
+ while (discriminator != null) {
+ final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix);
+ final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value));
+ if (!configuration.hasResultMap(discriminatedMapId)) {
+ break;
+ }
+ resultMap = configuration.getResultMap(discriminatedMapId);
+ Discriminator lastDiscriminator = discriminator;
+ discriminator = resultMap.getDiscriminator();
+ if (discriminator == lastDiscriminator || !pastDiscriminators.add(discriminatedMapId)) {
+ break;
+ }
+ }
+ return resultMap;
+ }
+
+ private Object getDiscriminatorValue(ResultSet rs, Discriminator discriminator, String columnPrefix)
+ throws SQLException {
+ final ResultMapping resultMapping = discriminator.getResultMapping();
+ final TypeHandler> typeHandler = resultMapping.getTypeHandler();
+ return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
+ }
+
+
+ protected String prependPrefix(String columnName, String prefix) {
+ if (columnName == null || columnName.length() == 0 || prefix == null || prefix.length() == 0) {
+ return columnName;
+ }
+ return prefix + columnName;
+ }
+
+ //
+ // HANDLE NESTED RESULT MAPS
+ //
+
+ private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap,
+ ResultHandler> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
+ final DefaultResultContext resultContext = new DefaultResultContext<>();
+ ResultSet resultSet = rsw.getResultSet();
+ skipRows(resultSet, rowBounds);
+ Object rowValue = previousRowValue;
+ while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
+ final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
+ final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
+ Object partialObject = nestedResultObjects.get(rowKey);
+ // issue #577 && #542
+ if (mappedStatement.isResultOrdered()) {
+ if (partialObject == null && rowValue != null) {
+ nestedResultObjects.clear();
+ storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
+ }
+ rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
+ } else {
+ rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
+ if (partialObject == null) {
+ storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
+ }
+ }
+ }
+ if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
+ storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
+ previousRowValue = null;
+ } else if (rowValue != null) {
+ previousRowValue = rowValue;
+ }
+ }
+
+ //
+ // NESTED RESULT MAP (JOIN MAPPING)
+ //
+
+ private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
+ String parentPrefix, CacheKey parentRowKey, boolean newObject) {
+ boolean foundValues = false;
+ for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {
+ final String nestedResultMapId = resultMapping.getNestedResultMapId();
+ if (nestedResultMapId != null && resultMapping.getResultSet() == null) {
+ try {
+ final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping);
+ final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);
+ if (resultMapping.getColumnPrefix() == null) {
+ // try to fill circular reference only when columnPrefix
+ // is not specified for the nested result map (issue #215)
+ Object ancestorObject = ancestorObjects.get(nestedResultMapId);
+ if (ancestorObject != null) {
+ if (newObject) {
+ linkObjects(metaObject, resultMapping, ancestorObject); // issue #385
+ }
+ continue;
+ }
+ }
+ final CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix);
+ final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);
+ Object rowValue = nestedResultObjects.get(combinedKey);
+ boolean knownValue = rowValue != null;
+ instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); // mandatory
+ if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) {
+ rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);
+ if (rowValue != null && !knownValue) {
+ linkObjects(metaObject, resultMapping, rowValue);
+ foundValues = true;
+ }
+ }
+ } catch (SQLException e) {
+ throw new ExecutorException(
+ "Error getting nested result map values for '" + resultMapping.getProperty() + "'. Cause: " + e, e);
+ }
+ }
+ }
+ return foundValues;
+ }
+
+ private String getColumnPrefix(String parentPrefix, ResultMapping resultMapping) {
+ final StringBuilder columnPrefixBuilder = new StringBuilder();
+ if (parentPrefix != null) {
+ columnPrefixBuilder.append(parentPrefix);
+ }
+ if (resultMapping.getColumnPrefix() != null) {
+ columnPrefixBuilder.append(resultMapping.getColumnPrefix());
+ }
+ return columnPrefixBuilder.length() == 0 ? null : columnPrefixBuilder.toString().toUpperCase(Locale.ENGLISH);
+ }
+
+ private boolean anyNotNullColumnHasValue(ResultMapping resultMapping, String columnPrefix, ResultSetWrapper rsw)
+ throws SQLException {
+ Set notNullColumns = resultMapping.getNotNullColumns();
+ if (notNullColumns != null && !notNullColumns.isEmpty()) {
+ ResultSet rs = rsw.getResultSet();
+ for (String column : notNullColumns) {
+ rs.getObject(prependPrefix(column, columnPrefix));
+ if (!rs.wasNull()) {
+ return true;
+ }
+ }
+ return false;
+ }
+ if (columnPrefix != null) {
+ for (String columnName : rsw.getColumnNames()) {
+ if (columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix.toUpperCase(Locale.ENGLISH))) {
+ return true;
+ }
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private ResultMap getNestedResultMap(ResultSet rs, String nestedResultMapId, String columnPrefix)
+ throws SQLException {
+ ResultMap nestedResultMap = configuration.getResultMap(nestedResultMapId);
+ return resolveDiscriminatedResultMap(rs, nestedResultMap, columnPrefix);
+ }
+
+ //
+ // UNIQUE RESULT KEY
+ //
+
+ private CacheKey createRowKey(ResultMap resultMap, ResultSetWrapper rsw, String columnPrefix) throws SQLException {
+ final CacheKey cacheKey = new CacheKey();
+ cacheKey.update(resultMap.getId());
+ List resultMappings = getResultMappingsForRowKey(resultMap);
+ if (resultMappings.isEmpty()) {
+ if (Map.class.isAssignableFrom(resultMap.getType())) {
+ createRowKeyForMap(rsw, cacheKey);
+ } else {
+ createRowKeyForUnmappedProperties(resultMap, rsw, cacheKey, columnPrefix);
+ }
+ } else {
+ createRowKeyForMappedProperties(resultMap, rsw, cacheKey, resultMappings, columnPrefix);
+ }
+ if (cacheKey.getUpdateCount() < 2) {
+ return CacheKey.NULL_CACHE_KEY;
+ }
+ return cacheKey;
+ }
+
+ private CacheKey combineKeys(CacheKey rowKey, CacheKey parentRowKey) {
+ if (rowKey.getUpdateCount() > 1 && parentRowKey.getUpdateCount() > 1) {
+ CacheKey combinedKey;
+ try {
+ combinedKey = rowKey.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new ExecutorException("Error cloning cache key. Cause: " + e, e);
+ }
+ combinedKey.update(parentRowKey);
+ return combinedKey;
+ }
+ return CacheKey.NULL_CACHE_KEY;
+ }
+
+ private List getResultMappingsForRowKey(ResultMap resultMap) {
+ List resultMappings = resultMap.getIdResultMappings();
+ if (resultMappings.isEmpty()) {
+ resultMappings = resultMap.getPropertyResultMappings();
+ }
+ return resultMappings;
+ }
+
+ private void createRowKeyForMappedProperties(ResultMap resultMap, ResultSetWrapper rsw, CacheKey cacheKey,
+ List resultMappings, String columnPrefix) throws SQLException {
+ for (ResultMapping resultMapping : resultMappings) {
+ if (resultMapping.isSimple()) {
+ final String column = prependPrefix(resultMapping.getColumn(), columnPrefix);
+ final TypeHandler> th = resultMapping.getTypeHandler();
+ List mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
+ // Issue #114
+ if (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))) {
+ final Object value = th.getResult(rsw.getResultSet(), column);
+ if (value != null || configuration.isReturnInstanceForEmptyRow()) {
+ cacheKey.update(column);
+ cacheKey.update(value);
+ }
+ }
+ }
+ }
+ }
+
+ private void createRowKeyForUnmappedProperties(ResultMap resultMap, ResultSetWrapper rsw, CacheKey cacheKey,
+ String columnPrefix) throws SQLException {
+ final MetaClass metaType = MetaClass.forClass(resultMap.getType(), reflectorFactory);
+ List unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
+ for (String column : unmappedColumnNames) {
+ String property = column;
+ if (columnPrefix != null && !columnPrefix.isEmpty()) {
+ // When columnPrefix is specified, ignore columns without the prefix.
+ if (!column.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
+ continue;
+ }
+ property = column.substring(columnPrefix.length());
+ }
+ if (metaType.findProperty(property, configuration.isMapUnderscoreToCamelCase()) != null) {
+ String value = rsw.getResultSet().getString(column);
+ if (value != null) {
+ cacheKey.update(column);
+ cacheKey.update(value);
+ }
+ }
+ }
+ }
+
+ private void createRowKeyForMap(ResultSetWrapper rsw, CacheKey cacheKey) throws SQLException {
+ List columnNames = rsw.getColumnNames();
+ for (String columnName : columnNames) {
+ final String value = rsw.getResultSet().getString(columnName);
+ if (value != null) {
+ cacheKey.update(columnName);
+ cacheKey.update(value);
+ }
+ }
+ }
+
+ private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue) {
+ final Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject);
+ if (collectionProperty != null) {
+ final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty);
+ targetMetaObject.add(rowValue);
+ } else {
+ metaObject.setValue(resultMapping.getProperty(), rowValue);
+ }
+ }
+
+ private Object instantiateCollectionPropertyIfAppropriate(ResultMapping resultMapping, MetaObject metaObject) {
+ final String propertyName = resultMapping.getProperty();
+ Object propertyValue = metaObject.getValue(propertyName);
+ if (propertyValue == null) {
+ Class> type = resultMapping.getJavaType();
+ if (type == null) {
+ type = metaObject.getSetterType(propertyName);
+ }
+ try {
+ if (objectFactory.isCollection(type)) {
+ propertyValue = objectFactory.create(type);
+ metaObject.setValue(propertyName, propertyValue);
+ return propertyValue;
+ }
+ } catch (Exception e) {
+ throw new ExecutorException(
+ "Error instantiating collection property for result '" + resultMapping.getProperty() + "'. Cause: " + e,
+ e);
+ }
+ } else if (objectFactory.isCollection(propertyValue.getClass())) {
+ return propertyValue;
+ }
+ return null;
+ }
+
+ private boolean hasTypeHandlerForResultObject(ResultSetWrapper rsw, Class> resultType) {
+ if (rsw.getColumnNames().size() == 1) {
+ return typeHandlerRegistry.hasTypeHandler(resultType, rsw.getJdbcType(rsw.getColumnNames().get(0)));
+ }
+ return typeHandlerRegistry.hasTypeHandler(resultType);
+ }
+
+}
diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexResultSetHandler.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexResultSetHandler.java
index a9f4ddc4..c04c4f17 100644
--- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexResultSetHandler.java
+++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/mybatis/FlexResultSetHandler.java
@@ -19,21 +19,26 @@ import com.mybatisflex.core.transaction.TransactionContext;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
-import org.apache.ibatis.executor.resultset.DefaultResultSetHandler;
+import org.apache.ibatis.executor.resultset.ResultSetWrapper;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.mapping.ResultMap;
+import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
+import org.apache.ibatis.type.TypeHandler;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
/**
* @author michael
- * 用于增强对 Cursor 查询处理
+ * 用于增强对 Cursor 查询处理,以及 List 的自动映射问题
*/
-public class FlexResultSetHandler extends DefaultResultSetHandler {
+public class FlexResultSetHandler extends FlexDefaultResultSetHandler {
public FlexResultSetHandler(Executor executor, MappedStatement mappedStatement, ParameterHandler parameterHandler
, ResultHandler> resultHandler, BoundSql boundSql, RowBounds rowBounds) {
@@ -57,6 +62,43 @@ public class FlexResultSetHandler extends DefaultResultSetHandler {
}
+ /**
+ * 修复当实体类中存在 List 或者 List 等自动映射出错的问题
+ * 本质问题应该出现 mybatis 判断有误
+ *
+ * https://gitee.com/mybatis-flex/mybatis-flex/issues/I7XBQS
+ * https://gitee.com/mybatis-flex/mybatis-flex/issues/I7X7G7
+ *
+ * @param rsw
+ * @param resultMap
+ * @param columnPrefix
+ * @throws SQLException
+ */
+ @Override
+ protected Object createPrimitiveResultObject(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix)
+ throws SQLException {
+ final Class> resultType = resultMap.getType();
+ final String columnName;
+ final TypeHandler> typeHandler;
+ if (!resultMap.getResultMappings().isEmpty()) {
+ final List resultMappingList = resultMap.getResultMappings();
+ final ResultMapping mapping = resultMappingList.get(0);
+ columnName = prependPrefix(mapping.getColumn(), columnPrefix);
+ typeHandler = mapping.getTypeHandler();
+ } else {
+ columnName = rsw.getColumnNames().get(0);
+ typeHandler = rsw.getTypeHandler(resultType, columnName);
+ }
+
+ List mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
+ if (columnName != null && mappedColumnNames.contains(columnName.toUpperCase(Locale.ENGLISH))) {
+ return typeHandler.getResult(rsw.getResultSet(), columnName);
+ }
+ return null;
+ }
+
+
+
static class FlexCursor implements Cursor {
private final Cursor originalCursor;
@@ -68,8 +110,7 @@ public class FlexResultSetHandler extends DefaultResultSetHandler {
@Override
public void close() {
- // do nothing
- // 由 TransactionContext 去关闭
+ // do nothing,由 TransactionContext 去关闭
}
@Override
diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfo.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfo.java
index ed87d6e8..2e8af126 100644
--- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfo.java
+++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/table/TableInfo.java
@@ -39,7 +39,6 @@ import org.apache.ibatis.reflection.Reflector;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandler;
-import org.apache.ibatis.type.UnknownTypeHandler;
import org.apache.ibatis.util.MapUtil;
import java.lang.reflect.Field;
@@ -657,7 +656,7 @@ public class TableInfo {
if (enumWrapper.hasEnumValueAnnotation()) {
value = enumWrapper.getEnumValue((Enum) value);
} else {
- value = ((Enum>)value).name();
+ value = ((Enum>) value).name();
}
}
}
@@ -1038,12 +1037,16 @@ public class TableInfo {
// List List 等
String columnName = TableInfoFactory.getColumnName(camelToUnderline, field, field.getAnnotation(Column.class));
// 映射
- String nestedResultMapId = entityClass.getName() + "." + field.getName();
+
ResultMapping resultMapping = new ResultMapping.Builder(configuration, null)
.column(columnName)
- .typeHandler(new UnknownTypeHandler(configuration))
+ .typeHandler(configuration.getTypeHandlerRegistry().getTypeHandler(genericClass))
.build();
- ResultMap nestedResultMap = new ResultMap.Builder(configuration, nestedResultMapId, genericClass, Collections.singletonList(resultMapping)).build();
+
+ String nestedResultMapId = entityClass.getName() + "." + field.getName();
+ ResultMap nestedResultMap = new ResultMap.Builder(configuration, nestedResultMapId, genericClass
+ , Collections.singletonList(resultMapping)).build();
+
configuration.addResultMap(nestedResultMap);
// 映射
resultMappings.add(new ResultMapping.Builder(configuration, field.getName())
diff --git a/mybatis-flex-test/mybatis-flex-native-test/src/main/java/com/mybatisflex/test/AccountDTO.java b/mybatis-flex-test/mybatis-flex-native-test/src/main/java/com/mybatisflex/test/AccountDTO.java
index 1505f680..779187da 100644
--- a/mybatis-flex-test/mybatis-flex-native-test/src/main/java/com/mybatisflex/test/AccountDTO.java
+++ b/mybatis-flex-test/mybatis-flex-native-test/src/main/java/com/mybatisflex/test/AccountDTO.java
@@ -48,6 +48,11 @@ public class AccountDTO {
private List articles;
+// @Column(ignore = true)
+ private List permissions;
+
+ private String testOtherField;
+
public Long getId() {
return id;
@@ -105,6 +110,32 @@ public class AccountDTO {
this.articles = articles;
}
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+
+ public String getTestOtherField() {
+ return testOtherField;
+ }
+
+ public void setTestOtherField(String testOtherField) {
+ this.testOtherField = testOtherField;
+ }
+
+ public List getPermissions() {
+ return permissions;
+ }
+
+ public void setPermissions(List permissions) {
+ this.permissions = permissions;
+ }
+
+
+
@Override
public String toString() {
return "AccountDTO{" +
@@ -115,7 +146,7 @@ public class AccountDTO {
", options=" + options +
", isDelete=" + isDelete +
", articles=" + articles +
+ ", permissions=" + permissions +
'}';
}
-
}
diff --git a/mybatis-flex-test/mybatis-flex-native-test/src/main/java/com/mybatisflex/test/AccountTester.java b/mybatis-flex-test/mybatis-flex-native-test/src/main/java/com/mybatisflex/test/AccountTester.java
index adf8f126..b5f738e0 100644
--- a/mybatis-flex-test/mybatis-flex-native-test/src/main/java/com/mybatisflex/test/AccountTester.java
+++ b/mybatis-flex-test/mybatis-flex-native-test/src/main/java/com/mybatisflex/test/AccountTester.java
@@ -178,7 +178,12 @@ public class AccountTester {
@Test
public void testSelectAsToDTO() {
- List accountDTOS = accountMapper.selectListByQueryAs(QueryWrapper.create(), AccountDTO.class);
+ QueryWrapper queryWrapper = QueryWrapper.create();
+// queryWrapper.select(ACCOUNT.ALL_COLUMNS,ARTICLE.TITLE.as(AccountDTO::getPermissions))
+ queryWrapper.select(ACCOUNT.ALL_COLUMNS,ACCOUNT.USER_NAME.as(AccountDTO::getTestOtherField))
+// queryWrapper.select(ACCOUNT.ALL_COLUMNS)
+ .from(ACCOUNT).leftJoin(ARTICLE).on(ACCOUNT.ID.eq(ARTICLE.ACCOUNT_ID));
+ List accountDTOS = accountMapper.selectListByQueryAs(queryWrapper, AccountDTO.class);
System.out.println(accountDTOS);
}