Skip to content

Commit a6949f0

Browse files
epochcoderharawata
authored andcommitted
mybatis#2618 When javaType has not (or partially) been specified, try to determine the best matching constructor
- falls back to current behaviour if any ambiguity is detected - remove setters from immutable `ResultMapping`
1 parent 2de2506 commit a6949f0

File tree

15 files changed

+506
-34
lines changed

15 files changed

+506
-34
lines changed

src/main/java/org/apache/ibatis/builder/MapperBuilderAssistant.java

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2009-2024 the original author or authors.
2+
* Copyright 2009-2025 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.
@@ -15,8 +15,10 @@
1515
*/
1616
package org.apache.ibatis.builder;
1717

18+
import java.lang.reflect.Constructor;
1819
import java.sql.ResultSet;
1920
import java.util.ArrayList;
21+
import java.util.Arrays;
2022
import java.util.Collections;
2123
import java.util.HashMap;
2224
import java.util.HashSet;
@@ -25,6 +27,7 @@
2527
import java.util.Properties;
2628
import java.util.Set;
2729
import java.util.StringTokenizer;
30+
import java.util.stream.Collectors;
2831

2932
import org.apache.ibatis.cache.Cache;
3033
import org.apache.ibatis.cache.decorators.LruCache;
@@ -467,4 +470,66 @@ private Class<?> resolveParameterJavaType(Class<?> resultType, String property,
467470
return javaType;
468471
}
469472

473+
/**
474+
* Attempts to assign a {@code javaType} to result mappings when it has been omitted, this is done based on matching
475+
* constructors of the specified {@code resultType}
476+
*
477+
* @param resultType
478+
* the result type of the object to be built
479+
* @param resultMappings
480+
* the current mappings
481+
*
482+
* @return null if there are no missing javaType mappings, or if no suitable mapping could be determined
483+
*/
484+
public List<ResultMapping> autoTypeResultMappingsForUnknownJavaTypes(Class<?> resultType,
485+
List<ResultMapping> resultMappings) {
486+
// check if we have any undefined java types present, and try to set them automatically
487+
if (resultMappings.stream().noneMatch(resultMapping -> Object.class.equals(resultMapping.getJavaType()))) {
488+
return null;
489+
}
490+
491+
final List<List<Class<?>>> matchingConstructors = Arrays.stream(resultType.getDeclaredConstructors())
492+
.map(Constructor::getParameterTypes).filter(parameters -> parameters.length == resultMappings.size())
493+
.map(Arrays::asList).collect(Collectors.toList());
494+
495+
final List<Class<?>> typesToMatch = resultMappings.stream().map(ResultMapping::getJavaType)
496+
.collect(Collectors.toList());
497+
498+
List<Class<?>> matchingTypes = null;
499+
500+
outer: for (final List<Class<?>> actualTypes : matchingConstructors) {
501+
for (int j = 0; j < typesToMatch.size(); j++) {
502+
final Class<?> type = typesToMatch.get(j);
503+
// pre-filled a type, check if it matches the constructor
504+
if (!Object.class.equals(type) && !type.equals(actualTypes.get(j))) {
505+
continue outer;
506+
}
507+
}
508+
509+
if (matchingTypes != null) {
510+
// multiple matches found, abort as we cannot reliably guess the correct one.
511+
matchingTypes = null;
512+
break;
513+
}
514+
515+
matchingTypes = actualTypes;
516+
}
517+
518+
if (matchingTypes == null) {
519+
return null;
520+
}
521+
522+
final List<ResultMapping> adjustedAutoTypeResultMappings = new ArrayList<>();
523+
for (int i = 0; i < resultMappings.size(); i++) {
524+
ResultMapping otherMapping = resultMappings.get(i);
525+
Class<?> identifiedMatchingJavaType = matchingTypes.get(i);
526+
527+
// given that we selected a new java type, overwrite the currently
528+
// selected type handler so it can get retrieved again from the registry
529+
adjustedAutoTypeResultMappings
530+
.add(new ResultMapping.Builder(otherMapping).javaType(identifiedMatchingJavaType).typeHandler(null).build());
531+
}
532+
533+
return adjustedAutoTypeResultMappings;
534+
}
470535
}

