Skip to content

Commit e51e199

Browse files
committed
101: Add support for immutable collection constructor creation
- completely isolate new behaviour from existing via flag `experimentalConstructorCollectionMapping` - tested with multiple nested levels of mapping
1 parent 043270b commit e51e199

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2500
-200
lines changed

pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@
104104
<name>Tomáš Neuberg</name>
105105
<email>[email protected]</email>
106106
</contributor>
107+
<contributor>
108+
<name>Willie Scholtz</name>
109+
<email>[email protected]</email>
110+
</contributor>
107111
</contributors>
108112

109113
<scm>

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2009-2023 the original author or authors.
2+
* Copyright 2009-2024 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.
@@ -293,6 +293,8 @@ private void settingsElement(Properties props) {
293293
booleanValueOf(props.getProperty("argNameBasedConstructorAutoMapping"), false));
294294
configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType")));
295295
configuration.setNullableOnForEach(booleanValueOf(props.getProperty("nullableOnForEach"), false));
296+
configuration.setExperimentalConstructorCollectionMapping(
297+
booleanValueOf(props.getProperty("experimentalConstructorCollectionMapping"), false));
296298
}
297299

298300
private void environmentsElement(XNode context) throws Exception {

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

Lines changed: 239 additions & 18 deletions
Large diffs are not rendered by default.
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/*
2+
* Copyright 2009-2024 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.executor.resultset;
17+
18+
import java.lang.reflect.Constructor;
19+
import java.lang.reflect.ParameterizedType;
20+
import java.lang.reflect.Type;
21+
import java.util.ArrayList;
22+
import java.util.Collection;
23+
import java.util.Collections;
24+
import java.util.HashMap;
25+
import java.util.List;
26+
import java.util.Map;
27+
import java.util.Optional;
28+
import java.util.stream.Collectors;
29+
30+
import org.apache.ibatis.executor.ExecutorException;
31+
import org.apache.ibatis.mapping.ResultMap;
32+
import org.apache.ibatis.mapping.ResultMapping;
33+
import org.apache.ibatis.reflection.ReflectionException;
34+
import org.apache.ibatis.reflection.factory.ObjectFactory;
35+
36+
/**
37+
* Represents an object that is still to be created once all nested results with collection values have been gathered
38+
*
39+
* @author Willie Scholtz
40+
*/
41+
final class PendingConstructorCreation {
42+
43+
private final Class<?> resultType;
44+
private final List<Class<?>> constructorArgTypes;
45+
private final List<Object> constructorArgs;
46+
private final Map<Integer, PendingCreationMetaInfo> linkedCollectionMetaInfo;
47+
private final Map<String, Collection<Object>> linkedCollectionsByResultMapId;
48+
private final Map<String, List<PendingConstructorCreation>> linkedCreationsByResultMapId;
49+
50+
PendingConstructorCreation(Class<?> resultType, List<Class<?>> types, List<Object> args) {
51+
// since all our keys are based on result map id, we know we will never go over args size
52+
final int maxSize = types.size();
53+
this.linkedCollectionMetaInfo = new HashMap<>(maxSize);
54+
this.linkedCollectionsByResultMapId = new HashMap<>(maxSize);
55+
this.linkedCreationsByResultMapId = new HashMap<>(maxSize);
56+
this.resultType = resultType;
57+
this.constructorArgTypes = types;
58+
this.constructorArgs = args;
59+
}
60+
61+
@SuppressWarnings("unchecked")
62+
Collection<Object> initializeCollectionForResultMapping(ObjectFactory objectFactory, ResultMap resultMap,
63+
ResultMapping constructorMapping, Integer index) {
64+
final Class<?> parameterType = constructorMapping.getJavaType();
65+
if (!objectFactory.isCollection(parameterType)) {
66+
throw new ExecutorException(
67+
"Cannot add a collection result to non-collection based resultMapping: " + constructorMapping);
68+
}
69+
70+
final String resultMapId = constructorMapping.getNestedResultMapId();
71+
return linkedCollectionsByResultMapId.computeIfAbsent(resultMapId, (k) -> {
72+
// this will allow us to verify the types of the collection before creating the final object
73+
linkedCollectionMetaInfo.put(index, new PendingCreationMetaInfo(resultMap.getType(), resultMapId));
74+
75+
// will be checked before we finally create the object) as we cannot reliably do that here
76+
return (Collection<Object>) objectFactory.create(parameterType);
77+
});
78+
}
79+
80+
void linkCreation(ResultMap nestedResultMap, PendingConstructorCreation pcc) {
81+
final String resultMapId = nestedResultMap.getId();
82+
final List<PendingConstructorCreation> pendingConstructorCreations = linkedCreationsByResultMapId
83+
.computeIfAbsent(resultMapId, (k) -> new ArrayList<>());
84+
85+
if (pendingConstructorCreations.contains(pcc)) {
86+
throw new ExecutorException("Cannot link inner pcc with same value, MyBatis programming error!");
87+
}
88+
89+
pendingConstructorCreations.add(pcc);
90+
}
91+
92+
void linkCollectionValue(ResultMapping constructorMapping, Object value) {
93+
// not necessary to add null results to the collection (is this a config flag?)
94+
if (value == null) {
95+
return;
96+
}
97+
98+
final String resultMapId = constructorMapping.getNestedResultMapId();
99+
if (!linkedCollectionsByResultMapId.containsKey(resultMapId)) {
100+
throw new ExecutorException("Cannot link collection value for resultMapping: " + constructorMapping
101+
+ ", resultMap has not been seen/initialized yet! Internal error");
102+
}
103+
104+
linkedCollectionsByResultMapId.get(resultMapId).add(value);
105+
}
106+
107+
/**
108+
* Verifies preconditions before we can actually create the result object, this is more of a sanity check to ensure
109+
* all the mappings are as we expect them to be.
110+
*
111+
* @param objectFactory
112+
* the object factory
113+
*/
114+
private void verifyCanCreate(ObjectFactory objectFactory) {
115+
// before we create, we need to get the constructor to be used and verify our types match
116+
// since we added to the collection completely unchecked
117+
final Constructor<?> resolvedConstructor = resolveConstructor(resultType, constructorArgTypes);
118+
final Type[] genericParameterTypes = resolvedConstructor.getGenericParameterTypes();
119+
for (int i = 0; i < genericParameterTypes.length; i++) {
120+
if (!linkedCollectionMetaInfo.containsKey(i)) {
121+
continue;
122+
}
123+
124+
final PendingCreationMetaInfo creationMetaInfo = linkedCollectionMetaInfo.get(i);
125+
final Class<?> resolvedItemType = checkResolvedItemType(creationMetaInfo, genericParameterTypes[i]);
126+
127+
// ensure we have an empty collection if there are linked creations for this arg
128+
final String resultMapId = creationMetaInfo.getResultMapId();
129+
if (linkedCreationsByResultMapId.containsKey(resultMapId)) {
130+
final Object emptyCollection = constructorArgs.get(i);
131+
if (emptyCollection == null || !objectFactory.isCollection(emptyCollection.getClass())) {
132+
throw new ExecutorException(
133+
"Expected empty collection for '" + resolvedItemType + "', this is a MyBatis internal error!");
134+
}
135+
} else {
136+
final Object linkedCollection = constructorArgs.get(i);
137+
if (!linkedCollectionsByResultMapId.containsKey(resultMapId)) {
138+
throw new ExecutorException("Expected linked collection for resultMap '" + resultMapId
139+
+ "', not found! this is a MyBatis internal error!");
140+
}
141+
142+
// comparing memory locations here (we rely on that fact)
143+
if (linkedCollection != linkedCollectionsByResultMapId.get(resultMapId)) {
144+
throw new ExecutorException("Expected linked collection in creation to be the same as arg for resultMap '"
145+
+ resultMapId + "', not equal! this is a MyBatis internal error!");
146+
}
147+
}
148+
}
149+
}
150+
151+
private static <T> Constructor<T> resolveConstructor(Class<T> type, List<Class<?>> constructorArgTypes) {
152+
try {
153+
if (constructorArgTypes == null) {
154+
return type.getDeclaredConstructor();
155+
}
156+
157+
return type.getDeclaredConstructor(constructorArgTypes.toArray(new Class[0]));
158+
} catch (Exception e) {
159+
String argTypes = Optional.ofNullable(constructorArgTypes).orElseGet(Collections::emptyList).stream()
160+
.map(Class::getSimpleName).collect(Collectors.joining(","));
161+
throw new ReflectionException(
162+
"Error resolving constructor for " + type + " with invalid types (" + argTypes + ") . Cause: " + e, e);
163+
}
164+
}
165+
166+
private static Class<?> checkResolvedItemType(PendingCreationMetaInfo creationMetaInfo, Type genericParameterTypes) {
167+
final ParameterizedType genericParameterType = (ParameterizedType) genericParameterTypes;
168+
final Class<?> expectedType = (Class<?>) genericParameterType.getActualTypeArguments()[0];
169+
final Class<?> resolvedItemType = creationMetaInfo.getArgumentType();
170+
171+
if (!expectedType.isAssignableFrom(resolvedItemType)) {
172+
throw new ReflectionException(
173+
"Expected type '" + resolvedItemType + "', while the actual type of the collection was '" + expectedType
174+
+ "', ensure your resultMap matches the type of the collection you are trying to inject");
175+
}
176+
177+
return resolvedItemType;
178+
}
179+
180+
@Override
181+
public String toString() {
182+
return "PendingConstructorCreation(" + this.hashCode() + "){" + "resultType=" + resultType + '}';
183+
}
184+
185+
/**
186+
* Recursively creates the final result of this creation.
187+
*
188+
* @param objectFactory
189+
* the object factory
190+
* @param verifyCreate
191+
* should we verify this object can be created, should only be needed once
192+
*
193+
* @return the new immutable result
194+
*/
195+
Object create(ObjectFactory objectFactory, boolean verifyCreate) {
196+
if (verifyCreate) {
197+
verifyCanCreate(objectFactory);
198+
}
199+
200+
final List<Object> newArguments = new ArrayList<>(constructorArgs.size());
201+
for (int i = 0; i < constructorArgs.size(); i++) {
202+
final PendingCreationMetaInfo creationMetaInfo = linkedCollectionMetaInfo.get(i);
203+
final Object existingArg = constructorArgs.get(i);
204+
205+
if (creationMetaInfo == null) {
206+
// we are not aware of this argument wrt pending creations
207+
newArguments.add(existingArg);
208+
continue;
209+
}
210+
211+
// time to finally build this collection
212+
final String resultMapId = creationMetaInfo.getResultMapId();
213+
if (linkedCreationsByResultMapId.containsKey(resultMapId)) {
214+
@SuppressWarnings("unchecked")
215+
final Collection<Object> emptyCollection = (Collection<Object>) existingArg;
216+
final List<PendingConstructorCreation> linkedCreations = linkedCreationsByResultMapId.get(resultMapId);
217+
218+
for (PendingConstructorCreation linkedCreation : linkedCreations) {
219+
emptyCollection.add(linkedCreation.create(objectFactory, verifyCreate));
220+
}
221+
222+
newArguments.add(emptyCollection);
223+
continue;
224+
}
225+
226+
// handle the base collection (it was built inline already)
227+
newArguments.add(existingArg);
228+
}
229+
230+
return objectFactory.create(resultType, constructorArgTypes, newArguments);
231+
}
232+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2009-2024 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.executor.resultset;
17+
18+
/**
19+
* @author Willie Scholtz
20+
*/
21+
final class PendingCreationMetaInfo {
22+
private final Class<?> argumentType;
23+
private final String resultMapId;
24+
25+
PendingCreationMetaInfo(Class<?> argumentType, String resultMapId) {
26+
this.argumentType = argumentType;
27+
this.resultMapId = resultMapId;
28+
}
29+
30+
Class<?> getArgumentType() {
31+
return argumentType;
32+
}
33+
34+
String getResultMapId() {
35+
return resultMapId;
36+
}
37+
38+
@Override
39+
public String toString() {
40+
return "PendingCreationMetaInfo{" + "argumentType=" + argumentType + ", resultMapId='" + resultMapId + '\'' + '}';
41+
}
42+
}

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2009-2023 the original author or authors.
2+
* Copyright 2009-2024 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.
@@ -46,6 +46,7 @@ public class ResultMap {
4646
private Set<String> mappedColumns;
4747
private Set<String> mappedProperties;
4848
private Discriminator discriminator;
49+
private boolean hasResultMapsUsingConstructorCollection;
4950
private boolean hasNestedResultMaps;
5051
private boolean hasNestedQueries;
5152
private Boolean autoMapping;
@@ -111,6 +112,15 @@ public ResultMap build() {
111112
}
112113
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
113114
resultMap.constructorResultMappings.add(resultMapping);
115+
116+
// #101
117+
if (resultMap.configuration.isExperimentalConstructorCollectionMappingEnabled()) {
118+
Class<?> javaType = resultMapping.getJavaType();
119+
resultMap.hasResultMapsUsingConstructorCollection = resultMap.hasResultMapsUsingConstructorCollection
120+
|| (resultMapping.getNestedQueryId() == null && javaType != null
121+
&& resultMap.configuration.getObjectFactory().isCollection(javaType));
122+
}
123+
114124
if (resultMapping.getProperty() != null) {
115125
constructorArgNames.add(resultMapping.getProperty());
116126
}
@@ -210,6 +220,10 @@ public String getId() {
210220
return id;
211221
}
212222

223+
public boolean hasResultMapsUsingConstructorCollection() {
224+
return hasResultMapsUsingConstructorCollection;
225+
}
226+
213227
public boolean hasNestedResultMaps() {
214228
return hasNestedResultMaps;
215229
}

src/main/java/org/apache/ibatis/session/Configuration.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ public class Configuration {
118118
protected boolean shrinkWhitespacesInSql;
119119
protected boolean nullableOnForEach;
120120
protected boolean argNameBasedConstructorAutoMapping;
121+
protected boolean experimentalConstructorCollectionMapping;
121122

122123
protected String logPrefix;
123124
protected Class<? extends Log> logImpl;
@@ -370,6 +371,14 @@ public boolean isSafeRowBoundsEnabled() {
370371
return safeRowBoundsEnabled;
371372
}
372373

374+
public void setExperimentalConstructorCollectionMapping(boolean experimentalConstructorCollectionMapping) {
375+
this.experimentalConstructorCollectionMapping = experimentalConstructorCollectionMapping;
376+
}
377+
378+
public boolean isExperimentalConstructorCollectionMappingEnabled() {
379+
return experimentalConstructorCollectionMapping;
380+
}
381+
373382
public void setSafeRowBoundsEnabled(boolean safeRowBoundsEnabled) {
374383
this.safeRowBoundsEnabled = safeRowBoundsEnabled;
375384
}

0 commit comments

Comments
 (0)