Skip to content

Argument name based constructor auto-mapping #2196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2009-2021 the original author or authors.
* Copyright 2009-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -269,6 +269,7 @@ private void settingsElement(Properties props) {
configuration.setLogPrefix(props.getProperty("logPrefix"));
configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
configuration.setShrinkWhitespacesInSql(booleanValueOf(props.getProperty("shrinkWhitespacesInSql"), false));
configuration.setArgNameBasedConstructorAutoMapping(booleanValueOf(props.getProperty("argNameBasedConstructorAutoMapping"), false));
configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType")));
configuration.setNullableOnForEach(booleanValueOf(props.getProperty("nullableOnForEach"), false));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2009-2021 the original author or authors.
* Copyright 2009-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,19 +16,24 @@
package org.apache.ibatis.executor.resultset;

import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;
import java.sql.CallableStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import org.apache.ibatis.annotations.AutomapConstructor;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.cursor.Cursor;
Expand Down Expand Up @@ -95,6 +100,7 @@ public class DefaultResultSetHandler implements ResultSetHandler {

// Cached Automappings
private final Map<String, List<UnMappedColumnAutoMapping>> autoMappingsCache = new HashMap<>();
private final Map<String, List<String>> constructorAutoMappingColumns = new HashMap<>();

// temporary marking flag that indicate using constructor mapping (use field to reduce memory usage)
private boolean useConstructorMappings;
Expand Down Expand Up @@ -519,6 +525,11 @@ private List<UnMappedColumnAutoMapping> createAutomaticMappings(ResultSetWrapper
if (autoMapping == null) {
autoMapping = new ArrayList<>();
final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix);
// Remove the entry to release the memory
List<String> mappedInConstructorAutoMapping = constructorAutoMappingColumns.remove(mapKey);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@harawata Might be useful for a comment here to understand why its removing vs others getting data. I think I grasp it but find it somewhat confusing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, @hazendaz !
Added a brief comment.
The entry is used only once, so there is no point in keeping it on memory.

if (mappedInConstructorAutoMapping != null) {
unmappedColumnNames.removeAll(mappedInConstructorAutoMapping);
}
for (String columnName : unmappedColumnNames) {
String propertyName = columnName;
if (columnPrefix != null && !columnPrefix.isEmpty()) {
Expand Down Expand Up @@ -655,7 +666,7 @@ private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, Lis
} else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
return objectFactory.create(resultType);
} else if (shouldApplyAutomaticMappings(resultMap, false)) {
return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs);
return createByConstructorSignature(rsw, resultMap, columnPrefix, resultType, constructorArgTypes, constructorArgs);
}
throw new ExecutorException("Do not know how to create an instance of " + resultType);
}
Expand Down Expand Up @@ -687,23 +698,61 @@ Object createParameterizedResultObject(ResultSetWrapper rsw, Class<?> resultType
return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
}

private Object createByConstructorSignature(ResultSetWrapper rsw, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) throws SQLException {
final Constructor<?>[] constructors = resultType.getDeclaredConstructors();
final Constructor<?> defaultConstructor = findDefaultConstructor(constructors);
if (defaultConstructor != null) {
return createUsingConstructor(rsw, resultType, constructorArgTypes, constructorArgs, defaultConstructor);
private Object createByConstructorSignature(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix, Class<?> resultType,
List<Class<?>> constructorArgTypes, List<Object> constructorArgs) throws SQLException {
return applyConstructorAutomapping(rsw, resultMap, columnPrefix, resultType, constructorArgTypes, constructorArgs,
findConstructorForAutomapping(resultType, rsw).orElseThrow(() -> new ExecutorException(
"No constructor found in " + resultType.getName() + " matching " + rsw.getClassNames())));
}

private Optional<Constructor<?>> findConstructorForAutomapping(final Class<?> resultType, ResultSetWrapper rsw) {
Constructor<?>[] constructors = resultType.getDeclaredConstructors();
if (constructors.length == 1) {
return Optional.of(constructors[0]);
}
for (final Constructor<?> constructor : constructors) {
if (constructor.isAnnotationPresent(AutomapConstructor.class)) {
return Optional.of(constructor);
}
}
if (configuration.isArgNameBasedConstructorAutoMapping()) {
// Finding-best-match type implementation is possible,
// but using @AutomapConstructor seems sufficient.
throw new ExecutorException(MessageFormat.format(
"'argNameBasedConstructorAutoMapping' is enabled and the class ''{0}'' has multiple constructors, so @AutomapConstructor must be added to one of the constructors.",
resultType.getName()));
} else {
for (Constructor<?> constructor : constructors) {
if (allowedConstructorUsingTypeHandlers(constructor, rsw.getJdbcTypes())) {
return createUsingConstructor(rsw, resultType, constructorArgTypes, constructorArgs, constructor);
}
return Arrays.stream(constructors).filter(x -> findUsableConstructorByArgTypes(x, rsw.getJdbcTypes())).findAny();
}
}

private boolean findUsableConstructorByArgTypes(final Constructor<?> constructor, final List<JdbcType> jdbcTypes) {
final Class<?>[] parameterTypes = constructor.getParameterTypes();
if (parameterTypes.length != jdbcTypes.size()) {
return false;
}
for (int i = 0; i < parameterTypes.length; i++) {
if (!typeHandlerRegistry.hasTypeHandler(parameterTypes[i], jdbcTypes.get(i))) {
return false;
}
}
throw new ExecutorException("No constructor found in " + resultType.getName() + " matching " + rsw.getClassNames());
return true;
}

private Object createUsingConstructor(ResultSetWrapper rsw, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor) throws SQLException {
private Object applyConstructorAutomapping(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor) throws SQLException {
boolean foundValues = false;
if (configuration.isArgNameBasedConstructorAutoMapping()) {
foundValues = applyArgNameBasedConstructorAutoMapping(rsw, resultMap, columnPrefix, resultType, constructorArgTypes, constructorArgs,
constructor, foundValues);
} else {
foundValues = applyColumnOrderBasedConstructorAutomapping(rsw, constructorArgTypes, constructorArgs, constructor,
foundValues);
}
return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
}

private boolean applyColumnOrderBasedConstructorAutomapping(ResultSetWrapper rsw, List<Class<?>> constructorArgTypes,
List<Object> constructorArgs, Constructor<?> constructor, boolean foundValues) throws SQLException {
for (int i = 0; i < constructor.getParameterTypes().length; i++) {
Class<?> parameterType = constructor.getParameterTypes()[i];
String columnName = rsw.getColumnNames().get(i);
Expand All @@ -713,33 +762,58 @@ private Object createUsingConstructor(ResultSetWrapper rsw, Class<?> resultType,
constructorArgs.add(value);
foundValues = value != null || foundValues;
}
return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null;
return foundValues;
}

private Constructor<?> findDefaultConstructor(final Constructor<?>[] constructors) {
if (constructors.length == 1) {
return constructors[0];
}

for (final Constructor<?> constructor : constructors) {
if (constructor.isAnnotationPresent(AutomapConstructor.class)) {
return constructor;
private boolean applyArgNameBasedConstructorAutoMapping(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix, Class<?> resultType,
List<Class<?>> constructorArgTypes, List<Object> constructorArgs, Constructor<?> constructor, boolean foundValues)
throws SQLException {
List<String> missingArgs = null;
Parameter[] params = constructor.getParameters();
for (Parameter param : params) {
boolean columnNotFound = true;
Param paramAnno = param.getAnnotation(Param.class);
String paramName = paramAnno == null ? param.getName() : paramAnno.value();
for (String columnName : rsw.getColumnNames()) {
if (columnMatchesParam(columnName, paramName, columnPrefix)) {
Class<?> paramType = param.getType();
TypeHandler<?> typeHandler = rsw.getTypeHandler(paramType, columnName);
Object value = typeHandler.getResult(rsw.getResultSet(), columnName);
constructorArgTypes.add(paramType);
constructorArgs.add(value);
final String mapKey = resultMap.getId() + ":" + columnPrefix;
if (!autoMappingsCache.containsKey(mapKey)) {
MapUtil.computeIfAbsent(constructorAutoMappingColumns, mapKey, k -> new ArrayList<>()).add(columnName);
}
columnNotFound = false;
foundValues = value != null || foundValues;
}
}
if (columnNotFound) {
if (missingArgs == null) {
missingArgs = new ArrayList<>();
}
missingArgs.add(paramName);
}
}
return null;
if (foundValues && constructorArgs.size() < params.length) {
throw new ExecutorException(MessageFormat.format("Constructor auto-mapping of ''{1}'' failed "
+ "because ''{0}'' were not found in the result set; "
+ "Available columns are ''{2}'' and mapUnderscoreToCamelCase is ''{3}''.",
missingArgs, constructor, rsw.getColumnNames(), configuration.isMapUnderscoreToCamelCase()));
}
return foundValues;
}

private boolean allowedConstructorUsingTypeHandlers(final Constructor<?> constructor, final List<JdbcType> jdbcTypes) {
final Class<?>[] parameterTypes = constructor.getParameterTypes();
if (parameterTypes.length != jdbcTypes.size()) {
return false;
}
for (int i = 0; i < parameterTypes.length; i++) {
if (!typeHandlerRegistry.hasTypeHandler(parameterTypes[i], jdbcTypes.get(i))) {
private boolean columnMatchesParam(String columnName, String paramName, String columnPrefix) {
if (columnPrefix != null) {
if (!columnName.toUpperCase(Locale.ENGLISH).startsWith(columnPrefix)) {
return false;
}
columnName = columnName.substring(columnPrefix.length());
}
return true;
return paramName
.equalsIgnoreCase(configuration.isMapUnderscoreToCamelCase() ? columnName.replace("_", "") : columnName);
}

private Object createPrimitiveResultObject(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
Expand Down
11 changes: 10 additions & 1 deletion src/main/java/org/apache/ibatis/session/Configuration.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2009-2021 the original author or authors.
* Copyright 2009-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -115,6 +115,7 @@ public class Configuration {
protected boolean returnInstanceForEmptyRow;
protected boolean shrinkWhitespacesInSql;
protected boolean nullableOnForEach;
protected boolean argNameBasedConstructorAutoMapping;

protected String logPrefix;
protected Class<? extends Log> logImpl;
Expand Down Expand Up @@ -320,6 +321,14 @@ public boolean isNullableOnForEach() {
return nullableOnForEach;
}

public boolean isArgNameBasedConstructorAutoMapping() {
return argNameBasedConstructorAutoMapping;
}

public void setArgNameBasedConstructorAutoMapping(boolean argNameBasedConstructorAutoMapping) {
this.argNameBasedConstructorAutoMapping = argNameBasedConstructorAutoMapping;
}

public String getDatabaseId() {
return databaseId;
}
Expand Down
16 changes: 15 additions & 1 deletion src/site/es/xdoc/configuration.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--

Copyright 2009-2021 the original author or authors.
Copyright 2009-2022 the original author or authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -591,6 +591,20 @@ SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environ
false
</td>
</tr>
<tr>
<td>
argNameBasedConstructorAutoMapping
</td>
<td>
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)
</td>
<td>
true | false
</td>
<td>
false
</td>
</tr>
</tbody>
</table>
<p>
Expand Down
16 changes: 15 additions & 1 deletion src/site/ja/xdoc/configuration.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--

Copyright 2009-2021 the original author or authors.
Copyright 2009-2022 the original author or authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -615,6 +615,20 @@ SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environ
false
</td>
</tr>
<tr>
<td>
argNameBasedConstructorAutoMapping
</td>
<td>
引数を受け取るコンストラクタに対して自動マッピングを適用する際、引数名に一致する列をマップ対象にします。<code>false</code> の場合は列の順序依存となります。 (導入されたバージョン: 3.5.10)
</td>
<td>
true | false
</td>
<td>
false
</td>
</tr>
</tbody>
</table>
<p>
Expand Down
16 changes: 15 additions & 1 deletion src/site/ko/xdoc/configuration.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--

Copyright 2009-2021 the original author or authors.
Copyright 2009-2022 the original author or authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -598,6 +598,20 @@ SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environ
false
</td>
</tr>
<tr>
<td>
argNameBasedConstructorAutoMapping
</td>
<td>
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)
</td>
<td>
true | false
</td>
<td>
false
</td>
</tr>
</tbody>
</table>
<p>위 설정을 모두 사용한 setting 엘리먼트의 예제이다:</p>
Expand Down
16 changes: 15 additions & 1 deletion src/site/xdoc/configuration.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--

Copyright 2009-2021 the original author or authors.
Copyright 2009-2022 the original author or authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -678,6 +678,20 @@ SqlSessionFactory factory =
false
</td>
</tr>
<tr>
<td>
argNameBasedConstructorAutoMapping
</td>
<td>
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)
</td>
<td>
true | false
</td>
<td>
false
</td>
</tr>
</tbody>
</table>
<p>
Expand Down
Loading