src/main/java/org/apache/ibatis/builder/annotation/MapperAnnotationBuilder.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2009-2024 the original author or authors.
2+
* Copyright 2009-2025 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.
@@ -29,6 +29,7 @@
2929
import java.util.HashMap;
3030
import java.util.List;
3131
import java.util.Map;
32+
import java.util.Objects;
3233
import java.util.Optional;
3334
import java.util.Properties;
3435
import java.util.Set;
@@ -514,6 +515,7 @@ private boolean hasNestedSelect(Result result) {
514515
}
515516

516517
private void applyConstructorArgs(Arg[] args, Class<?> resultType, List<ResultMapping> resultMappings) {
518+
final List<ResultMapping> mappings = new ArrayList<>();
517519
for (Arg arg : args) {
518520
List<ResultFlag> flags = new ArrayList<>();
519521
flags.add(ResultFlag.CONSTRUCTOR);
@@ -527,8 +529,11 @@ private void applyConstructorArgs(Arg[] args, Class<?> resultType, List<ResultMa
527529
nullOrEmpty(arg.column()), arg.javaType() == void.class ? null : arg.javaType(),
528530
arg.jdbcType() == JdbcType.UNDEFINED ? null : arg.jdbcType(), nullOrEmpty(arg.select()),
529531
nullOrEmpty(arg.resultMap()), null, nullOrEmpty(arg.columnPrefix()), typeHandler, flags, null, null, false);
530-
resultMappings.add(resultMapping);
532+
mappings.add(resultMapping);
531533
}
534+
535+
final List<ResultMapping> autoMappings = assistant.autoTypeResultMappingsForUnknownJavaTypes(resultType, mappings);
536+
resultMappings.addAll(Objects.requireNonNullElse(autoMappings, mappings));
532537
}
533538

