Skip to content

Commit d4af490

Browse files
authored
Merge pull request #3378 from epochcoder/feat/2618-auto-determine-type-when-missing
When `javaType` has not (or partially) been specified, determine the best matching constructor
2 parents 1b2346c + add7b26 commit d4af490

23 files changed

+1425
-117
lines changed

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

+1-2
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.
@@ -466,5 +466,4 @@ private Class<?> resolveParameterJavaType(Class<?> resultType, String property,
466466
}
467467
return javaType;
468468
}
469-
470469
}

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

+382
Large diffs are not rendered by default.

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

+22-15
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.
@@ -63,6 +63,7 @@
6363
import org.apache.ibatis.builder.CacheRefResolver;
6464
import org.apache.ibatis.builder.IncompleteElementException;
6565
import org.apache.ibatis.builder.MapperBuilderAssistant;
66+
import org.apache.ibatis.builder.ResultMappingConstructorResolver;
6667
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
6768
import org.apache.ibatis.cursor.Cursor;
6869
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
@@ -237,7 +238,7 @@ private String generateResultMapName(Method method) {
237238
private void applyResultMap(String resultMapId, Class<?> returnType, Arg[] args, Result[] results,
238239
TypeDiscriminator discriminator) {
239240
List<ResultMapping> resultMappings = new ArrayList<>();
240-
applyConstructorArgs(args, returnType, resultMappings);
241+
applyConstructorArgs(args, returnType, resultMappings, resultMapId);
241242
applyResults(results, returnType, resultMappings);
242243
Discriminator disc = applyDiscriminator(resultMapId, returnType, discriminator);
243244
// TODO add AutoMappingBehaviour
@@ -251,7 +252,7 @@ private void createDiscriminatorResultMaps(String resultMapId, Class<?> resultTy
251252
String caseResultMapId = resultMapId + "-" + c.value();
252253
List<ResultMapping> resultMappings = new ArrayList<>();
253254
// issue #136
254-
applyConstructorArgs(c.constructArgs(), resultType, resultMappings);
255+
applyConstructorArgs(c.constructArgs(), resultType, resultMappings, resultMapId);
255256
applyResults(c.results(), resultType, resultMappings);
256257
// TODO add AutoMappingBehaviour
257258
assistant.addResultMap(caseResultMapId, c.type(), resultMapId, null, resultMappings, null);
@@ -461,15 +462,15 @@ private void applyResults(Result[] results, Class<?> resultType, List<ResultMapp
461462

462463
private String findColumnPrefix(Result result) {
463464
String columnPrefix = result.one().columnPrefix();
464-
if (columnPrefix.length() < 1) {
465+
if (columnPrefix.isEmpty()) {
465466
columnPrefix = result.many().columnPrefix();
466467
}
467468
return columnPrefix;
468469
}
469470

470471
private String nestedResultMapId(Result result) {
471472
String resultMapId = result.one().resultMap();
472-
if (resultMapId.length() < 1) {
473+
if (resultMapId.isEmpty()) {
473474
resultMapId = result.many().resultMap();
474475
}
475476
if (!resultMapId.contains(".")) {
@@ -479,15 +480,15 @@ private String nestedResultMapId(Result result) {
479480
}
480481

481482
private boolean hasNestedResultMap(Result result) {
482-
if (result.one().resultMap().length() > 0 && result.many().resultMap().length() > 0) {
483+
if (!result.one().resultMap().isEmpty() && !result.many().resultMap().isEmpty()) {
483484
throw new BuilderException("Cannot use both @One and @Many annotations in the same @Result");
484485
}
485-
return result.one().resultMap().length() > 0 || result.many().resultMap().length() > 0;
486+
return !result.one().resultMap().isEmpty() || !result.many().resultMap().isEmpty();
486487
}
487488

488489
private String nestedSelectId(Result result) {
489490
String nestedSelect = result.one().select();
490-
if (nestedSelect.length() < 1) {
491+
if (nestedSelect.isEmpty()) {
491492
nestedSelect = result.many().select();
492493
}
493494
if (!nestedSelect.contains(".")) {
@@ -498,22 +499,24 @@ private String nestedSelectId(Result result) {
498499

499500
private boolean isLazy(Result result) {
500501
boolean isLazy = configuration.isLazyLoadingEnabled();
501-
if (result.one().select().length() > 0 && FetchType.DEFAULT != result.one().fetchType()) {
502+
if (!result.one().select().isEmpty() && FetchType.DEFAULT != result.one().fetchType()) {
502503
isLazy = result.one().fetchType() == FetchType.LAZY;
503-
} else if (result.many().select().length() > 0 && FetchType.DEFAULT != result.many().fetchType()) {
504+
} else if (!result.many().select().isEmpty() && FetchType.DEFAULT != result.many().fetchType()) {
504505
isLazy = result.many().fetchType() == FetchType.LAZY;
505506
}
506507
return isLazy;
507508
}
508509

509510
private boolean hasNestedSelect(Result result) {
510-
if (result.one().select().length() > 0 && result.many().select().length() > 0) {
511+
if (!result.one().select().isEmpty() && !result.many().select().isEmpty()) {
511512
throw new BuilderException("Cannot use both @One and @Many annotations in the same @Result");
512513
}
513-
return result.one().select().length() > 0 || result.many().select().length() > 0;
514+
return !result.one().select().isEmpty() || !result.many().select().isEmpty();
514515
}
515516

516-
private void applyConstructorArgs(Arg[] args, Class<?> resultType, List<ResultMapping> resultMappings) {
517+
private void applyConstructorArgs(Arg[] args, Class<?> resultType, List<ResultMapping> resultMappings,
518+
String resultMapId) {
519+
final List<ResultMapping> mappings = new ArrayList<>();
517520
for (Arg arg : args) {
518521
List<ResultFlag> flags = new ArrayList<>();
519522
flags.add(ResultFlag.CONSTRUCTOR);
@@ -527,12 +530,16 @@ private void applyConstructorArgs(Arg[] args, Class<?> resultType, List<ResultMa
527530
nullOrEmpty(arg.column()), arg.javaType() == void.class ? null : arg.javaType(),
528531
arg.jdbcType() == JdbcType.UNDEFINED ? null : arg.jdbcType(), nullOrEmpty(arg.select()),
529532
nullOrEmpty(arg.resultMap()), null, nullOrEmpty(arg.columnPrefix()), typeHandler, flags, null, null, false);
530-
resultMappings.add(resultMapping);
533+
mappings.add(resultMapping);
531534
}
535+
536+
final ResultMappingConstructorResolver resolver = new ResultMappingConstructorResolver(configuration, mappings,
537+
resultType, resultMapId);
538+
resultMappings.addAll(resolver.resolveWithConstructor());
532539
}
533540

534541
private String nullOrEmpty(String value) {
535-
return value == null || value.trim().length() == 0 ? null : value;
542+
return value == null || value.trim().isEmpty() ? null : value;
536543
}
537544

538545
private KeyGenerator handleSelectKeyAnnotation(SelectKey selectKeyAnnotation, String baseStatementId,

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

+19-7
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.
@@ -31,6 +31,7 @@
3131
import org.apache.ibatis.builder.IncompleteElementException;
3232
import org.apache.ibatis.builder.MapperBuilderAssistant;
3333
import org.apache.ibatis.builder.ResultMapResolver;
34+
import org.apache.ibatis.builder.ResultMappingConstructorResolver;
3435
import org.apache.ibatis.cache.Cache;
3536
import org.apache.ibatis.executor.ErrorContext;
3637
import org.apache.ibatis.io.Resources;
@@ -223,12 +224,17 @@ private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> addi
223224
if (typeClass == null) {
224225
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
225226
}
227+
228+
String id = resultMapNode.getStringAttribute("id", resultMapNode::getValueBasedIdentifier);
229+
String extend = resultMapNode.getStringAttribute("extends");
230+
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
231+
226232
Discriminator discriminator = null;
227233
List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
228234
List<XNode> resultChildren = resultMapNode.getChildren();
229235
for (XNode resultChild : resultChildren) {
230236
if ("constructor".equals(resultChild.getName())) {
231-
processConstructorElement(resultChild, typeClass, resultMappings);
237+
processConstructorElement(resultChild, typeClass, resultMappings, id);
232238
} else if ("discriminator".equals(resultChild.getName())) {
233239
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
234240
} else {
@@ -239,9 +245,7 @@ private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> addi
239245
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
240246
}
241247
}
242-
String id = resultMapNode.getStringAttribute("id", resultMapNode::getValueBasedIdentifier);
243-
String extend = resultMapNode.getStringAttribute("extends");
244-
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
248+
245249
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator,
246250
resultMappings, autoMapping);
247251
try {
@@ -265,16 +269,24 @@ protected Class<?> inheritEnclosingType(XNode resultMapNode, Class<?> enclosingT
265269
return null;
266270
}
267271

268-
private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) {
272+
private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings,
273+
String id) {
269274
List<XNode> argChildren = resultChild.getChildren();
275+
276+
final List<ResultMapping> mappings = new ArrayList<>();
270277
for (XNode argChild : argChildren) {
271278
List<ResultFlag> flags = new ArrayList<>();
272279
flags.add(ResultFlag.CONSTRUCTOR);
273280
if ("idArg".equals(argChild.getName())) {
274281
flags.add(ResultFlag.ID);
275282
}
276-
resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
283+
284+
mappings.add(buildResultMappingFromContext(argChild, resultType, flags));
277285
}
286+
287+
final ResultMappingConstructorResolver resolver = new ResultMappingConstructorResolver(configuration, mappings,
288+
resultType, id);
289+
resultMappings.addAll(resolver.resolveWithConstructor());
278290
}
279291

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

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

+9-86
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,13 @@
1515
*/
1616
package org.apache.ibatis.mapping;
1717

18-
import java.lang.annotation.Annotation;
19-
import java.lang.reflect.Constructor;
2018
import java.util.ArrayList;
2119
import java.util.Collections;
2220
import java.util.HashSet;
2321
import java.util.List;
2422
import java.util.Locale;
2523
import java.util.Set;
2624

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;
3225
import org.apache.ibatis.session.Configuration;
3326

3427
/**
@@ -55,8 +48,6 @@ private ResultMap() {
5548
}
5649

5750
public static class Builder {
58-
private static final Log log = LogFactory.getLog(Builder.class);
59-
6051
private final ResultMap resultMap = new ResultMap();
6152

6253
public Builder(Configuration configuration, String id, Class<?> type, List<ResultMapping> resultMappings) {
@@ -85,16 +76,18 @@ public ResultMap build() {
8576
if (resultMap.id == null) {
8677
throw new IllegalArgumentException("ResultMaps must have an id");
8778
}
79+
8880
resultMap.mappedColumns = new HashSet<>();
8981
resultMap.mappedProperties = new HashSet<>();
9082
resultMap.idResultMappings = new ArrayList<>();
9183
resultMap.constructorResultMappings = new ArrayList<>();
9284
resultMap.propertyResultMappings = new ArrayList<>();
93-
final List<String> constructorArgNames = new ArrayList<>();
85+
9486
for (ResultMapping resultMapping : resultMap.resultMappings) {
9587
resultMap.hasNestedQueries = resultMap.hasNestedQueries || resultMapping.getNestedQueryId() != null;
9688
resultMap.hasNestedResultMaps = resultMap.hasNestedResultMaps
9789
|| resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null;
90+
9891
final String column = resultMapping.getColumn();
9992
if (column != null) {
10093
resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));
@@ -106,10 +99,12 @@ public ResultMap build() {
10699
}
107100
}
108101
}
102+
109103
final String property = resultMapping.getProperty();
110104
if (property != null) {
111105
resultMap.mappedProperties.add(property);
112106
}
107+
113108
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
114109
resultMap.constructorResultMappings.add(resultMapping);
115110

@@ -118,99 +113,27 @@ public ResultMap build() {
118113
resultMap.hasResultMapsUsingConstructorCollection = resultMap.hasResultMapsUsingConstructorCollection
119114
|| (resultMapping.getNestedQueryId() == null && resultMapping.getTypeHandler() == null && javaType != null
120115
&& resultMap.configuration.getObjectFactory().isCollection(javaType));
121-
122-
if (resultMapping.getProperty() != null) {
123-
constructorArgNames.add(resultMapping.getProperty());
124-
}
125116
} else {
126117
resultMap.propertyResultMappings.add(resultMapping);
127118
}
119+
128120
if (resultMapping.getFlags().contains(ResultFlag.ID)) {
129121
resultMap.idResultMappings.add(resultMapping);
130122
}
131123
}
124+
132125
if (resultMap.idResultMappings.isEmpty()) {
133126
resultMap.idResultMappings.addAll(resultMap.resultMappings);
134127
}
135-
if (!constructorArgNames.isEmpty()) {
136-
final List<String> actualArgNames = argNamesOfMatchingConstructor(constructorArgNames);
137-
if (actualArgNames == null) {
138-
throw new BuilderException("Error in result map '" + resultMap.id + "'. Failed to find a constructor in '"
139-
+ 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.");
141-
}
142-
resultMap.constructorResultMappings.sort((o1, o2) -> {
143-
int paramIdx1 = actualArgNames.indexOf(o1.getProperty());
144-
int paramIdx2 = actualArgNames.indexOf(o2.getProperty());
145-
return paramIdx1 - paramIdx2;
146-
});
147-
}
128+
148129
// lock down collections
149130
resultMap.resultMappings = Collections.unmodifiableList(resultMap.resultMappings);
150131
resultMap.idResultMappings = Collections.unmodifiableList(resultMap.idResultMappings);
151132
resultMap.constructorResultMappings = Collections.unmodifiableList(resultMap.constructorResultMappings);
152133
resultMap.propertyResultMappings = Collections.unmodifiableList(resultMap.propertyResultMappings);
153134
resultMap.mappedColumns = Collections.unmodifiableSet(resultMap.mappedColumns);
154-
return resultMap;
155-
}
156-
157-
private List<String> argNamesOfMatchingConstructor(List<String> constructorArgNames) {
158-
Constructor<?>[] constructors = resultMap.type.getDeclaredConstructors();
159-
for (Constructor<?> constructor : constructors) {
160-
Class<?>[] paramTypes = constructor.getParameterTypes();
161-
if (constructorArgNames.size() == paramTypes.length) {
162-
List<String> paramNames = getArgNames(constructor);
163-
if (constructorArgNames.containsAll(paramNames)
164-
&& argTypesMatch(constructorArgNames, paramTypes, paramNames)) {
165-
return paramNames;
166-
}
167-
}
168-
}
169-
return null;
170-
}
171-
172-
private boolean argTypesMatch(final List<String> constructorArgNames, Class<?>[] paramTypes,
173-
List<String> paramNames) {
174-
for (int i = 0; i < constructorArgNames.size(); i++) {
175-
Class<?> actualType = paramTypes[paramNames.indexOf(constructorArgNames.get(i))];
176-
Class<?> specifiedType = resultMap.constructorResultMappings.get(i).getJavaType();
177-
if (!actualType.equals(specifiedType)) {
178-
if (log.isDebugEnabled()) {
179-
log.debug("While building result map '" + resultMap.id + "', found a constructor with arg names "
180-
+ constructorArgNames + ", but the type of '" + constructorArgNames.get(i)
181-
+ "' did not match. Specified: [" + specifiedType.getName() + "] Declared: [" + actualType.getName()
182-
+ "]");
183-
}
184-
return false;
185-
}
186-
}
187-
return true;
188-
}
189135

190-
private List<String> getArgNames(Constructor<?> constructor) {
191-
List<String> paramNames = new ArrayList<>();
192-
List<String> actualParamNames = null;
193-
final Annotation[][] paramAnnotations = constructor.getParameterAnnotations();
194-
int paramCount = paramAnnotations.length;
195-
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
196-
String name = null;
197-
for (Annotation annotation : paramAnnotations[paramIndex]) {
198-
if (annotation instanceof Param) {
199-
name = ((Param) annotation).value();
200-
break;
201-
}
202-
}
203-
if (name == null && resultMap.configuration.isUseActualParamName()) {
204-
if (actualParamNames == null) {
205-
actualParamNames = ParamNameUtil.getParamNames(constructor);
206-
}
207-
if (actualParamNames.size() > paramIndex) {
208-
name = actualParamNames.get(paramIndex);
209-
}
210-
}
211-
paramNames.add(name != null ? name : "arg" + paramIndex);
212-
}
213-
return paramNames;
136+
return resultMap;
214137
}
215138
}
216139

0 commit comments

Comments
 (0)