Skip to content

Commit 1e10bc7

Browse files
authored
Merge pull request #2196 from harawata/arg-name-based-constructor-automapping
Argument name based constructor auto-mapping
2 parents 13d5d0d + ba5cc2b commit 1e10bc7

20 files changed

+622
-36
lines changed

src/main/java/org/apache/ibatis/builder/xml/XMLConfigBuilder.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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.
@@ -269,6 +269,7 @@ private void settingsElement(Properties props) {
269269
configuration.setLogPrefix(props.getProperty("logPrefix"));
270270
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
271271
configuration.setShrinkWhitespacesInSql(booleanValueOf(props.getProperty("shrinkWhitespacesInSql"), false));
272+
configuration.setArgNameBasedConstructorAutoMapping(booleanValueOf(props.getProperty("argNameBasedConstructorAutoMapping"), false));
272273
configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType")));
273274
configuration.setNullableOnForEach(booleanValueOf(props.getProperty("nullableOnForEach"), false));
274275
}

src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java

Lines changed: 105 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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.
@@ -16,19 +16,24 @@
1616
package org.apache.ibatis.executor.resultset;
1717

1818
import java.lang.reflect.Constructor;
19+
import java.lang.reflect.Parameter;
1920
import java.sql.CallableStatement;
2021
import java.sql.ResultSet;
2122
import java.sql.SQLException;
2223
import java.sql.Statement;
24+
import java.text.MessageFormat;
2325
import java.util.ArrayList;
26+
import java.util.Arrays;
2427
import java.util.HashMap;
2528
import java.util.HashSet;
2629
import java.util.List;
2730
import java.util.Locale;
2831
import java.util.Map;
32+
import java.util.Optional;
2933
import java.util.Set;
3034

3135
import org.apache.ibatis.annotations.AutomapConstructor;
36+
import org.apache.ibatis.annotations.Param;
3237
import org.apache.ibatis.binding.MapperMethod.ParamMap;
3338
import org.apache.ibatis.cache.CacheKey;
3439
import 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 {

src/main/java/org/apache/ibatis/session/Configuration.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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.
@@ -115,6 +115,7 @@ public class Configuration {
115115
protected boolean returnInstanceForEmptyRow;
116116
protected boolean shrinkWhitespacesInSql;
117117
protected boolean nullableOnForEach;
118+
protected boolean argNameBasedConstructorAutoMapping;
118119

119120
protected String logPrefix;
120121
protected Class<? extends Log> logImpl;
@@ -320,6 +321,14 @@ public boolean isNullableOnForEach() {
320321
return nullableOnForEach;
321322
}
322323

324+
public boolean isArgNameBasedConstructorAutoMapping() {
325+
return argNameBasedConstructorAutoMapping;
326+
}
327+
328+
public void setArgNameBasedConstructorAutoMapping(boolean argNameBasedConstructorAutoMapping) {
329+
this.argNameBasedConstructorAutoMapping = argNameBasedConstructorAutoMapping;
330+
}
331+
323332
public String getDatabaseId() {
324333
return databaseId;
325334
}

src/site/es/xdoc/configuration.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,20 @@ SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environ
591591
false
592592
</td>
593593
</tr>
594+
<tr>
595+
<td>
596+
argNameBasedConstructorAutoMapping
597+
</td>
598+
<td>
599+
When applying constructor auto-mapping, argument name is used to search the column to map instead of relying on the column order. (Since 3.5.10)
600+
</td>
601+
<td>
602+
true | false
603+
</td>
604+
<td>
605+
false
606+
</td>
607+
</tr>
594608
</tbody>
595609
</table>
596610
<p>

src/site/ja/xdoc/configuration.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,20 @@ SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environ
615615
false
616616
</td>
617617
</tr>
618+
<tr>
619+
<td>
620+
argNameBasedConstructorAutoMapping
621+
</td>
622+
<td>
623+
引数を受け取るコンストラクタに対して自動マッピングを適用する際、引数名に一致する列をマップ対象にします。<code>false</code> の場合は列の順序依存となります。 (導入されたバージョン: 3.5.10)
624+
</td>
625+
<td>
626+
true | false
627+
</td>
628+
<td>
629+
false
630+
</td>
631+
</tr>
618632
</tbody>
619633
</table>
620634
<p>

src/site/ko/xdoc/configuration.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,20 @@ SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environ
598598
false
599599
</td>
600600
</tr>
601+
<tr>
602+
<td>
603+
argNameBasedConstructorAutoMapping
604+
</td>
605+
<td>
606+
When applying constructor auto-mapping, argument name is used to search the column to map instead of relying on the column order. (Since 3.5.10)
607+
</td>
608+
<td>
609+
true | false
610+
</td>
611+
<td>
612+
false
613+
</td>
614+
</tr>
601615
</tbody>
602616
</table>
603617
<p>위 설정을 모두 사용한 setting 엘리먼트의 예제이다:</p>

src/site/xdoc/configuration.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,20 @@ SqlSessionFactory factory =
678678
false
679679
</td>
680680
</tr>
681+
<tr>
682+
<td>
683+
argNameBasedConstructorAutoMapping
684+
</td>
685+
<td>
686+
When applying constructor auto-mapping, argument name is used to search the column to map instead of relying on the column order. (Since 3.5.10)
687+
</td>
688+
<td>
689+
true | false
690+
</td>
691+
<td>
692+
false
693+
</td>
694+
</tr>
681695
</tbody>
682696
</table>
683697
<p>

src/site/zh/xdoc/configuration.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,20 @@ SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environ
609609
false
610610
</td>
611611
</tr>
612+
<tr>
613+
<td>
614+
argNameBasedConstructorAutoMapping
615+
</td>
616+
<td>
617+
When applying constructor auto-mapping, argument name is used to search the column to map instead of relying on the column order. (Since 3.5.10)
618+
</td>
619+
<td>
620+
true | false
621+
</td>
622+
<td>
623+
false
624+
</td>
625+
</tr>
612626
</tbody>
613627
</table>
614628
<p>

src/test/java/org/apache/ibatis/builder/CustomizedSettingsMapperConfig.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8" ?>
22
<!--
33
4-
Copyright 2009-2021 the original author or authors.
4+
Copyright 2009-2022 the original author or authors.
55
66
Licensed under the Apache License, Version 2.0 (the "License");
77
you may not use this file except in compliance with the License.
@@ -55,6 +55,7 @@
5555
<setting name="configurationFactory" value="java.lang.String"/>
5656
<setting name="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
5757
<setting name="shrinkWhitespacesInSql" value="true"/>
58+
<setting name="argNameBasedConstructorAutoMapping" value="true"/>
5859
<setting name="defaultSqlProviderType" value="org.apache.ibatis.builder.XmlConfigBuilderTest$MySqlProvider"/>
5960
<setting name="nullableOnForEach" value="true"/>
6061
</settings>

0 commit comments

Comments
 (0)