534539
private String nullOrEmpty(String value) {

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2009-2024 the original author or authors.
2+
* Copyright 2009-2025 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.
@@ -23,6 +23,7 @@
2323
import java.util.HashMap;
2424
import java.util.List;
2525
import java.util.Map;
26+
import java.util.Objects;
2627
import java.util.Properties;
2728

2829
import org.apache.ibatis.builder.BaseBuilder;
@@ -267,14 +268,21 @@ protected Class<?> inheritEnclosingType(XNode resultMapNode, Class<?> enclosingT
267268

268269
private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) {
269270
List<XNode> argChildren = resultChild.getChildren();
271+
272+
final List<ResultMapping> mappings = new ArrayList<>();
270273
for (XNode argChild : argChildren) {
271274
List<ResultFlag> flags = new ArrayList<>();
272275
flags.add(ResultFlag.CONSTRUCTOR);
273276
if ("idArg".equals(argChild.getName())) {
274277
flags.add(ResultFlag.ID);
275278
}
276-
resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
279+
280+
mappings.add(buildResultMappingFromContext(argChild, resultType, flags));
277281
}
282+
283+
final List<ResultMapping> autoMappings = builderAssistant.autoTypeResultMappingsForUnknownJavaTypes(resultType,
284+
mappings);
285+
resultMappings.addAll(Objects.requireNonNullElse(autoMappings, mappings));
278286
}
279287

280288
private Discriminator processDiscriminatorElement(XNode context, Class<?> resultType,

src/main/java/org/apache/ibatis/mapping/ResultMap.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2009-2024 the original author or authors.
2+
* Copyright 2009-2025 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.
@@ -15,6 +15,13 @@
1515
*/
1616
package org.apache.ibatis.mapping;
1717

18+
import org.apache.ibatis.annotations.Param;
19+
import org.apache.ibatis.builder.BuilderException;
20+
import org.apache.ibatis.logging.Log;
21+
import org.apache.ibatis.logging.LogFactory;
22+
import org.apache.ibatis.reflection.ParamNameUtil;
23+
import org.apache.ibatis.session.Configuration;
24+
1825
import java.lang.annotation.Annotation;
1926
import java.lang.reflect.Constructor;
2027
import java.util.ArrayList;
@@ -24,13 +31,6 @@
2431
import java.util.Locale;
2532
import java.util.Set;
2633

27-
import org.apache.ibatis.annotations.Param;
28-
import org.apache.ibatis.builder.BuilderException;
29-
import org.apache.ibatis.logging.Log;
30-
import org.apache.ibatis.logging.LogFactory;
31-
import org.apache.ibatis.reflection.ParamNameUtil;
32-
import org.apache.ibatis.session.Configuration;
33-
3434
/**
3535
* @author Clinton Begin
3636
*/
@@ -137,7 +137,7 @@ public ResultMap build() {
137137
if (actualArgNames == null) {
138138
throw new BuilderException("Error in result map '" + resultMap.id + "'. Failed to find a constructor in '"
139139
+ resultMap.getType().getName() + "' with arg names " + constructorArgNames
140-
+ ". Note that 'javaType' is required when there is no writable property with the same name ('name' is optional, BTW). There might be more info in debug log.");
140+
+ ". Note that 'javaType' is required when there is ambiguous constructors or there is no writable property with the same name ('name' is optional, BTW). There might be more info in debug log.");
141141
}
142142
resultMap.constructorResultMappings.sort((o1, o2) -> {
143143
int paramIdx1 = actualArgNames.indexOf(o1.getProperty());

src/main/java/org/apache/ibatis/mapping/ResultMapping.java

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2009-2024 the original author or authors.
2+
* Copyright 2009-2025 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.
@@ -15,16 +15,16 @@
1515
*/
1616
package org.apache.ibatis.mapping;
1717

18-
import java.util.ArrayList;
19-
import java.util.Collections;
20-
import java.util.List;
21-
import java.util.Set;
22-
2318
import org.apache.ibatis.session.Configuration;
2419
import org.apache.ibatis.type.JdbcType;
2520
import org.apache.ibatis.type.TypeHandler;
2621
import org.apache.ibatis.type.TypeHandlerRegistry;
2722

23+
import java.util.ArrayList;
24+
import java.util.Collections;
25+
import java.util.List;
26+
import java.util.Set;
27+
2828
/**
2929
* @author Clinton Begin
3030
*/
@@ -72,6 +72,24 @@ public Builder(Configuration configuration, String property) {
7272
resultMapping.lazy = configuration.isLazyLoadingEnabled();
7373
}
7474

75+
public Builder(ResultMapping otherMapping) {
76+
this(otherMapping.configuration, otherMapping.property);
77+
78+
resultMapping.flags.addAll(otherMapping.flags);
79+
resultMapping.composites.addAll(otherMapping.composites);
80+
81+
resultMapping.column = otherMapping.column;
82+
resultMapping.javaType = otherMapping.javaType;
83+
resultMapping.jdbcType = otherMapping.jdbcType;
84+
resultMapping.typeHandler = otherMapping.typeHandler;
85+
resultMapping.nestedResultMapId = otherMapping.nestedResultMapId;
86+
resultMapping.nestedQueryId = otherMapping.nestedQueryId;
87+
resultMapping.notNullColumns = otherMapping.notNullColumns;
88+
resultMapping.columnPrefix = otherMapping.columnPrefix;
89+
resultMapping.resultSet = otherMapping.resultSet;
90+
resultMapping.foreignColumn = otherMapping.foreignColumn;
91+
}
92+
7593
public Builder javaType(Class<?> javaType) {
7694
resultMapping.javaType = javaType;
7795
return this;
@@ -243,18 +261,10 @@ public String getForeignColumn() {
243261
return foreignColumn;
244262
}
245263

246-
public void setForeignColumn(String foreignColumn) {
247-
this.foreignColumn = foreignColumn;
248-
}
249-
250264
public boolean isLazy() {
251265
return lazy;
252266
}
253267

254-
public void setLazy(boolean lazy) {
255-
this.lazy = lazy;
256-
}
257-
258268
public boolean isSimple() {
259269
return this.nestedResultMapId == null && this.nestedQueryId == null && this.resultSet == null;
260270
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2009-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.apache.ibatis.submitted.auto_type_from_non_ambiguous_constructor;
17+
18+
public record Account(long accountId, String accountName, String accountType) {
19+
20+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2009-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.apache.ibatis.submitted.auto_type_from_non_ambiguous_constructor;
17+
18+
public record Account2(long accountId, String accountName, String accountType) {
19+
20+
public Account2(long accountId, String accountName, String accountType, String extraInfo) {
21+
this(accountId, accountName, accountType);
22+
}
23+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2009-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.apache.ibatis.submitted.auto_type_from_non_ambiguous_constructor;
17+
18+
public record Account3(long accountId, String accountName, String accountType) {
19+
20+
public Account3(long accountId, int mismatch, String accountType) {
21+
this(accountId, "MismatchedAccountI", accountType);
22+
}
23+
24+
public Account3(long accountId, long mismatch, String accountType) {
25+
this(accountId, "MismatchedAccountL", accountType);
26+
}
27+
}

0 commit comments

Comments
 (0)