View Javadoc
1   /*
2    *    Copyright 2009-2024 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)
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 'improve' 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 Set<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     Class<?>[] parameterTypes = constructor.getParameterTypes();
786     for (int i = 0; i < parameterTypes.length; i++) {
787       Class<?> parameterType = parameterTypes[i];
788       String columnName = rsw.getColumnNames().get(i);
789       TypeHandler<?> typeHandler = rsw.getTypeHandler(parameterType, columnName);
790       Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
791       constructorArgTypes.add(parameterType);
792       constructorArgs.add(value);
793       foundValues = value != null || foundValues;
794     }
795     return foundValues;
796   }
797 
798   private boolean applyArgNameBasedConstructorAutoMapping(ResultSetWrapper rsw, ResultMap resultMap,
799       String columnPrefix, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor,
800       boolean foundValues) throws SQLException {
801     List<String> missingArgs = null;
802     Parameter[] params = constructor.getParameters();
803     for (Parameter param : params) {
804       boolean columnNotFound = true;
805       Param paramAnno = param.getAnnotation(Param.class);
806       String paramName = paramAnno == null ? param.getName() : paramAnno.value();
807       for (String columnName : rsw.getColumnNames()) {
808         if (columnMatchesParam(columnName, paramName, columnPrefix)) {
809           Class<?> paramType = param.getType();
810           TypeHandler<?> typeHandler = rsw.getTypeHandler(paramType, columnName);
811           Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
812           constructorArgTypes.add(paramType);
813           constructorArgs.add(value);
814           final String mapKey = resultMap.getId() + ":" + columnPrefix;
815           if (!autoMappingsCache.containsKey(mapKey)) {
816             MapUtil.computeIfAbsent(constructorAutoMappingColumns, mapKey, k -> new ArrayList<>()).add(columnName);
817           }
818           columnNotFound = false;
819           foundValues = value != null || foundValues;
820         }
821       }
822       if (columnNotFound) {
823         if (missingArgs == null) {
824           missingArgs = new ArrayList<>();
825         }
826         missingArgs.add(paramName);
827       }
828     }
829     if (foundValues && constructorArgs.size() < params.length) {
830       throw new ExecutorException(MessageFormat.format(
831           "Constructor auto-mapping of ''{1}'' failed " + "because ''{0}'' were not found in the result set; "
832               + "Available columns are ''{2}'' and mapUnderscoreToCamelCase is ''{3}''.",
833           missingArgs, constructor, rsw.getColumnNames(), configuration.isMapUnderscoreToCamelCase()));
834     }
835     return foundValues;
836   }
837 
838   private boolean columnMatchesParam(String columnName, String paramName, String columnPrefix) {
839     if (columnPrefix != null) {
840       if (!columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
841         return false;
842       }
843       columnName = columnName.substring(columnPrefix.length());
844     }
845     return paramName
846         .equalsIgnoreCase(configuration.isMapUnderscoreToCamelCase() ? columnName.replace("_", "") : columnName);
847   }
848 
849   private Object createPrimitiveResultObject(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix)
850       throws SQLException {
851     final Class<?> resultType = resultMap.getType();
852     final String columnName;
853     if (!resultMap.getResultMappings().isEmpty()) {
854       final List<ResultMapping> resultMappingList = resultMap.getResultMappings();
855       final ResultMapping mapping = resultMappingList.get(0);
856       columnName = prependPrefix(mapping.getColumn(), columnPrefix);
857     } else {
858       columnName = rsw.getColumnNames().get(0);
859     }
860     final TypeHandler<?> typeHandler = rsw.getTypeHandler(resultType, columnName);
861     return typeHandler.getResult(rsw.getResultSet(), columnName);
862   }
863 
864   //
865   // NESTED QUERY
866   //
867 
868   private Object getNestedQueryConstructorValue(ResultSet rs, ResultMapping constructorMapping, String columnPrefix)
869       throws SQLException {
870     final String nestedQueryId = constructorMapping.getNestedQueryId();
871     final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
872     final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
873     final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, constructorMapping,
874         nestedQueryParameterType, columnPrefix);
875     Object value = null;
876     if (nestedQueryParameterObject != null) {
877       final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
878       final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT,
879           nestedBoundSql);
880       final Class<?> targetType = constructorMapping.getJavaType();
881       final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery,
882           nestedQueryParameterObject, targetType, key, nestedBoundSql);
883       value = resultLoader.loadResult();
884     }
885     return value;
886   }
887 
888   private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping,
889       ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
890     final String nestedQueryId = propertyMapping.getNestedQueryId();
891     final String property = propertyMapping.getProperty();
892     final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
893     final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
894     final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping,
895         nestedQueryParameterType, columnPrefix);
896     Object value = null;
897     if (nestedQueryParameterObject != null) {
898       final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
899       final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT,
900           nestedBoundSql);
901       final Class<?> targetType = propertyMapping.getJavaType();
902       if (executor.isCached(nestedQuery, key)) {
903         executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
904         value = DEFERRED;
905       } else {
906         final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery,
907             nestedQueryParameterObject, targetType, key, nestedBoundSql);
908         if (propertyMapping.isLazy()) {
909           lazyLoader.addLoader(property, metaResultObject, resultLoader);
910           value = DEFERRED;
911         } else {
912           value = resultLoader.loadResult();
913         }
914       }
915     }
916     return value;
917   }
918 
919   private Object prepareParameterForNestedQuery(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType,
920       String columnPrefix) throws SQLException {
921     if (resultMapping.isCompositeResult()) {
922       return prepareCompositeKeyParameter(rs, resultMapping, parameterType, columnPrefix);
923     }
924     return prepareSimpleKeyParameter(rs, resultMapping, parameterType, columnPrefix);
925   }
926 
927   private Object prepareSimpleKeyParameter(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType,
928       String columnPrefix) throws SQLException {
929     final TypeHandler<?> typeHandler;
930     if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
931       typeHandler = typeHandlerRegistry.getTypeHandler(parameterType);
932     } else {
933       typeHandler = typeHandlerRegistry.getUnknownTypeHandler();
934     }
935     return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
936   }
937 
938   private Object prepareCompositeKeyParameter(ResultSet rs, ResultMapping resultMapping, Class<?> parameterType,
939       String columnPrefix) throws SQLException {
940     final Object parameterObject = instantiateParameterObject(parameterType);
941     final MetaObject metaObject = configuration.newMetaObject(parameterObject);
942     boolean foundValues = false;
943     for (ResultMapping innerResultMapping : resultMapping.getComposites()) {
944       final Class<?> propType = metaObject.getSetterType(innerResultMapping.getProperty());
945       final TypeHandler<?> typeHandler = typeHandlerRegistry.getTypeHandler(propType);
946       final Object propValue = typeHandler.getResult(rs, prependPrefix(innerResultMapping.getColumn(), columnPrefix));
947       // issue #353 & #560 do not execute nested query if key is null
948       if (propValue != null) {
949         metaObject.setValue(innerResultMapping.getProperty(), propValue);
950         foundValues = true;
951       }
952     }
953     return foundValues ? parameterObject : null;
954   }
955 
956   private Object instantiateParameterObject(Class<?> parameterType) {
957     if (parameterType == null) {
958       return new HashMap<>();
959     }
960     if (ParamMap.class.equals(parameterType)) {
961       return new HashMap<>(); // issue #649
962     } else {
963       return objectFactory.create(parameterType);
964     }
965   }
966 
967   //
968   // DISCRIMINATOR
969   //
970 
971   public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix)
972       throws SQLException {
973     Set<String> pastDiscriminators = new HashSet<>();
974     Discriminator discriminator = resultMap.getDiscriminator();
975     while (discriminator != null) {
976       final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix);
977       final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value));
978       if (!configuration.hasResultMap(discriminatedMapId)) {
979         break;
980       }
981       resultMap = configuration.getResultMap(discriminatedMapId);
982       Discriminator lastDiscriminator = discriminator;
983       discriminator = resultMap.getDiscriminator();
984       if (discriminator == lastDiscriminator || !pastDiscriminators.add(discriminatedMapId)) {
985         break;
986       }
987     }
988     return resultMap;
989   }
990 
991   private Object getDiscriminatorValue(ResultSet rs, Discriminator discriminator, String columnPrefix)
992       throws SQLException {
993     final ResultMapping resultMapping = discriminator.getResultMapping();
994     final TypeHandler<?> typeHandler = resultMapping.getTypeHandler();
995     return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
996   }
997 
998   private String prependPrefix(String columnName, String prefix) {
999     if (columnName == null || columnName.length() == 0 || prefix == null || prefix.length() == 0) {
1000       return columnName;
1001     }
1002     return prefix + columnName;
1003   }
1004 
1005   //
1006   // HANDLE NESTED RESULT MAPS
1007   //
1008 
1009   private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap,
1010       ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
1011     final DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
1012     ResultSet resultSet = rsw.getResultSet();
1013     skipRows(resultSet, rowBounds);
1014     Object rowValue = previousRowValue;
1015     while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
1016       final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
1017       final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null);
1018       Object partialObject = nestedResultObjects.get(rowKey);
1019       // issue #577 && #542
1020       if (mappedStatement.isResultOrdered()) {
1021         if (partialObject == null && rowValue != null) {
1022           nestedResultObjects.clear();
1023           storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
1024         }
1025         rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
1026       } else {
1027         rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject);
1028         if (partialObject == null) {
1029           storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
1030         }
1031       }
1032     }
1033     if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) {
1034       storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
1035       previousRowValue = null;
1036     } else if (rowValue != null) {
1037       previousRowValue = rowValue;
1038     }
1039   }
1040 
1041   //
1042   // NESTED RESULT MAP (JOIN MAPPING)
1043   //
1044 
1045   private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject,
1046       String parentPrefix, CacheKey parentRowKey, boolean newObject) {
1047     boolean foundValues = false;
1048     for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {
1049       final String nestedResultMapId = resultMapping.getNestedResultMapId();
1050       if (nestedResultMapId != null && resultMapping.getResultSet() == null) {
1051         try {
1052           final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping);
1053           final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);
1054           if (resultMapping.getColumnPrefix() == null) {
1055             // try to fill circular reference only when columnPrefix
1056             // is not specified for the nested result map (issue #215)
1057             Object ancestorObject = ancestorObjects.get(nestedResultMapId);
1058             if (ancestorObject != null) {
1059               if (newObject) {
1060                 linkObjects(metaObject, resultMapping, ancestorObject); // issue #385
1061               }
1062               continue;
1063             }
1064           }
1065           final CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix);
1066           final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);
1067           Object rowValue = nestedResultObjects.get(combinedKey);
1068           boolean knownValue = rowValue != null;
1069           instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); // mandatory
1070           if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) {
1071             rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);
1072             if (rowValue != null && !knownValue) {
1073               linkObjects(metaObject, resultMapping, rowValue);
1074               foundValues = true;
1075             }
1076           }
1077         } catch (SQLException e) {
1078           throw new ExecutorException(
1079               "Error getting nested result map values for '" + resultMapping.getProperty() + "'.  Cause: " + e, e);
1080         }
1081       }
1082     }
1083     return foundValues;
1084   }
1085 
1086   private String getColumnPrefix(String parentPrefix, ResultMapping resultMapping) {
1087     final StringBuilder columnPrefixBuilder = new StringBuilder();
1088     if (parentPrefix != null) {
1089       columnPrefixBuilder.append(parentPrefix);
1090     }
1091     if (resultMapping.getColumnPrefix() != null) {
1092       columnPrefixBuilder.append(resultMapping.getColumnPrefix());
1093     }
1094     return columnPrefixBuilder.length() == 0 ? null : columnPrefixBuilder.toString().toUpperCase(Locale.ENGLISH);
1095   }
1096 
1097   private boolean anyNotNullColumnHasValue(ResultMapping resultMapping, String columnPrefix, ResultSetWrapper rsw)
1098       throws SQLException {
1099     Set<String> notNullColumns = resultMapping.getNotNullColumns();
1100     if (notNullColumns != null && !notNullColumns.isEmpty()) {
1101       ResultSet rs = rsw.getResultSet();
1102       for (String column : notNullColumns) {
1103         rs.getObject(prependPrefix(column, columnPrefix));
1104         if (!rs.wasNull()) {
1105           return true;
1106         }
1107       }
1108       return false;
1109     }
1110     if (columnPrefix != null) {
1111       for (String columnName : rsw.getColumnNames()) {
1112         if (columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix.toUpperCase(Locale.ENGLISH))) {
1113           return true;
1114         }
1115       }
1116       return false;
1117     }
1118     return true;
1119   }
1120 
1121   private ResultMap getNestedResultMap(ResultSet rs, String nestedResultMapId, String columnPrefix)
1122       throws SQLException {
1123     ResultMap nestedResultMap = configuration.getResultMap(nestedResultMapId);
1124     return resolveDiscriminatedResultMap(rs, nestedResultMap, columnPrefix);
1125   }
1126 
1127   //
1128   // UNIQUE RESULT KEY
1129   //
1130 
1131   private CacheKey createRowKey(ResultMap resultMap, ResultSetWrapper rsw, String columnPrefix) throws SQLException {
1132     final CacheKey cacheKey = new CacheKey();
1133     cacheKey.update(resultMap.getId());
1134     List<ResultMapping> resultMappings = getResultMappingsForRowKey(resultMap);
1135     if (resultMappings.isEmpty()) {
1136       if (Map.class.isAssignableFrom(resultMap.getType())) {
1137         createRowKeyForMap(rsw, cacheKey);
1138       } else {
1139         createRowKeyForUnmappedProperties(resultMap, rsw, cacheKey, columnPrefix);
1140       }
1141     } else {
1142       createRowKeyForMappedProperties(resultMap, rsw, cacheKey, resultMappings, columnPrefix);
1143     }
1144     if (cacheKey.getUpdateCount() < 2) {
1145       return CacheKey.NULL_CACHE_KEY;
1146     }
1147     return cacheKey;
1148   }
1149 
1150   private CacheKey combineKeys(CacheKey rowKey, CacheKey parentRowKey) {
1151     if (rowKey.getUpdateCount() > 1 && parentRowKey.getUpdateCount() > 1) {
1152       CacheKey combinedKey;
1153       try {
1154         combinedKey = rowKey.clone();
1155       } catch (CloneNotSupportedException e) {
1156         throw new ExecutorException("Error cloning cache key.  Cause: " + e, e);
1157       }
1158       combinedKey.update(parentRowKey);
1159       return combinedKey;
1160     }
1161     return CacheKey.NULL_CACHE_KEY;
1162   }
1163 
1164   private List<ResultMapping> getResultMappingsForRowKey(ResultMap resultMap) {
1165     List<ResultMapping> resultMappings = resultMap.getIdResultMappings();
1166     if (resultMappings.isEmpty()) {
1167       resultMappings = resultMap.getPropertyResultMappings();
1168     }
1169     return resultMappings;
1170   }
1171 
1172   private void createRowKeyForMappedProperties(ResultMap resultMap, ResultSetWrapper rsw, CacheKey cacheKey,
1173       List<ResultMapping> resultMappings, String columnPrefix) throws SQLException {
1174     for (ResultMapping resultMapping : resultMappings) {
1175       if (resultMapping.isSimple()) {
1176         final String column = prependPrefix(resultMapping.getColumn(), columnPrefix);
1177         final TypeHandler<?> th = resultMapping.getTypeHandler();
1178         Set<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
1179         // Issue #114
1180         if (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))) {
1181           final Object value = th.getResult(rsw.getResultSet(), column);
1182           if (value != null || configuration.isReturnInstanceForEmptyRow()) {
1183             cacheKey.update(column);
1184             cacheKey.update(value);
1185           }
1186         }
1187       }
1188     }
1189   }
1190 
1191   private void createRowKeyForUnmappedProperties(ResultMap resultMap, ResultSetWrapper rsw, CacheKey cacheKey,
1192       String columnPrefix) throws SQLException {
1193     final MetaClass metaType = MetaClass.forClass(resultMap.getType(), reflectorFactory);
1194     List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
1195     for (String column : unmappedColumnNames) {
1196       String property = column;
1197       if (columnPrefix != null && !columnPrefix.isEmpty()) {
1198         // When columnPrefix is specified, ignore columns without the prefix.
1199         if (!column.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
1200           continue;
1201         }
1202         property = column.substring(columnPrefix.length());
1203       }
1204       if (metaType.findProperty(property, configuration.isMapUnderscoreToCamelCase()) != null) {
1205         String value = rsw.getResultSet().getString(column);
1206         if (value != null) {
1207           cacheKey.update(column);
1208           cacheKey.update(value);
1209         }
1210       }
1211     }
1212   }
1213 
1214   private void createRowKeyForMap(ResultSetWrapper rsw, CacheKey cacheKey) throws SQLException {
1215     List<String> columnNames = rsw.getColumnNames();
1216     for (String columnName : columnNames) {
1217       final String value = rsw.getResultSet().getString(columnName);
1218       if (value != null) {
1219         cacheKey.update(columnName);
1220         cacheKey.update(value);
1221       }
1222     }
1223   }
1224 
1225   private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue) {
1226     final Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject);
1227     if (collectionProperty != null) {
1228       final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty);
1229       targetMetaObject.add(rowValue);
1230     } else {
1231       metaObject.setValue(resultMapping.getProperty(), rowValue);
1232     }
1233   }
1234 
1235   private Object instantiateCollectionPropertyIfAppropriate(ResultMapping resultMapping, MetaObject metaObject) {
1236     final String propertyName = resultMapping.getProperty();
1237     Object propertyValue = metaObject.getValue(propertyName);
1238     if (propertyValue == null) {
1239       Class<?> type = resultMapping.getJavaType();
1240       if (type == null) {
1241         type = metaObject.getSetterType(propertyName);
1242       }
1243       try {
1244         if (objectFactory.isCollection(type)) {
1245           propertyValue = objectFactory.create(type);
1246           metaObject.setValue(propertyName, propertyValue);
1247           return propertyValue;
1248         }
1249       } catch (Exception e) {
1250         throw new ExecutorException(
1251             "Error instantiating collection property for result '" + resultMapping.getProperty() + "'.  Cause: " + e,
1252             e);
1253       }
1254     } else if (objectFactory.isCollection(propertyValue.getClass())) {
1255       return propertyValue;
1256     }
1257     return null;
1258   }
1259 
1260   private boolean hasTypeHandlerForResultObject(ResultSetWrapper rsw, Class<?> resultType) {
1261     if (rsw.getColumnNames().size() == 1) {
1262       return typeHandlerRegistry.hasTypeHandler(resultType, rsw.getJdbcType(rsw.getColumnNames().get(0)));
1263     }
1264     return typeHandlerRegistry.hasTypeHandler(resultType);
1265   }
1266 
1267 }