11/*
2- * Copyright 2009-2021 the original author or authors.
2+ * Copyright 2009-2022 the original author or authors.
33 *
44 * Licensed under the Apache License, Version 2.0 (the "License");
55 * you may not use this file except in compliance with the License.
1616package org .apache .ibatis .executor .resultset ;
1717
1818import java .lang .reflect .Constructor ;
19+ import java .lang .reflect .Parameter ;
1920import java .sql .CallableStatement ;
2021import java .sql .ResultSet ;
2122import java .sql .SQLException ;
2223import java .sql .Statement ;
24+ import java .text .MessageFormat ;
2325import java .util .ArrayList ;
26+ import java .util .Arrays ;
2427import java .util .HashMap ;
2528import java .util .HashSet ;
2629import java .util .List ;
2730import java .util .Locale ;
2831import java .util .Map ;
32+ import java .util .Optional ;
2933import java .util .Set ;
3034
3135import org .apache .ibatis .annotations .AutomapConstructor ;
36+ import org .apache .ibatis .annotations .Param ;
3237import org .apache .ibatis .binding .MapperMethod .ParamMap ;
3338import org .apache .ibatis .cache .CacheKey ;
3439import org .apache .ibatis .cursor .Cursor ;
@@ -95,6 +100,7 @@ public class DefaultResultSetHandler implements ResultSetHandler {
95100
96101 // Cached Automappings
97102 private final Map <String , List <UnMappedColumnAutoMapping >> autoMappingsCache = new HashMap <>();
103+ private final Map <String , List <String >> constructorAutoMappingColumns = new HashMap <>();
98104
99105 // temporary marking flag that indicate using constructor mapping (use field to reduce memory usage)
100106 private boolean useConstructorMappings ;
@@ -519,6 +525,11 @@ private List<UnMappedColumnAutoMapping> createAutomaticMappings(ResultSetWrapper
519525 if (autoMapping == null ) {
520526 autoMapping = new ArrayList <>();
521527 final List <String > unmappedColumnNames = rsw .getUnmappedColumnNames (resultMap , columnPrefix );
528+ // Remove the entry to release the memory
529+ List <String > mappedInConstructorAutoMapping = constructorAutoMappingColumns .remove (mapKey );
530+ if (mappedInConstructorAutoMapping != null ) {
531+ unmappedColumnNames .removeAll (mappedInConstructorAutoMapping );
532+ }
522533 for (String columnName : unmappedColumnNames ) {
523534 String propertyName = columnName ;
524535 if (columnPrefix != null && !columnPrefix .isEmpty ()) {
@@ -655,7 +666,7 @@ private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, Lis
655666 } else if (resultType .isInterface () || metaType .hasDefaultConstructor ()) {
656667 return objectFactory .create (resultType );
657668 } else if (shouldApplyAutomaticMappings (resultMap , false )) {
658- return createByConstructorSignature (rsw , resultType , constructorArgTypes , constructorArgs );
669+ return createByConstructorSignature (rsw , resultMap , columnPrefix , resultType , constructorArgTypes , constructorArgs );
659670 }
660671 throw new ExecutorException ("Do not know how to create an instance of " + resultType );
661672 }
@@ -687,23 +698,61 @@ Object createParameterizedResultObject(ResultSetWrapper rsw, Class<?> resultType
687698 return foundValues ? objectFactory .create (resultType , constructorArgTypes , constructorArgs ) : null ;
688699 }
689700
690- private Object createByConstructorSignature (ResultSetWrapper rsw , Class <?> resultType , List <Class <?>> constructorArgTypes , List <Object > constructorArgs ) throws SQLException {
691- final Constructor <?>[] constructors = resultType .getDeclaredConstructors ();
692- final Constructor <?> defaultConstructor = findDefaultConstructor (constructors );
693- if (defaultConstructor != null ) {
694- return createUsingConstructor (rsw , resultType , constructorArgTypes , constructorArgs , defaultConstructor );
701+ private Object createByConstructorSignature (ResultSetWrapper rsw , ResultMap resultMap , String columnPrefix , Class <?> resultType ,
702+ List <Class <?>> constructorArgTypes , List <Object > constructorArgs ) throws SQLException {
703+ return applyConstructorAutomapping (rsw , resultMap , columnPrefix , resultType , constructorArgTypes , constructorArgs ,
704+ findConstructorForAutomapping (resultType , rsw ).orElseThrow (() -> new ExecutorException (
705+ "No constructor found in " + resultType .getName () + " matching " + rsw .getClassNames ())));
706+ }
707+
708+ private Optional <Constructor <?>> findConstructorForAutomapping (final Class <?> resultType , ResultSetWrapper rsw ) {
709+ Constructor <?>[] constructors = resultType .getDeclaredConstructors ();
710+ if (constructors .length == 1 ) {
711+ return Optional .of (constructors [0 ]);
712+ }
713+ for (final Constructor <?> constructor : constructors ) {
714+ if (constructor .isAnnotationPresent (AutomapConstructor .class )) {
715+ return Optional .of (constructor );
716+ }
717+ }
718+ if (configuration .isArgNameBasedConstructorAutoMapping ()) {
719+ // Finding-best-match type implementation is possible,
720+ // but using @AutomapConstructor seems sufficient.
721+ throw new ExecutorException (MessageFormat .format (
722+ "'argNameBasedConstructorAutoMapping' is enabled and the class ''{0}'' has multiple constructors, so @AutomapConstructor must be added to one of the constructors." ,
723+ resultType .getName ()));
695724 } else {
696- for (Constructor <?> constructor : constructors ) {
697- if (allowedConstructorUsingTypeHandlers (constructor , rsw .getJdbcTypes ())) {
698- return createUsingConstructor (rsw , resultType , constructorArgTypes , constructorArgs , constructor );
699- }
725+ return Arrays .stream (constructors ).filter (x -> findUsableConstructorByArgTypes (x , rsw .getJdbcTypes ())).findAny ();
726+ }
727+ }
728+
729+ private boolean findUsableConstructorByArgTypes (final Constructor <?> constructor , final List <JdbcType > jdbcTypes ) {
730+ final Class <?>[] parameterTypes = constructor .getParameterTypes ();
731+ if (parameterTypes .length != jdbcTypes .size ()) {
732+ return false ;
733+ }
734+ for (int i = 0 ; i < parameterTypes .length ; i ++) {
735+ if (!typeHandlerRegistry .hasTypeHandler (parameterTypes [i ], jdbcTypes .get (i ))) {
736+ return false ;
700737 }
701738 }
702- throw new ExecutorException ( "No constructor found in " + resultType . getName () + " matching " + rsw . getClassNames ()) ;
739+ return true ;
703740 }
704741
705- private Object createUsingConstructor (ResultSetWrapper rsw , Class <?> resultType , List <Class <?>> constructorArgTypes , List <Object > constructorArgs , Constructor <?> constructor ) throws SQLException {
742+ private Object applyConstructorAutomapping (ResultSetWrapper rsw , ResultMap resultMap , String columnPrefix , Class <?> resultType , List <Class <?>> constructorArgTypes , List <Object > constructorArgs , Constructor <?> constructor ) throws SQLException {
706743 boolean foundValues = false ;
744+ if (configuration .isArgNameBasedConstructorAutoMapping ()) {
745+ foundValues = applyArgNameBasedConstructorAutoMapping (rsw , resultMap , columnPrefix , resultType , constructorArgTypes , constructorArgs ,
746+ constructor , foundValues );
747+ } else {
748+ foundValues = applyColumnOrderBasedConstructorAutomapping (rsw , constructorArgTypes , constructorArgs , constructor ,
749+ foundValues );
750+ }
751+ return foundValues ? objectFactory .create (resultType , constructorArgTypes , constructorArgs ) : null ;
752+ }
753+
754+ private boolean applyColumnOrderBasedConstructorAutomapping (ResultSetWrapper rsw , List <Class <?>> constructorArgTypes ,
755+ List <Object > constructorArgs , Constructor <?> constructor , boolean foundValues ) throws SQLException {
707756 for (int i = 0 ; i < constructor .getParameterTypes ().length ; i ++) {
708757 Class <?> parameterType = constructor .getParameterTypes ()[i ];
709758 String columnName = rsw .getColumnNames ().get (i );
@@ -713,33 +762,58 @@ private Object createUsingConstructor(ResultSetWrapper rsw, Class<?> resultType,
713762 constructorArgs .add (value );
714763 foundValues = value != null || foundValues ;
715764 }
716- return foundValues ? objectFactory . create ( resultType , constructorArgTypes , constructorArgs ) : null ;
765+ return foundValues ;
717766 }
718767
719- private Constructor <?> findDefaultConstructor (final Constructor <?>[] constructors ) {
720- if (constructors .length == 1 ) {
721- return constructors [0 ];
722- }
723-
724- for (final Constructor <?> constructor : constructors ) {
725- if (constructor .isAnnotationPresent (AutomapConstructor .class )) {
726- return constructor ;
768+ private boolean applyArgNameBasedConstructorAutoMapping (ResultSetWrapper rsw , ResultMap resultMap , String columnPrefix , Class <?> resultType ,
769+ List <Class <?>> constructorArgTypes , List <Object > constructorArgs , Constructor <?> constructor , boolean foundValues )
770+ throws SQLException {
771+ List <String > missingArgs = null ;
772+ Parameter [] params = constructor .getParameters ();
773+ for (Parameter param : params ) {
774+ boolean columnNotFound = true ;
775+ Param paramAnno = param .getAnnotation (Param .class );
776+ String paramName = paramAnno == null ? param .getName () : paramAnno .value ();
777+ for (String columnName : rsw .getColumnNames ()) {
778+ if (columnMatchesParam (columnName , paramName , columnPrefix )) {
779+ Class <?> paramType = param .getType ();
780+ TypeHandler <?> typeHandler = rsw .getTypeHandler (paramType , columnName );
781+ Object value = typeHandler .getResult (rsw .getResultSet (), columnName );
782+ constructorArgTypes .add (paramType );
783+ constructorArgs .add (value );
784+ final String mapKey = resultMap .getId () + ":" + columnPrefix ;
785+ if (!autoMappingsCache .containsKey (mapKey )) {
786+ MapUtil .computeIfAbsent (constructorAutoMappingColumns , mapKey , k -> new ArrayList <>()).add (columnName );
787+ }
788+ columnNotFound = false ;
789+ foundValues = value != null || foundValues ;
790+ }
791+ }
792+ if (columnNotFound ) {
793+ if (missingArgs == null ) {
794+ missingArgs = new ArrayList <>();
795+ }
796+ missingArgs .add (paramName );
727797 }
728798 }
729- return null ;
799+ if (foundValues && constructorArgs .size () < params .length ) {
800+ throw new ExecutorException (MessageFormat .format ("Constructor auto-mapping of ''{1}'' failed "
801+ + "because ''{0}'' were not found in the result set; "
802+ + "Available columns are ''{2}'' and mapUnderscoreToCamelCase is ''{3}''." ,
803+ missingArgs , constructor , rsw .getColumnNames (), configuration .isMapUnderscoreToCamelCase ()));
804+ }
805+ return foundValues ;
730806 }
731807
732- private boolean allowedConstructorUsingTypeHandlers (final Constructor <?> constructor , final List <JdbcType > jdbcTypes ) {
733- final Class <?>[] parameterTypes = constructor .getParameterTypes ();
734- if (parameterTypes .length != jdbcTypes .size ()) {
735- return false ;
736- }
737- for (int i = 0 ; i < parameterTypes .length ; i ++) {
738- if (!typeHandlerRegistry .hasTypeHandler (parameterTypes [i ], jdbcTypes .get (i ))) {
808+ private boolean columnMatchesParam (String columnName , String paramName , String columnPrefix ) {
809+ if (columnPrefix != null ) {
810+ if (!columnName .toUpperCase (Locale .ENGLISH ).startsWith (columnPrefix )) {
739811 return false ;
740812 }
813+ columnName = columnName .substring (columnPrefix .length ());
741814 }
742- return true ;
815+ return paramName
816+ .equalsIgnoreCase (configuration .isMapUnderscoreToCamelCase () ? columnName .replace ("_" , "" ) : columnName );
743817 }
744818
745819 private Object createPrimitiveResultObject (ResultSetWrapper rsw , ResultMap resultMap , String columnPrefix ) throws SQLException {
0 commit comments