001/*
002 *  Copyright (c) 2022-2025, Mybatis-Flex (fuhai999@gmail.com).
003 *  <p>
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *  <p>
008 *  http://www.apache.org/licenses/LICENSE-2.0
009 *  <p>
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package com.mybatisflex.core.mybatis;
017
018import com.mybatisflex.core.FlexGlobalConfig;
019import com.mybatisflex.core.util.MapUtil;
020import org.apache.ibatis.annotations.AutomapConstructor;
021import org.apache.ibatis.annotations.Param;
022import org.apache.ibatis.binding.MapperMethod.ParamMap;
023import org.apache.ibatis.cache.CacheKey;
024import org.apache.ibatis.cursor.Cursor;
025import org.apache.ibatis.cursor.defaults.DefaultCursor;
026import org.apache.ibatis.executor.ErrorContext;
027import org.apache.ibatis.executor.Executor;
028import org.apache.ibatis.executor.ExecutorException;
029import org.apache.ibatis.executor.loader.ResultLoader;
030import org.apache.ibatis.executor.loader.ResultLoaderMap;
031import org.apache.ibatis.executor.parameter.ParameterHandler;
032import org.apache.ibatis.executor.result.DefaultResultContext;
033import org.apache.ibatis.executor.result.DefaultResultHandler;
034import org.apache.ibatis.executor.result.ResultMapException;
035import org.apache.ibatis.executor.resultset.DefaultResultSetHandler;
036import org.apache.ibatis.executor.resultset.ResultSetWrapper;
037import org.apache.ibatis.mapping.*;
038import org.apache.ibatis.reflection.MetaClass;
039import org.apache.ibatis.reflection.MetaObject;
040import org.apache.ibatis.reflection.ReflectorFactory;
041import org.apache.ibatis.reflection.factory.ObjectFactory;
042import org.apache.ibatis.session.*;
043import org.apache.ibatis.type.JdbcType;
044import org.apache.ibatis.type.TypeHandler;
045import org.apache.ibatis.type.TypeHandlerRegistry;
046import org.apache.ibatis.type.UnknownTypeHandler;
047
048import java.lang.reflect.Constructor;
049import java.lang.reflect.Executable;
050import java.lang.reflect.Parameter;
051import java.sql.CallableStatement;
052import java.sql.ResultSet;
053import java.sql.SQLException;
054import java.sql.Statement;
055import java.text.MessageFormat;
056import java.util.*;
057
058/**
059 * 复制于 DefaultResultSetHandler,并开放若干方法,方便子类重写
060 *
061 * @author Clinton Begin
062 * @author Eduardo Macarron
063 * @author Iwao AVE!
064 * @author Kazuki Shimizu
065 * @author Michael
066 */
067public class FlexDefaultResultSetHandler extends DefaultResultSetHandler {
068
069    private static final Object DEFERRED = new Object();
070
071    private final Executor executor;
072    private final Configuration configuration;
073    private final MappedStatement mappedStatement;
074    private final RowBounds rowBounds;
075    private final ParameterHandler parameterHandler;
076    private final ResultHandler<?> resultHandler;
077    private final BoundSql boundSql;
078    private final TypeHandlerRegistry typeHandlerRegistry;
079    private final ObjectFactory objectFactory;
080    private final ReflectorFactory reflectorFactory;
081
082    // nested resultmaps
083    private final Map<CacheKey, Object> nestedResultObjects = new HashMap<>();
084    private final Map<String, Object> ancestorObjects = new HashMap<>();
085    private Object previousRowValue;
086
087    // multiple resultsets
088    private final Map<String, ResultMapping> nextResultMaps = new HashMap<>();
089    private final Map<CacheKey, List<PendingRelation>> pendingRelations = new HashMap<>();
090
091    // Cached Automappings
092    private final Map<String, List<UnMappedColumnAutoMapping>> autoMappingsCache = new HashMap<>();
093    private final Map<String, List<String>> constructorAutoMappingColumns = new HashMap<>();
094
095    // temporary marking flag that indicate using constructor mapping (use field to reduce memory usage)
096    private boolean useConstructorMappings;
097
098    private static class PendingRelation {
099        public MetaObject metaObject;
100        public ResultMapping propertyMapping;
101    }
102
103    private static class UnMappedColumnAutoMapping {
104        private final String column;
105        private final String property;
106        private final TypeHandler<?> typeHandler;
107        private final boolean primitive;
108
109        public UnMappedColumnAutoMapping(String column, String property, TypeHandler<?> typeHandler, boolean primitive) {
110            this.column = column;
111            this.property = property;
112            this.typeHandler = typeHandler;
113            this.primitive = primitive;
114        }
115    }
116
117    public FlexDefaultResultSetHandler(Executor executor, MappedStatement mappedStatement, ParameterHandler parameterHandler,
118                                       ResultHandler<?> resultHandler, BoundSql boundSql, RowBounds rowBounds) {
119        super(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
120        this.executor = executor;
121        this.configuration = mappedStatement.getConfiguration();
122        this.mappedStatement = mappedStatement;
123        this.rowBounds = rowBounds;
124        this.parameterHandler = parameterHandler;
125        this.boundSql = boundSql;
126        this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
127        this.objectFactory = configuration.getObjectFactory();
128        this.reflectorFactory = configuration.getReflectorFactory();
129        this.resultHandler = resultHandler;
130    }
131
132    //
133    // HANDLE OUTPUT PARAMETER
134    //
135
136    @Override
137    public void handleOutputParameters(CallableStatement cs) throws SQLException {
138        final Object parameterObject = parameterHandler.getParameterObject();
139        final MetaObject metaParam = configuration.newMetaObject(parameterObject);
140        final List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
141        for (int i = 0; i < parameterMappings.size(); i++) {
142            final ParameterMapping parameterMapping = parameterMappings.get(i);
143            if (parameterMapping.getMode() == ParameterMode.OUT || parameterMapping.getMode() == ParameterMode.INOUT) {
144                if (ResultSet.class.equals(parameterMapping.getJavaType())) {
145                    handleRefCursorOutputParameter((ResultSet) cs.getObject(i + 1), parameterMapping, metaParam);
146                } else {
147                    final TypeHandler<?> typeHandler = parameterMapping.getTypeHandler();
148                    metaParam.setValue(parameterMapping.getProperty(), typeHandler.getResult(cs, i + 1));
149                }
150            }
151        }
152    }
153
154    private void handleRefCursorOutputParameter(ResultSet rs, ParameterMapping parameterMapping, MetaObject metaParam)
155        throws SQLException {
156        if (rs == null) {
157            return;
158        }
159        try {
160            final String resultMapId = parameterMapping.getResultMapId();
161            final ResultMap resultMap = configuration.getResultMap(resultMapId);
162            final ResultSetWrapper rsw = new ResultSetWrapper(rs, configuration);
163            if (this.resultHandler == null) {
164                final DefaultResultHandler resultHandler = new DefaultResultHandler(objectFactory);
165                handleRowValues(rsw, resultMap, resultHandler, new RowBounds(), null);
166                metaParam.setValue(parameterMapping.getProperty(), resultHandler.getResultList());
167            } else {
168                handleRowValues(rsw, resultMap, resultHandler, new RowBounds(), null);
169            }
170        } finally {
171            // issue #228 (close resultsets)
172            closeResultSet(rs);
173        }
174    }
175
176    //
177    // HANDLE RESULT SETS
178    //
179    @Override
180    public List<Object> handleResultSets(Statement stmt) throws SQLException {
181        ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
182
183        final List<Object> multipleResults = new ArrayList<>();
184
185        int resultSetCount = 0;
186        ResultSetWrapper rsw = getFirstResultSet(stmt);
187
188        List<ResultMap> resultMaps = mappedStatement.getResultMaps();
189        int resultMapCount = resultMaps.size();
190        validateResultMapsCount(rsw, resultMapCount);
191        while (rsw != null && resultMapCount > resultSetCount) {
192            ResultMap resultMap = resultMaps.get(resultSetCount);
193            handleResultSet(rsw, resultMap, multipleResults, null);
194            rsw = getNextResultSet(stmt);
195            cleanUpAfterHandlingResultSet();
196            resultSetCount++;
197        }
198
199        String[] resultSets = mappedStatement.getResultSets();
200        if (resultSets != null) {
201            while (rsw != null && resultSetCount < resultSets.length) {
202                ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
203                if (parentMapping != null) {
204                    String nestedResultMapId = parentMapping.getNestedResultMapId();
205                    ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
206                    handleResultSet(rsw, resultMap, null, parentMapping);
207                }
208                rsw = getNextResultSet(stmt);
209                cleanUpAfterHandlingResultSet();
210                resultSetCount++;
211            }
212        }
213
214        return collapseSingleResultList(multipleResults);
215    }
216
217    @Override
218    public <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException {
219        ErrorContext.instance().activity("handling cursor results").object(mappedStatement.getId());
220
221        ResultSetWrapper rsw = getFirstResultSet(stmt);
222
223        List<ResultMap> resultMaps = mappedStatement.getResultMaps();
224
225        int resultMapCount = resultMaps.size();
226        validateResultMapsCount(rsw, resultMapCount);
227        if (resultMapCount != 1) {
228            throw new ExecutorException("Cursor results cannot be mapped to multiple resultMaps");
229        }
230
231        ResultMap resultMap = resultMaps.get(0);
232        return new DefaultCursor<>(this, resultMap, rsw, rowBounds);
233    }
234
235    private ResultSetWrapper getFirstResultSet(Statement stmt) throws SQLException {
236        ResultSet rs = stmt.getResultSet();
237        while (rs == null) {
238            // move forward to get the first resultset in case the driver
239            // doesn't return the resultset as the first result (HSQLDB 2.1)
240            if (stmt.getMoreResults()) {
241                rs = stmt.getResultSet();
242            } else if (stmt.getUpdateCount() == -1) {
243                // no more results. Must be no resultset
244                break;
245            }
246        }
247        return rs != null ? new ResultSetWrapper(rs, configuration) : null;
248    }
249
250    private ResultSetWrapper getNextResultSet(Statement stmt) {
251        // Making this method tolerant of bad JDBC drivers
252        try {
253            // Crazy Standard JDBC way of determining if there are more results
254            if (stmt.getConnection().getMetaData().supportsMultipleResultSets()
255                && (stmt.getMoreResults() || (stmt.getUpdateCount() != -1))) {
256                ResultSet rs = stmt.getResultSet();
257                if (rs == null) {
258                    return getNextResultSet(stmt);
259                }
260                return new ResultSetWrapper(rs, configuration);
261            }
262        } catch (Exception e) {
263            // Intentionally ignored.
264        }
265        return null;
266    }
267
268    private void closeResultSet(ResultSet rs) {
269        try {
270            if (rs != null) {
271                rs.close();
272            }
273        } catch (SQLException e) {
274            // ignore
275        }
276    }
277
278    private void cleanUpAfterHandlingResultSet() {
279        nestedResultObjects.clear();
280    }
281
282    private void validateResultMapsCount(ResultSetWrapper rsw, int resultMapCount) {
283        if (rsw != null && resultMapCount < 1) {
284            throw new ExecutorException(
285                "A query was run and no Result Maps were found for the Mapped Statement '" + mappedStatement.getId()
286                    + "'. 'resultType' or 'resultMap' must be specified when there is no corresponding method.");
287        }
288    }
289
290    private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults,
291                                 ResultMapping parentMapping) throws SQLException {
292        try {
293            if (parentMapping != null) {
294                handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
295            } else if (resultHandler == null) {
296                if (resultMap.getId().startsWith("com.mybatisflex.core.row.RowMapper.selectFirstAndSecondColumnsAsMap")) {
297                    ResultSet resultSet = rsw.getResultSet();
298                    skipRows(resultSet, rowBounds);
299                    Map<Object, Object> row = new HashMap<>();
300                    while (!resultSet.isClosed() && resultSet.next()) {
301                        row.put(resultSet.getObject(1), resultSet.getObject(2));
302                    }
303                    List<Map<Object, Object>> mapArrayList = new ArrayList<>(1);
304                    mapArrayList.add(row);
305                    multipleResults.add(mapArrayList);
306                } else {
307                    DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
308                    handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
309                    multipleResults.add(defaultResultHandler.getResultList());
310                }
311            } else {
312                handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
313            }
314        } finally {
315            // issue #228 (close resultsets)
316            closeResultSet(rsw.getResultSet());
317        }
318    }
319
320    @SuppressWarnings("unchecked")
321    private List<Object> collapseSingleResultList(List<Object> multipleResults) {
322        return multipleResults.size() == 1 ? (List<Object>) multipleResults.get(0) : multipleResults;
323    }
324
325    //
326    // HANDLE ROWS FOR SIMPLE RESULTMAP
327    //
328
329    @Override
330    public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler,
331                                RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
332        if (resultMap.hasNestedResultMaps()) {
333            ensureNoRowBounds();
334            checkResultHandler();
335            handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
336        } else {
337            handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
338        }
339    }
340
341    private void ensureNoRowBounds() {
342        if (configuration.isSafeRowBoundsEnabled() && rowBounds != null
343            && (rowBounds.getLimit() < RowBounds.NO_ROW_LIMIT || rowBounds.getOffset() > RowBounds.NO_ROW_OFFSET)) {
344            throw new ExecutorException(
345                "Mapped Statements with nested result mappings cannot be safely constrained by RowBounds. "
346                    + "Use safeRowBoundsEnabled=false setting to bypass this check.");
347        }
348    }
349
350    @Override
351    protected void checkResultHandler() {
352        if (resultHandler != null && configuration.isSafeResultHandlerEnabled() && !mappedStatement.isResultOrdered()) {
353            throw new ExecutorException(
354                "Mapped Statements with nested result mappings cannot be safely used with a custom ResultHandler. "
355                    + "Use safeResultHandlerEnabled=false setting to bypass this check "
356                    + "or ensure your statement returns ordered data and set resultOrdered=true on it.");
357        }
358    }
359
360    private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap,
361                                                   ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
362        DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
363        ResultSet resultSet = rsw.getResultSet();
364        skipRows(resultSet, rowBounds);
365        while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
366            ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
367            Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
368            storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
369        }
370    }
371
372    private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue,
373                             ResultMapping parentMapping, ResultSet rs) throws SQLException {
374        if (parentMapping != null) {
375            linkToParents(rs, parentMapping, rowValue);
376        } else {
377            callResultHandler(resultHandler, resultContext, rowValue);
378        }
379    }
380
381    @SuppressWarnings("unchecked" /* because ResultHandler<?> is always ResultHandler<Object> */)
382    private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext,
383                                   Object rowValue) {
384        resultContext.nextResultObject(rowValue);
385        ((ResultHandler<Object>) resultHandler).handleResult(resultContext);
386    }
387
388    private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) {
389        return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
390    }
391
392    private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
393        if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
394            if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
395                rs.absolute(rowBounds.getOffset());
396            }
397        } else {
398            for (int i = 0; i < rowBounds.getOffset(); i++) {
399                if (!rs.next()) {
400                    break;
401                }
402            }
403        }
404    }
405
406    //
407    // GET VALUE FROM ROW FOR SIMPLE RESULT MAP
408    //
409
410    private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
411        final ResultLoaderMap lazyLoader = new ResultLoaderMap();
412        Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
413        if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
414            final MetaObject metaObject = configuration.newMetaObject(rowValue);
415            boolean foundValues = this.useConstructorMappings;
416            if (shouldApplyAutomaticMappings(resultMap, false)) {
417                foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
418            }
419            foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
420            foundValues = lazyLoader.size() > 0 || foundValues;
421            rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
422        }
423        return rowValue;
424    }
425
426    //
427    // GET VALUE FROM ROW FOR NESTED RESULT MAP
428    //
429
430    private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix,
431                               Object partialObject) throws SQLException {
432        final String resultMapId = resultMap.getId();
433        Object rowValue = partialObject;
434        if (rowValue != null) {
435            final MetaObject metaObject = configuration.newMetaObject(rowValue);
436            putAncestor(rowValue, resultMapId);
437            applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);
438            ancestorObjects.remove(resultMapId);
439        } else {
440            final ResultLoaderMap lazyLoader = new ResultLoaderMap();
441            rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
442            if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
443                final MetaObject metaObject = configuration.newMetaObject(rowValue);
444                boolean foundValues = this.useConstructorMappings;
445                if (shouldApplyAutomaticMappings(resultMap, true)) {
446                    foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
447                }
448                foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
449                putAncestor(rowValue, resultMapId);
450                foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true)
451                    || foundValues;
452                ancestorObjects.remove(resultMapId);
453                foundValues = lazyLoader.size() > 0 || foundValues;
454                rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
455            }
456            if (combinedKey != CacheKey.NULL_CACHE_KEY) {
457                nestedResultObjects.put(combinedKey, rowValue);
458            }
459        }
460        return rowValue;
461    }
462
463    private void putAncestor(Object resultObject, String resultMapId) {
464        ancestorObjects.put(resultMapId, resultObject);
465    }
466
467    private boolean shouldApplyAutomaticMappings(ResultMap resultMap, boolean isNested) {
468        if (resultMap.getAutoMapping() != null) {
469            return resultMap.getAutoMapping();
470        }
471        if (isNested) {
472            return AutoMappingBehavior.FULL == configuration.getAutoMappingBehavior();
473        } else {
474            return AutoMappingBehavior.NONE != configuration.getAutoMappingBehavior();
475        }
476    }
477
478    //
479    // PROPERTY MAPPINGS
480    //
481
482    private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
483                                          ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
484        final Collection<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
485        boolean foundValues = false;
486        final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
487        for (ResultMapping propertyMapping : propertyMappings) {
488            String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
489            if (propertyMapping.getNestedResultMapId() != null) {
490                // the user added a column attribute to a nested result map, ignore it
491                column = null;
492            }
493            if (propertyMapping.isCompositeResult()
494                || column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))
495                || propertyMapping.getResultSet() != null) {
496                Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader,
497                    columnPrefix);
498                // issue #541 make property optional
499                final String property = propertyMapping.getProperty();
500                if (property == null) {
501                    continue;
502                }
503                if (value == DEFERRED) {
504                    foundValues = true;
505                    continue;
506                }
507                if (value != null) {
508                    foundValues = true;
509                }
510                if (value != null
511                    || configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive()) {
512                    // gcode issue #377, call setter on nulls (value is not 'found')
513                    metaObject.setValue(property, value);
514                }
515            }
516        }
517        return foundValues;
518    }
519
520    private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,
521                                           ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
522        if (propertyMapping.getNestedQueryId() != null) {
523            return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
524        }
525        if (propertyMapping.getResultSet() != null) {
526            addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK?
527            return DEFERRED;
528        } else {
529            final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
530            final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
531            return typeHandler.getResult(rs, column);
532        }
533    }
534
535    private List<UnMappedColumnAutoMapping> createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap,
536                                                                    MetaObject metaObject, String columnPrefix) throws SQLException {
537        final String mapKey = resultMap.getId() + ":" + columnPrefix;
538        List<UnMappedColumnAutoMapping> autoMapping = autoMappingsCache.get(mapKey);
539        if (autoMapping == null) {
540            autoMapping = new ArrayList<>();
541            final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
542            // Remove the entry to release the memory
543            List<String> mappedInConstructorAutoMapping = constructorAutoMappingColumns.remove(mapKey);
544            if (mappedInConstructorAutoMapping != null) {
545                unmappedColumnNames.removeAll(mappedInConstructorAutoMapping);
546            }
547            for (String columnName : unmappedColumnNames) {
548                String propertyName = columnName;
549                if (columnPrefix != null && !columnPrefix.isEmpty()) {
550                    // When columnPrefix is specified,
551                    // ignore columns without the prefix.
552                    if (!columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
553                        continue;
554                    }
555                    propertyName = columnName.substring(columnPrefix.length());
556                }
557                final String property = metaObject.findProperty(propertyName, configuration.isMapUnderscoreToCamelCase());
558                if (property != null && metaObject.hasSetter(property)) {
559                    if (resultMap.getMappedProperties().contains(property)) {
560                        continue;
561                    }
562                    final Class<?> propertyType = metaObject.getSetterType(property);
563                    if (typeHandlerRegistry.hasTypeHandler(propertyType, rsw.getJdbcType(columnName))) {
564                        final TypeHandler<?> typeHandler = rsw.getTypeHandler(propertyType, columnName);
565                        autoMapping
566                            .add(new UnMappedColumnAutoMapping(columnName, property, typeHandler, propertyType.isPrimitive()));
567                    } else {
568                        configuration.getAutoMappingUnknownColumnBehavior().doAction(mappedStatement, columnName, property,
569                            propertyType);
570                    }
571                } else {
572                    configuration.getAutoMappingUnknownColumnBehavior().doAction(mappedStatement, columnName,
573                        property != null ? property : propertyName, null);
574                }
575            }
576            autoMappingsCache.put(mapKey, autoMapping);
577        }
578        return autoMapping;
579    }
580
581    private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
582                                           String columnPrefix) throws SQLException {
583        List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
584        boolean foundValues = false;
585        if (!autoMapping.isEmpty()) {
586            for (UnMappedColumnAutoMapping mapping : autoMapping) {
587                final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
588                if (value != null) {
589                    foundValues = true;
590                }
591                if (value != null || configuration.isCallSettersOnNulls() && !mapping.primitive) {
592                    // gcode issue #377, call setter on nulls (value is not 'found')
593                    metaObject.setValue(mapping.property, value);
594                }
595            }
596        } else {
597            if (FlexGlobalConfig.getUnMappedColumnHandler() != null) {
598                // 增加未匹配列自定义处理
599                final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
600                for (String unmappedColumnName : unmappedColumnNames) {
601                    // 不明确类型,直接取object
602                    final Object value = typeHandlerRegistry.getMappingTypeHandler(UnknownTypeHandler.class).getResult(rsw.getResultSet(), unmappedColumnName);
603                    // 自定义处理未匹配列
604                    FlexGlobalConfig.getUnMappedColumnHandler().handleUnMappedColumn(metaObject, unmappedColumnName, value);
605                }
606            }
607        }
608        return foundValues;
609    }
610
611    // MULTIPLE RESULT SETS
612
613    private void linkToParents(ResultSet rs, ResultMapping parentMapping, Object rowValue) throws SQLException {
614        CacheKey parentKey = createKeyForMultipleResults(rs, parentMapping, parentMapping.getColumn(),
615            parentMapping.getForeignColumn());
616        List<PendingRelation> parents = pendingRelations.get(parentKey);
617        if (parents != null) {
618            for (PendingRelation parent : parents) {
619                if (parent != null && rowValue != null) {
620                    linkObjects(parent.metaObject, parent.propertyMapping, rowValue);
621                }
622            }
623        }
624    }
625
626    private void addPendingChildRelation(ResultSet rs, MetaObject metaResultObject, ResultMapping parentMapping)
627        throws SQLException {
628        CacheKey cacheKey = createKeyForMultipleResults(rs, parentMapping, parentMapping.getColumn(),
629            parentMapping.getColumn());
630        PendingRelation deferLoad = new PendingRelation();
631        deferLoad.metaObject = metaResultObject;
632        deferLoad.propertyMapping = parentMapping;
633        List<PendingRelation> relations = MapUtil.computeIfAbsent(pendingRelations, cacheKey, k -> new ArrayList<>());
634        // issue #255
635        relations.add(deferLoad);
636        ResultMapping previous = nextResultMaps.get(parentMapping.getResultSet());
637        if (previous == null) {
638            nextResultMaps.put(parentMapping.getResultSet(), parentMapping);
639        } else if (!previous.equals(parentMapping)) {
640            throw new ExecutorException("Two different properties are mapped to the same resultSet");
641        }
642    }
643
644    private CacheKey createKeyForMultipleResults(ResultSet rs, ResultMapping resultMapping, String names, String columns)
645        throws SQLException {
646        CacheKey cacheKey = new CacheKey();
647        cacheKey.update(resultMapping);
648        if (columns != null && names != null) {
649            String[] columnsArray = columns.split(",");
650            String[] namesArray = names.split(",");
651            for (int i = 0; i < columnsArray.length; i++) {
652                Object value = rs.getString(columnsArray[i]);
653                if (value != null) {
654                    cacheKey.update(namesArray[i]);
655                    cacheKey.update(value);
656                }
657            }
658        }
659        return cacheKey;
660    }
661
662    //
663    // INSTANTIATION & CONSTRUCTOR MAPPING
664    //
665
666    private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader,
667                                      String columnPrefix) throws SQLException {
668        this.useConstructorMappings = false; // reset previous mapping result
669        final List<Class<?>> constructorArgTypes = new ArrayList<>();
670        final List<Object> constructorArgs = new ArrayList<>();
671        Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
672        if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
673            final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
674            for (ResultMapping propertyMapping : propertyMappings) {
675                // issue gcode #109 && issue #149
676                if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
677                    resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration,
678                        objectFactory, constructorArgTypes, constructorArgs);
679                    break;
680                }
681            }
682        }
683        this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
684        return resultObject;
685    }
686
687    private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes,
688                                      List<Object> constructorArgs, String columnPrefix) throws SQLException {
689        final Class<?> resultType = resultMap.getType();
690        final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
691        final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
692        if (hasTypeHandlerForResultObject(rsw, resultType)) {
693            return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
694        }
695        if (!constructorMappings.isEmpty()) {
696            return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs,
697                columnPrefix);
698        } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
699            return objectFactory.create(resultType);
700        } else if (shouldApplyAutomaticMappings(resultMap, false)) {
701            return createByConstructorSignature(rsw, resultMap, columnPrefix, resultType, constructorArgTypes,
702                constructorArgs);
703        }
704        throw new ExecutorException("Do not know how to create an instance of " + resultType);
705    }
706
707    Object createParameterizedResultObject(ResultSetWrapper rsw, Class<?> resultType,
708                                           List<ResultMapping> constructorMappings, List<Class<?>> constructorArgTypes, List<Object> constructorArgs,
709                                           String columnPrefix) {
710        boolean foundValues = false;
711        for (ResultMapping constructorMapping : constructorMappings) {
712            final Class<?> parameterType = constructorMapping.getJavaType();
713            final String column = constructorMapping.getColumn();
714            final Object value;
715            try {
716                if (constructorMapping.getNestedQueryId() != null) {
717                    value = getNestedQueryConstructorValue(rsw.getResultSet(), constructorMapping, columnPrefix);
718                } else if (constructorMapping.getNestedResultMapId() != null) {
719                    final ResultMap resultMap = configuration.getResultMap(constructorMapping.getNestedResultMapId());
720                    value = getRowValue(rsw, resultMap, getColumnPrefix(columnPrefix, constructorMapping));
721                } else {
722                    final TypeHandler<?> typeHandler = constructorMapping.getTypeHandler();
723                    value = typeHandler.getResult(rsw.getResultSet(), prependPrefix(column, columnPrefix));
724                }
725            } catch (ResultMapException | SQLException e) {
726                throw new ExecutorException("Could not process result for mapping: " + constructorMapping, e);
727            }
728            constructorArgTypes.add(parameterType);
729            constructorArgs.add(value);
730            foundValues = value != null || foundValues;
731        }
732        return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
733    }
734
735    private Object createByConstructorSignature(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix,
736                                                Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) throws SQLException {
737        return applyConstructorAutomapping(rsw, resultMap, columnPrefix, resultType, constructorArgTypes, constructorArgs,
738            findConstructorForAutomapping(resultType, rsw).orElseThrow(() -> new ExecutorException(
739                "No constructor found in " + resultType.getName() + " matching " + rsw.getClassNames())));
740    }
741
742    private Optional<Constructor<?>> findConstructorForAutomapping(final Class<?> resultType, ResultSetWrapper rsw) {
743        Constructor<?>[] constructors = resultType.getDeclaredConstructors();
744        if (constructors.length == 1) {
745            return Optional.of(constructors[0]);
746        }
747        Optional<Constructor<?>> annotated = Arrays.stream(constructors)
748            .filter(x -> x.isAnnotationPresent(AutomapConstructor.class)).reduce((x, y) -> {
749                throw new ExecutorException("@AutomapConstructor should be used in only one constructor.");
750            });
751        if (annotated.isPresent()) {
752            return annotated;
753        }
754        if (configuration.isArgNameBasedConstructorAutoMapping()) {
755            // Finding-best-match type implementation is possible,
756            // but using @AutomapConstructor seems sufficient.
757            throw new ExecutorException(MessageFormat.format(
758                "'argNameBasedConstructorAutoMapping' is enabled and the class ''{0}'' has multiple constructors, so @AutomapConstructor must be added to one of the constructors.",
759                resultType.getName()));
760        } else {
761            return Arrays.stream(constructors).filter(x -> findUsableConstructorByArgTypes(x, rsw.getJdbcTypes())).findAny();
762        }
763    }
764
765    private boolean findUsableConstructorByArgTypes(final Constructor<?> constructor, final List<JdbcType> jdbcTypes) {
766        final Class<?>[] parameterTypes = constructor.getParameterTypes();
767        if (parameterTypes.length != jdbcTypes.size()) {
768            return false;
769        }
770        for (int i = 0; i < parameterTypes.length; i++) {
771            if (!typeHandlerRegistry.hasTypeHandler(parameterTypes[i], jdbcTypes.get(i))) {
772                return false;
773            }
774        }
775        return true;
776    }
777
778    private Object applyConstructorAutomapping(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix,
779                                               Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor)
780        throws SQLException {
781        boolean foundValues = false;
782        if (configuration.isArgNameBasedConstructorAutoMapping()) {
783            foundValues = applyArgNameBasedConstructorAutoMapping(rsw, resultMap, columnPrefix, constructorArgTypes,
784                constructorArgs, constructor, foundValues);
785        } else {
786            foundValues = applyColumnOrderBasedConstructorAutomapping(rsw, constructorArgTypes, constructorArgs, constructor,
787                foundValues);
788        }
789        return foundValues || configuration.isReturnInstanceForEmptyRow()
790            ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
791    }
792
793    private boolean applyColumnOrderBasedConstructorAutomapping(ResultSetWrapper rsw, List<Class<?>> constructorArgTypes,
794                                                                List<Object> constructorArgs, Constructor<?> constructor, boolean foundValues) throws SQLException {
795
796        // fixed IndexOutOfBoundsException https://gitee.com/mybatis-flex/mybatis-flex/issues/I98ZO9
797        List<String> columnNames = rsw.getColumnNames();
798        if (columnNames.size() < constructor.getParameterCount()) {
799            throw new IllegalArgumentException("Can not invoke the constructor[" + buildMethodString(constructor) + "] with value names: "
800                + Arrays.toString(columnNames.toArray()) + ",\n"
801                + "Perhaps you can add a default (no parameters) constructor to fix it."
802            );
803        }
804
805        Class<?>[] parameterTypes = constructor.getParameterTypes();
806        for (int i = 0; i < parameterTypes.length; i++) {
807            Class<?> parameterType = parameterTypes[i];
808            String columnName = columnNames.get(i);
809            TypeHandler<?> typeHandler = rsw.getTypeHandler(parameterType, columnName);
810            Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
811            constructorArgTypes.add(parameterType);
812            constructorArgs.add(value);
813            foundValues = value != null || foundValues;
814        }
815        return foundValues;
816    }
817
818
819    private static String buildMethodString(Executable method) {
820        StringBuilder sb = new StringBuilder()
821            .append(method.getDeclaringClass().getName())
822            .append(".")
823            .append(method.getName())
824            .append("(");
825
826        Class<?>[] params = method.getParameterTypes();
827        int in = 0;
828        for (Class<?> clazz : params) {
829            sb.append(clazz.getName());
830            if (++in < params.length) {
831                sb.append(",");
832            }
833        }
834
835        return sb.append(")").toString();
836    }
837
838
839    private boolean applyArgNameBasedConstructorAutoMapping(ResultSetWrapper rsw, ResultMap resultMap,
840                                                            String columnPrefix, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor,
841                                                            boolean foundValues) throws SQLException {
842        List<String> missingArgs = null;
843        Parameter[] params = constructor.getParameters();
844        for (Parameter param : params) {
845            boolean columnNotFound = true;
846            Param paramAnno = param.getAnnotation(Param.class);
847            String paramName = paramAnno == null ? param.getName() : paramAnno.value();
848            for (String columnName : rsw.getColumnNames()) {
849                if (columnMatchesParam(columnName, paramName, columnPrefix)) {
850                    Class<?> paramType = param.getType();
851                    TypeHandler<?> typeHandler = rsw.getTypeHandler(paramType, columnName);
852                    Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
853                    constructorArgTypes.add(paramType);
854                    constructorArgs.add(value);
855                    final String mapKey = resultMap.getId() + ":" + columnPrefix;
856                    if (!autoMappingsCache.containsKey(mapKey)) {
857                        MapUtil.computeIfAbsent(constructorAutoMappingColumns, mapKey, k -> new ArrayList<>()).add(columnName);
858                    }
859                    columnNotFound = false;
860                    foundValues = value != null || foundValues;
861                }
862            }
863            if (columnNotFound) {
864                if (missingArgs == null) {
865                    missingArgs = new ArrayList<>();
866                }
867                missingArgs.add(paramName);
868            }
869        }
870        if (foundValues && constructorArgs.size() < params.length) {
871            throw new ExecutorException(MessageFormat.format(
872                "Constructor auto-mapping of ''{1}'' failed " + "because ''{0}'' were not found in the result set; "
873                    + "Available columns are ''{2}'' and mapUnderscoreToCamelCase is ''{3}''.",
874                missingArgs, constructor, rsw.getColumnNames(), configuration.isMapUnderscoreToCamelCase()));
875        }
876        return foundValues;
877    }
878
879    private boolean columnMatchesParam(String columnName, String paramName, String columnPrefix) {
880        if (columnPrefix != null) {
881            if (!columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
882                return false;
883            }
884            columnName = columnName.substring(columnPrefix.length());
885        }
886        return paramName
887            .equalsIgnoreCase(configuration.isMapUnderscoreToCamelCase() ? columnName.replace("_", "") : columnName);
888    }
889
890    protected Object createPrimitiveResultObject(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix)
891        throws SQLException {
892        final Class<?> resultType = resultMap.getType();
893        final String columnName;
894        if (!resultMap.getResultMappings().isEmpty()) {
895            final List<ResultMapping> resultMappingList = resultMap.getResultMappings();
896            final ResultMapping mapping = resultMappingList.get(0);
897            columnName = prependPrefix(mapping.getColumn(), columnPrefix);
898        } else {
899            columnName = rsw.getColumnNames().get(0);
900        }
901        final TypeHandler<?> typeHandler = rsw.getTypeHandler(resultType, columnName);
902        return typeHandler.getResult(rsw.getResultSet(), columnName);
903    }
904
905    //
906    // NESTED QUERY
907    //
908
909    private Object getNestedQueryConstructorValue(ResultSet rs, ResultMapping constructorMapping, String columnPrefix)
910        throws SQLException {
911        final String nestedQueryId = constructorMapping.getNestedQueryId();
912        final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
913        final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
914        final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, constructorMapping,
915            nestedQueryParameterType, columnPrefix);
916        Object value = null;
917        if (nestedQueryParameterObject != null) {
918            final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
919            final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT,
920                nestedBoundSql);
921            final Class<?> targetType = constructorMapping.getJavaType();
922            final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery,
923                nestedQueryParameterObject, targetType, key, nestedBoundSql);
924            value = resultLoader.loadResult();
925        }
926        return value;
927    }
928
929    private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,
930                                              ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
931        final String nestedQueryId = propertyMapping.getNestedQueryId();
932        final String property = propertyMapping.getProperty();
933        final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
934        final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
935        final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping,
936            nestedQueryParameterType, columnPrefix);
937        Object value = null;
938        if (nestedQueryParameterObject != null) {
939            final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
940            final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT,
941                nestedBoundSql);
942            final Class<?> targetType = propertyMapping.getJavaType();
943            if (executor.isCached(nestedQuery, key)) {
944                executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
945                value = DEFERRED;
946            } else {
947                final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery,
948                    nestedQueryParameterObject, targetType, key, nestedBoundSql);
949                if (propertyMapping.isLazy()) {
950                    lazyLoader.addLoader(property, metaResultObject, resultLoader);
951                    value = DEFERRED;
952                } else {
953                    value = resultLoader.loadResult();
954                }
955            }
956        }
957        return value;
958    }
959
960    private Object prepareParameterForNestedQuery(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType,
961                                                  String columnPrefix) throws SQLException {
962        if (resultMapping.isCompositeResult()) {
963            return prepareCompositeKeyParameter(rs, resultMapping, parameterType, columnPrefix);
964        }
965        return prepareSimpleKeyParameter(rs, resultMapping, parameterType, columnPrefix);
966    }
967
968    private Object prepareSimpleKeyParameter(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType,
969                                             String columnPrefix) throws SQLException {
970        final TypeHandler<?> typeHandler;
971        if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
972            typeHandler = typeHandlerRegistry.getTypeHandler(parameterType);
973        } else {
974            typeHandler = typeHandlerRegistry.getUnknownTypeHandler();
975        }
976        return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
977    }
978
979    private Object prepareCompositeKeyParameter(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType,
980                                                String columnPrefix) throws SQLException {
981        final Object parameterObject = instantiateParameterObject(parameterType);
982        final MetaObject metaObject = configuration.newMetaObject(parameterObject);
983        boolean foundValues = false;
984        for (ResultMapping innerResultMapping : resultMapping.getComposites()) {
985            final Class<?> propType = metaObject.getSetterType(innerResultMapping.getProperty());
986            final TypeHandler<?> typeHandler = typeHandlerRegistry.getTypeHandler(propType);
987            final Object propValue = typeHandler.getResult(rs, prependPrefix(innerResultMapping.getColumn(), columnPrefix));
988            // issue #353 & #560 do not execute nested query if key is null
989            if (propValue != null) {
990                metaObject.setValue(innerResultMapping.getProperty(), propValue);
991                foundValues = true;
992            }
993        }
994        return foundValues ? parameterObject : null;
995    }
996
997    private Object instantiateParameterObject(Class<?> parameterType) {
998        if (parameterType == null) {
999            return new HashMap<>();
1000        }
1001        if (ParamMap.class.equals(parameterType)) {
1002            return new HashMap<>(); // issue #649
1003        } else {
1004            return objectFactory.create(parameterType);
1005        }
1006    }
1007
1008    //
1009    // DISCRIMINATOR
1010    //
1011
1012    @Override
1013    public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix)
1014        throws SQLException {
1015        Set<String> pastDiscriminators = new HashSet<>();
1016        Discriminator discriminator = resultMap.getDiscriminator();
1017        while (discriminator != null) {
1018            final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix);
1019            final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value));
1020            if (!configuration.hasResultMap(discriminatedMapId)) {
1021                break;
1022            }
1023            resultMap = configuration.getResultMap(discriminatedMapId);
1024            Discriminator lastDiscriminator = discriminator;
1025            discriminator = resultMap.getDiscriminator();
1026            if (discriminator == lastDiscriminator || !pastDiscriminators.add(discriminatedMapId)) {
1027                break;
1028            }
1029        }
1030        return resultMap;
1031    }
1032
1033    private Object getDiscriminatorValue(ResultSet rs, Discriminator discriminator, String columnPrefix)
1034        throws SQLException {
1035        final ResultMapping resultMapping = discriminator.getResultMapping();
1036        final TypeHandler<?> typeHandler = resultMapping.getTypeHandler();
1037        return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
1038    }
1039
1040
1041    protected String prependPrefix(String columnName, String prefix) {
1042        if (columnName == null || columnName.length() == 0 || prefix == null || prefix.length() == 0) {
1043            return columnName;
1044        }
1045        return prefix + columnName;
1046    }
1047
1048    //
1049    // HANDLE NESTED RESULT MAPS
1050    //
1051
1052    private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap,
1053                                                   ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
1054        final DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
1055        ResultSet resultSet = rsw.getResultSet();
1056        skipRows(resultSet, rowBounds);
1057        Object rowValue = previousRowValue;
1058        while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
1059            final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
1060            final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
1061            Object partialObject = nestedResultObjects.get(rowKey);
1062            // issue #577 && #542
1063            if (mappedStatement.isResultOrdered()) {
1064                if (partialObject == null && rowValue != null) {
1065                    nestedResultObjects.clear();
1066                    storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
1067                }
1068                rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
1069            } else {
1070                rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
1071                if (partialObject == null) {
1072                    storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
1073                }
1074            }
1075        }
1076        if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
1077            storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
1078            previousRowValue = null;
1079        } else if (rowValue != null) {
1080            previousRowValue = rowValue;
1081        }
1082    }
1083
1084    //
1085    // NESTED RESULT MAP (JOIN MAPPING)
1086    //
1087
1088    private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
1089                                              String parentPrefix, CacheKey parentRowKey, boolean newObject) {
1090        boolean foundValues = false;
1091        for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {
1092            final String nestedResultMapId = resultMapping.getNestedResultMapId();
1093            if (nestedResultMapId != null && resultMapping.getResultSet() == null) {
1094                try {
1095                    final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping);
1096                    final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);
1097                    if (resultMapping.getColumnPrefix() == null) {
1098                        // try to fill circular reference only when columnPrefix
1099                        // is not specified for the nested result map (issue #215)
1100                        Object ancestorObject = ancestorObjects.get(nestedResultMapId);
1101                        if (ancestorObject != null) {
1102                            if (newObject) {
1103                                linkObjects(metaObject, resultMapping, ancestorObject); // issue #385
1104                            }
1105                            continue;
1106                        }
1107                    }
1108                    final CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix);
1109                    final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);
1110                    Object rowValue = nestedResultObjects.get(combinedKey);
1111                    boolean knownValue = rowValue != null;
1112                    instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); // mandatory
1113                    if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) {
1114                        rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);
1115                        if (rowValue != null && !knownValue) {
1116                            linkObjects(metaObject, resultMapping, rowValue);
1117                            foundValues = true;
1118                        }
1119                    }
1120                } catch (SQLException e) {
1121                    throw new ExecutorException(
1122                        "Error getting nested result map values for '" + resultMapping.getProperty() + "'.  Cause: " + e, e);
1123                }
1124            }
1125        }
1126        return foundValues;
1127    }
1128
1129    private String getColumnPrefix(String parentPrefix, ResultMapping resultMapping) {
1130        final StringBuilder columnPrefixBuilder = new StringBuilder();
1131        if (parentPrefix != null) {
1132            columnPrefixBuilder.append(parentPrefix);
1133        }
1134        if (resultMapping.getColumnPrefix() != null) {
1135            columnPrefixBuilder.append(resultMapping.getColumnPrefix());
1136        }
1137        return columnPrefixBuilder.length() == 0 ? null : columnPrefixBuilder.toString().toUpperCase(Locale.ENGLISH);
1138    }
1139
1140    private boolean anyNotNullColumnHasValue(ResultMapping resultMapping, String columnPrefix, ResultSetWrapper rsw)
1141        throws SQLException {
1142        Set<String> notNullColumns = resultMapping.getNotNullColumns();
1143        if (notNullColumns != null && !notNullColumns.isEmpty()) {
1144            ResultSet rs = rsw.getResultSet();
1145            for (String column : notNullColumns) {
1146                rs.getObject(prependPrefix(column, columnPrefix));
1147                if (!rs.wasNull()) {
1148                    return true;
1149                }
1150            }
1151            return false;
1152        }
1153        if (columnPrefix != null) {
1154            for (String columnName : rsw.getColumnNames()) {
1155                if (columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix.toUpperCase(Locale.ENGLISH))) {
1156                    return true;
1157                }
1158            }
1159            return false;
1160        }
1161        return true;
1162    }
1163
1164    private ResultMap getNestedResultMap(ResultSet rs, String nestedResultMapId, String columnPrefix)
1165        throws SQLException {
1166        ResultMap nestedResultMap = configuration.getResultMap(nestedResultMapId);
1167        return resolveDiscriminatedResultMap(rs, nestedResultMap, columnPrefix);
1168    }
1169
1170    //
1171    // UNIQUE RESULT KEY
1172    //
1173
1174    private CacheKey createRowKey(ResultMap resultMap, ResultSetWrapper rsw, String columnPrefix) throws SQLException {
1175        final CacheKey cacheKey = new CacheKey();
1176        cacheKey.update(resultMap.getId());
1177        List<ResultMapping> resultMappings = getResultMappingsForRowKey(resultMap);
1178        if (resultMappings.isEmpty()) {
1179            if (Map.class.isAssignableFrom(resultMap.getType())) {
1180                createRowKeyForMap(rsw, cacheKey);
1181            } else {
1182                createRowKeyForUnmappedProperties(resultMap, rsw, cacheKey, columnPrefix);
1183            }
1184        } else {
1185            createRowKeyForMappedProperties(resultMap, rsw, cacheKey, resultMappings, columnPrefix);
1186        }
1187        if (cacheKey.getUpdateCount() < 2) {
1188            return CacheKey.NULL_CACHE_KEY;
1189        }
1190        return cacheKey;
1191    }
1192
1193    private CacheKey combineKeys(CacheKey rowKey, CacheKey parentRowKey) {
1194        if (rowKey.getUpdateCount() > 1 && parentRowKey.getUpdateCount() > 1) {
1195            CacheKey combinedKey;
1196            try {
1197                combinedKey = rowKey.clone();
1198            } catch (CloneNotSupportedException e) {
1199                throw new ExecutorException("Error cloning cache key.  Cause: " + e, e);
1200            }
1201            combinedKey.update(parentRowKey);
1202            return combinedKey;
1203        }
1204        return CacheKey.NULL_CACHE_KEY;
1205    }
1206
1207    private List<ResultMapping> getResultMappingsForRowKey(ResultMap resultMap) {
1208        List<ResultMapping> resultMappings = resultMap.getIdResultMappings();
1209        if (resultMappings.isEmpty()) {
1210            resultMappings = resultMap.getPropertyResultMappings();
1211        }
1212        return resultMappings;
1213    }
1214
1215    private void createRowKeyForMappedProperties(ResultMap resultMap, ResultSetWrapper rsw, CacheKey cacheKey,
1216                                                 List<ResultMapping> resultMappings, String columnPrefix) throws SQLException {
1217        for (ResultMapping resultMapping : resultMappings) {
1218            if (resultMapping.isSimple()) {
1219                final String column = prependPrefix(resultMapping.getColumn(), columnPrefix);
1220                final TypeHandler<?> th = resultMapping.getTypeHandler();
1221                Collection<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
1222                // Issue #114
1223                if (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))) {
1224                    final Object value = th.getResult(rsw.getResultSet(), column);
1225                    if (value != null || configuration.isReturnInstanceForEmptyRow()) {
1226                        cacheKey.update(column);
1227                        cacheKey.update(value);
1228                    }
1229                }
1230            }
1231        }
1232    }
1233
1234    private void createRowKeyForUnmappedProperties(ResultMap resultMap, ResultSetWrapper rsw, CacheKey cacheKey,
1235                                                   String columnPrefix) throws SQLException {
1236        final MetaClass metaType = MetaClass.forClass(resultMap.getType(), reflectorFactory);
1237        List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
1238        for (String column : unmappedColumnNames) {
1239            String property = column;
1240            if (columnPrefix != null && !columnPrefix.isEmpty()) {
1241                // When columnPrefix is specified, ignore columns without the prefix.
1242                if (!column.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
1243                    continue;
1244                }
1245                property = column.substring(columnPrefix.length());
1246            }
1247            if (metaType.findProperty(property, configuration.isMapUnderscoreToCamelCase()) != null) {
1248                String value = rsw.getResultSet().getString(column);
1249                if (value != null) {
1250                    cacheKey.update(column);
1251                    cacheKey.update(value);
1252                }
1253            }
1254        }
1255    }
1256
1257    private void createRowKeyForMap(ResultSetWrapper rsw, CacheKey cacheKey) throws SQLException {
1258        List<String> columnNames = rsw.getColumnNames();
1259        for (String columnName : columnNames) {
1260            final String value = rsw.getResultSet().getString(columnName);
1261            if (value != null) {
1262                cacheKey.update(columnName);
1263                cacheKey.update(value);
1264            }
1265        }
1266    }
1267
1268    private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue) {
1269        final Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject);
1270        if (collectionProperty != null) {
1271            final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty);
1272            targetMetaObject.add(rowValue);
1273        } else {
1274            metaObject.setValue(resultMapping.getProperty(), rowValue);
1275        }
1276    }
1277
1278    private Object instantiateCollectionPropertyIfAppropriate(ResultMapping resultMapping, MetaObject metaObject) {
1279        final String propertyName = resultMapping.getProperty();
1280        Object propertyValue = metaObject.getValue(propertyName);
1281        if (propertyValue == null) {
1282            Class<?> type = resultMapping.getJavaType();
1283            if (type == null) {
1284                type = metaObject.getSetterType(propertyName);
1285            }
1286            try {
1287                if (objectFactory.isCollection(type)) {
1288                    propertyValue = objectFactory.create(type);
1289                    metaObject.setValue(propertyName, propertyValue);
1290                    return propertyValue;
1291                }
1292            } catch (Exception e) {
1293                throw new ExecutorException(
1294                    "Error instantiating collection property for result '" + resultMapping.getProperty() + "'.  Cause: " + e,
1295                    e);
1296            }
1297        } else if (objectFactory.isCollection(propertyValue.getClass())) {
1298            return propertyValue;
1299        }
1300        return null;
1301    }
1302
1303    private boolean hasTypeHandlerForResultObject(ResultSetWrapper rsw, Class<?> resultType) {
1304        if (rsw.getColumnNames().size() == 1) {
1305            return typeHandlerRegistry.hasTypeHandler(resultType, rsw.getJdbcType(rsw.getColumnNames().get(0)));
1306        }
1307        return typeHandlerRegistry.hasTypeHandler(resultType);
1308    }
1309
1310}