Skip to content

Commit 395d36e

Browse files
committed
Added argNameBasedConstructorAutoMapping
When there are multiple constructors, users must use `@AutomapConstructor`. Although it would be possible to implement find-the-best-match type logic,
1 parent 590dfe8 commit 395d36e

File tree

13 files changed

+423
-39
lines changed

13 files changed

+423
-39
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}
274275

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

Lines changed: 88 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2009-2020 the original author or authors.
2+
* Copyright 2009-2021 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;
@@ -686,49 +691,35 @@ Object createParameterizedResultObject(ResultSetWrapper rsw, Class<?> resultType
686691
return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
687692
}
688693

689-
private Object createByConstructorSignature(ResultSetWrapper rsw, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) throws SQLException {
690-
final Constructor<?>[] constructors = resultType.getDeclaredConstructors();
691-
final Constructor<?> defaultConstructor = findDefaultConstructor(constructors);
692-
if (defaultConstructor != null) {
693-
return createUsingConstructor(rsw, resultType, constructorArgTypes, constructorArgs, defaultConstructor);
694-
} else {
695-
for (Constructor<?> constructor : constructors) {
696-
if (allowedConstructorUsingTypeHandlers(constructor, rsw.getJdbcTypes())) {
697-
return createUsingConstructor(rsw, resultType, constructorArgTypes, constructorArgs, constructor);
698-
}
699-
}
700-
}
701-
throw new ExecutorException("No constructor found in " + resultType.getName() + " matching " + rsw.getClassNames());
694+
private Object createByConstructorSignature(ResultSetWrapper rsw, Class<?> resultType,
695+
List<Class<?>> constructorArgTypes, List<Object> constructorArgs) throws SQLException {
696+
return applyConstructorAutomapping(rsw, resultType, constructorArgTypes, constructorArgs,
697+
findConstructorForAutomapping(resultType, rsw).orElseThrow(() -> new ExecutorException(
698+
"No constructor found in " + resultType.getName() + " matching " + rsw.getClassNames())));
702699
}
703700

704-
private Object createUsingConstructor(ResultSetWrapper rsw, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor) throws SQLException {
705-
boolean foundValues = false;
706-
for (int i = 0; i < constructor.getParameterTypes().length; i++) {
707-
Class<?> parameterType = constructor.getParameterTypes()[i];
708-
String columnName = rsw.getColumnNames().get(i);
709-
TypeHandler<?> typeHandler = rsw.getTypeHandler(parameterType, columnName);
710-
Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
711-
constructorArgTypes.add(parameterType);
712-
constructorArgs.add(value);
713-
foundValues = value != null || foundValues;
714-
}
715-
return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
716-
}
717-
718-
private Constructor<?> findDefaultConstructor(final Constructor<?>[] constructors) {
701+
private Optional<Constructor<?>> findConstructorForAutomapping(final Class<?> resultType, ResultSetWrapper rsw) {
702+
Constructor<?>[] constructors = resultType.getDeclaredConstructors();
719703
if (constructors.length == 1) {
720-
return constructors[0];
704+
return Optional.of(constructors[0]);
721705
}
722-
723706
for (final Constructor<?> constructor : constructors) {
724707
if (constructor.isAnnotationPresent(AutomapConstructor.class)) {
725-
return constructor;
708+
return Optional.of(constructor);
726709
}
727710
}
728-
return null;
711+
if (configuration.isArgNameBasedConstructorAutoMapping()) {
712+
// Finding-best-match type implementation is possible,
713+
// but using @AutomapConstructor seems sufficient.
714+
throw new ExecutorException(MessageFormat.format(
715+
"'argNameBasedConstructorAutoMapping' is enabled and the class ''{0}'' has multiple constructors, so @AutomapConstructor must be added to one of the constructors.",
716+
resultType.getName()));
717+
} else {
718+
return Arrays.stream(constructors).filter(x -> findUsableConstructorByArgTypes(x, rsw.getJdbcTypes())).findAny();
719+
}
729720
}
730721

731-
private boolean allowedConstructorUsingTypeHandlers(final Constructor<?> constructor, final List<JdbcType> jdbcTypes) {
722+
private boolean findUsableConstructorByArgTypes(final Constructor<?> constructor, final List<JdbcType> jdbcTypes) {
732723
final Class<?>[] parameterTypes = constructor.getParameterTypes();
733724
if (parameterTypes.length != jdbcTypes.size()) {
734725
return false;
@@ -741,6 +732,69 @@ private boolean allowedConstructorUsingTypeHandlers(final Constructor<?> constru
741732
return true;
742733
}
743734

735+
private Object applyConstructorAutomapping(ResultSetWrapper rsw, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor) throws SQLException {
736+
boolean foundValues = false;
737+
if (configuration.isArgNameBasedConstructorAutoMapping()) {
738+
foundValues = applyArgNameBasedConstructorAutoMapping(rsw, resultType, constructorArgTypes, constructorArgs,
739+
constructor, foundValues);
740+
} else {
741+
foundValues = applyColumnOrderBasedConstructorAutomapping(rsw, constructorArgTypes, constructorArgs, constructor,
742+
foundValues);
743+
}
744+
return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
745+
}
746+
747+
private boolean applyColumnOrderBasedConstructorAutomapping(ResultSetWrapper rsw, List<Class<?>> constructorArgTypes,
748+
List<Object> constructorArgs, Constructor<?> constructor, boolean foundValues) throws SQLException {
749+
for (int i = 0; i < constructor.getParameterTypes().length; i++) {
750+
Class<?> parameterType = constructor.getParameterTypes()[i];
751+
String columnName = rsw.getColumnNames().get(i);
752+
TypeHandler<?> typeHandler = rsw.getTypeHandler(parameterType, columnName);
753+
Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
754+
constructorArgTypes.add(parameterType);
755+
constructorArgs.add(value);
756+
foundValues = value != null || foundValues;
757+
}
758+
return foundValues;
759+
}
760+
761+
private boolean applyArgNameBasedConstructorAutoMapping(ResultSetWrapper rsw, Class<?> resultType,
762+
List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor, boolean foundValues)
763+
throws SQLException {
764+
List<String> missingArgs = null;
765+
Parameter[] params = constructor.getParameters();
766+
for (Parameter param : params) {
767+
boolean columnNotFound = true;
768+
Param paramAnno = param.getAnnotation(Param.class);
769+
String paramName = paramAnno == null ? param.getName() : paramAnno.value();
770+
for (String columnName : rsw.getColumnNames()) {
771+
if (paramName.equalsIgnoreCase(
772+
configuration.isMapUnderscoreToCamelCase() ? columnName.replace("_", "") : columnName)) {
773+
Class<?> paramType = param.getType();
774+
TypeHandler<?> typeHandler = rsw.getTypeHandler(paramType, columnName);
775+
Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
776+
constructorArgTypes.add(paramType);
777+
constructorArgs.add(value);
778+
columnNotFound = false;
779+
foundValues = value != null || foundValues;
780+
}
781+
}
782+
if (columnNotFound) {
783+
if (missingArgs == null) {
784+
missingArgs = new ArrayList<>();
785+
}
786+
missingArgs.add(paramName);
787+
}
788+
}
789+
if (foundValues && constructorArgs.size() < params.length) {
790+
throw new ExecutorException(MessageFormat.format("Constructor auto-mapping of ''{1}'' failed "
791+
+ "because ''{0}'' were not found in the result set; "
792+
+ "Available columns are ''{2}'' and mapUnderscoreToCamelCase is ''{3}''.",
793+
missingArgs, constructor, rsw.getColumnNames(), configuration.isMapUnderscoreToCamelCase()));
794+
}
795+
return foundValues;
796+
}
797+
744798
private Object createPrimitiveResultObject(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
745799
final Class<?> resultType = resultMap.getType();
746800
final String columnName;

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-2020 the original author or authors.
2+
* Copyright 2009-2021 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.
@@ -114,6 +114,7 @@ public class Configuration {
114114
protected boolean useActualParamName = true;
115115
protected boolean returnInstanceForEmptyRow;
116116
protected boolean shrinkWhitespacesInSql;
117+
protected boolean argNameBasedConstructorAutoMapping;
117118

118119
protected String logPrefix;
119120
protected Class<? extends Log> logImpl;
@@ -297,6 +298,14 @@ public void setShrinkWhitespacesInSql(boolean shrinkWhitespacesInSql) {
297298
this.shrinkWhitespacesInSql = shrinkWhitespacesInSql;
298299
}
299300

301+
public boolean isArgNameBasedConstructorAutoMapping() {
302+
return argNameBasedConstructorAutoMapping;
303+
}
304+
305+
public void setArgNameBasedConstructorAutoMapping(boolean argNameBasedConstructorAutoMapping) {
306+
this.argNameBasedConstructorAutoMapping = argNameBasedConstructorAutoMapping;
307+
}
308+
300309
public String getDatabaseId() {
301310
return databaseId;
302311
}

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-2020 the original author or authors.
4+
Copyright 2009-2021 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
</settings>
6061

src/test/java/org/apache/ibatis/builder/XmlConfigBuilderTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2009-2020 the original author or authors.
2+
* Copyright 2009-2021 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.
@@ -101,6 +101,7 @@ void shouldSuccessfullyLoadMinimalXMLConfigFile() throws Exception {
101101
assertNull(config.getConfigurationFactory());
102102
assertThat(config.getTypeHandlerRegistry().getTypeHandler(RoundingMode.class)).isInstanceOf(EnumTypeHandler.class);
103103
assertThat(config.isShrinkWhitespacesInSql()).isFalse();
104+
assertThat(config.isArgNameBasedConstructorAutoMapping()).isFalse();
104105
assertThat(config.getDefaultSqlProviderType()).isNull();
105106
}
106107
}
@@ -197,6 +198,7 @@ void shouldSuccessfullyLoadXMLConfigFile() throws Exception {
197198
assertThat(config.getVfsImpl().getName()).isEqualTo(JBoss6VFS.class.getName());
198199
assertThat(config.getConfigurationFactory().getName()).isEqualTo(String.class.getName());
199200
assertThat(config.isShrinkWhitespacesInSql()).isTrue();
201+
assertThat(config.isArgNameBasedConstructorAutoMapping()).isTrue();
200202
assertThat(config.getDefaultSqlProviderType().getName()).isEqualTo(MySqlProvider.class.getName());
201203

202204
assertThat(config.getTypeAliasRegistry().getTypeAliases().get("blogauthor")).isEqualTo(Author.class);

src/test/java/org/apache/ibatis/builder/xsd/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-2020 the original author or authors.
4+
Copyright 2009-2021 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.
@@ -51,6 +51,7 @@
5151
<setting name="vfsImpl" value="org.apache.ibatis.io.JBoss6VFS"/>
5252
<setting name="configurationFactory" value="java.lang.String"/>
5353
<setting name="shrinkWhitespacesInSql" value="true"/>
54+
<setting name="argNameBasedConstructorAutoMapping" value="true"/>
5455
</settings>
5556

5657
<typeAliases>

src/test/java/org/apache/ibatis/builder/xsd/XmlConfigBuilderTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2009-2020 the original author or authors.
2+
* Copyright 2009-2021 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.
@@ -85,6 +85,7 @@ void shouldSuccessfullyLoadMinimalXMLConfigFile() throws Exception {
8585
assertNull(config.getLogImpl());
8686
assertNull(config.getConfigurationFactory());
8787
assertFalse(config.isShrinkWhitespacesInSql());
88+
assertFalse(config.isArgNameBasedConstructorAutoMapping());
8889
} finally {
8990
// System.clearProperty(XPathParser.KEY_USE_XSD);
9091
}
@@ -123,6 +124,7 @@ void shouldSuccessfullyLoadXMLConfigFile() throws Exception {
123124
assertEquals(JBoss6VFS.class.getName(), config.getVfsImpl().getName());
124125
assertEquals(String.class.getName(), config.getConfigurationFactory().getName());
125126
assertTrue(config.isShrinkWhitespacesInSql());
127+
assertTrue(config.isArgNameBasedConstructorAutoMapping());
126128

127129
assertEquals(Author.class, config.getTypeAliasRegistry().getTypeAliases().get("blogauthor"));
128130
assertEquals(Blog.class, config.getTypeAliasRegistry().getTypeAliases().get("blog"));

0 commit comments

Comments
 (0)