1
1
/*
2
- * Copyright 2009-2021 the original author or authors.
2
+ * Copyright 2009-2022 the original author or authors.
3
3
*
4
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
5
* you may not use this file except in compliance with the License.
16
16
package org .apache .ibatis .executor .resultset ;
17
17
18
18
import java .lang .reflect .Constructor ;
19
+ import java .lang .reflect .Parameter ;
19
20
import java .sql .CallableStatement ;
20
21
import java .sql .ResultSet ;
21
22
import java .sql .SQLException ;
22
23
import java .sql .Statement ;
24
+ import java .text .MessageFormat ;
23
25
import java .util .ArrayList ;
26
+ import java .util .Arrays ;
24
27
import java .util .HashMap ;
25
28
import java .util .HashSet ;
26
29
import java .util .List ;
27
30
import java .util .Locale ;
28
31
import java .util .Map ;
32
+ import java .util .Optional ;
29
33
import java .util .Set ;
30
34
31
35
import org .apache .ibatis .annotations .AutomapConstructor ;
36
+ import org .apache .ibatis .annotations .Param ;
32
37
import org .apache .ibatis .binding .MapperMethod .ParamMap ;
33
38
import org .apache .ibatis .cache .CacheKey ;
34
39
import org .apache .ibatis .cursor .Cursor ;
@@ -95,6 +100,7 @@ public class DefaultResultSetHandler implements ResultSetHandler {
95
100
96
101
// Cached Automappings
97
102
private final Map <String , List <UnMappedColumnAutoMapping >> autoMappingsCache = new HashMap <>();
103
+ private final Map <String , List <String >> constructorAutoMappingColumns = new HashMap <>();
98
104
99
105
// temporary marking flag that indicate using constructor mapping (use field to reduce memory usage)
100
106
private boolean useConstructorMappings ;
@@ -519,6 +525,11 @@ private List<UnMappedColumnAutoMapping> createAutomaticMappings(ResultSetWrapper
519
525
if (autoMapping == null ) {
520
526
autoMapping = new ArrayList <>();
521
527
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
+ }
522
533
for (String columnName : unmappedColumnNames ) {
523
534
String propertyName = columnName ;
524
535
if (columnPrefix != null && !columnPrefix .isEmpty ()) {
@@ -655,7 +666,7 @@ private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, Lis
655
666
} else if (resultType .isInterface () || metaType .hasDefaultConstructor ()) {
656
667
return objectFactory .create (resultType );
657
668
} else if (shouldApplyAutomaticMappings (resultMap , false )) {
658
- return createByConstructorSignature (rsw , resultType , constructorArgTypes , constructorArgs );
669
+ return createByConstructorSignature (rsw , resultMap , columnPrefix , resultType , constructorArgTypes , constructorArgs );
659
670
}
660
671
throw new ExecutorException ("Do not know how to create an instance of " + resultType );
661
672
}
@@ -687,23 +698,61 @@ Object createParameterizedResultObject(ResultSetWrapper rsw, Class<?> resultType
687
698
return foundValues ? objectFactory .create (resultType , constructorArgTypes , constructorArgs ) : null ;
688
699
}
689
700
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 ()));
695
724
} 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 ;
700
737
}
701
738
}
702
- throw new ExecutorException ( "No constructor found in " + resultType . getName () + " matching " + rsw . getClassNames ()) ;
739
+ return true ;
703
740
}
704
741
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 {
706
743
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 {
707
756
for (int i = 0 ; i < constructor .getParameterTypes ().length ; i ++) {
708
757
Class <?> parameterType = constructor .getParameterTypes ()[i ];
709
758
String columnName = rsw .getColumnNames ().get (i );
@@ -713,33 +762,58 @@ private Object createUsingConstructor(ResultSetWrapper rsw, Class<?> resultType,
713
762
constructorArgs .add (value );
714
763
foundValues = value != null || foundValues ;
715
764
}
716
- return foundValues ? objectFactory . create ( resultType , constructorArgTypes , constructorArgs ) : null ;
765
+ return foundValues ;
717
766
}
718
767
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 );
727
797
}
728
798
}
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 ;
730
806
}
731
807
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 )) {
739
811
return false ;
740
812
}
813
+ columnName = columnName .substring (columnPrefix .length ());
741
814
}
742
- return true ;
815
+ return paramName
816
+ .equalsIgnoreCase (configuration .isMapUnderscoreToCamelCase () ? columnName .replace ("_" , "" ) : columnName );
743
817
}
744
818
745
819
private Object createPrimitiveResultObject (ResultSetWrapper rsw , ResultMap resultMap , String columnPrefix ) throws SQLException {
0 commit comments