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