diff --git a/src/main/java/org/apache/ibatis/builder/xml/XMLConfigBuilder.java b/src/main/java/org/apache/ibatis/builder/xml/XMLConfigBuilder.java index 3bb8ecf73c0..a985737f226 100644 --- a/src/main/java/org/apache/ibatis/builder/xml/XMLConfigBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/xml/XMLConfigBuilder.java @@ -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. @@ -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)); } diff --git a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java index 38993c54fe2..0973b36a6da 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java @@ -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. @@ -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; @@ -95,6 +100,7 @@ public class DefaultResultSetHandler implements ResultSetHandler { // Cached Automappings private final Map> autoMappingsCache = new HashMap<>(); + private final Map> constructorAutoMappingColumns = new HashMap<>(); // temporary marking flag that indicate using constructor mapping (use field to reduce memory usage) private boolean useConstructorMappings; @@ -519,6 +525,11 @@ private List createAutomaticMappings(ResultSetWrapper if (autoMapping == null) { autoMapping = new ArrayList<>(); final List unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix); + // Remove the entry to release the memory + List mappedInConstructorAutoMapping = constructorAutoMappingColumns.remove(mapKey); + if (mappedInConstructorAutoMapping != null) { + unmappedColumnNames.removeAll(mappedInConstructorAutoMapping); + } for (String columnName : unmappedColumnNames) { String propertyName = columnName; if (columnPrefix != null && !columnPrefix.isEmpty()) { @@ -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); } @@ -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> constructorArgTypes, List 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> constructorArgTypes, List 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> 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 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> constructorArgTypes, List constructorArgs, Constructor constructor) throws SQLException { + private Object applyConstructorAutomapping(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix, Class resultType, List> constructorArgTypes, List 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> constructorArgTypes, + List 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); @@ -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> constructorArgTypes, List constructorArgs, Constructor constructor, boolean foundValues) + throws SQLException { + List 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 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 { diff --git a/src/main/java/org/apache/ibatis/session/Configuration.java b/src/main/java/org/apache/ibatis/session/Configuration.java index ee28f5ec818..73adc5dd661 100644 --- a/src/main/java/org/apache/ibatis/session/Configuration.java +++ b/src/main/java/org/apache/ibatis/session/Configuration.java @@ -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. @@ -115,6 +115,7 @@ public class Configuration { protected boolean returnInstanceForEmptyRow; protected boolean shrinkWhitespacesInSql; protected boolean nullableOnForEach; + protected boolean argNameBasedConstructorAutoMapping; protected String logPrefix; protected Class logImpl; @@ -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; } diff --git a/src/site/es/xdoc/configuration.xml b/src/site/es/xdoc/configuration.xml index 4d76c30a2d0..12b7e929207 100644 --- a/src/site/es/xdoc/configuration.xml +++ b/src/site/es/xdoc/configuration.xml @@ -1,7 +1,7 @@ + + + + + + + + + + + + + diff --git a/src/test/java/org/apache/ibatis/submitted/arg_name_baesd_constructor_automapping/Task.java b/src/test/java/org/apache/ibatis/submitted/arg_name_baesd_constructor_automapping/Task.java new file mode 100644 index 00000000000..f59801ffb31 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/arg_name_baesd_constructor_automapping/Task.java @@ -0,0 +1,46 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.arg_name_baesd_constructor_automapping; + +import org.apache.ibatis.annotations.Param; + +public class Task { + private final Integer id; + private final String name; + private User assignee; + + public Task(@Param("id") Integer id, @Param("name") String name) { + super(); + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public User getAssignee() { + return assignee; + } + + public void setAssignee(User assignee) { + this.assignee = assignee; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/arg_name_baesd_constructor_automapping/User.java b/src/test/java/org/apache/ibatis/submitted/arg_name_baesd_constructor_automapping/User.java new file mode 100644 index 00000000000..d9133a98969 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/arg_name_baesd_constructor_automapping/User.java @@ -0,0 +1,45 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.arg_name_baesd_constructor_automapping; + +public class User { + + private Integer id; + private String name; + private Long team; + + public User(Integer id, String name) { + super(); + this.id = id; + this.name = name + "!"; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setTeam(Long team) { + this.team = team; + } + + public Long getTeam() { + return team; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/arg_name_baesd_constructor_automapping/User2.java b/src/test/java/org/apache/ibatis/submitted/arg_name_baesd_constructor_automapping/User2.java new file mode 100644 index 00000000000..cdc17732ae2 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/arg_name_baesd_constructor_automapping/User2.java @@ -0,0 +1,38 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.arg_name_baesd_constructor_automapping; + +import org.apache.ibatis.annotations.Param; + +public class User2 { + + private Integer userId; + private String name; + + public User2(Integer userId, @Param("userName") String name) { + super(); + this.userId = userId; + this.name = name; + } + + public Integer getUserId() { + return userId; + } + + public String getName() { + return name; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/arg_name_baesd_constructor_automapping/mybatis-config.xml b/src/test/java/org/apache/ibatis/submitted/arg_name_baesd_constructor_automapping/mybatis-config.xml new file mode 100644 index 00000000000..6a28314ec19 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/arg_name_baesd_constructor_automapping/mybatis-config.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + +