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