Skip to content

Commit 558c8a1

Browse files
committed
mybatis#101: Pass CollectionInConstructorTest
- link simple objects - awareness of constructor mapping prefix for PendingConstructorCreation - don't verify creation when custom object factory is used
1 parent 7fdfb4d commit 558c8a1

File tree

7 files changed

+174
-46
lines changed

7 files changed

+174
-46
lines changed

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

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -360,13 +360,26 @@ protected void checkResultHandler() {
360360

361361
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap,
362362
ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
363+
final boolean useCollectionConstructorInjection = resultMap.hasResultMapsUsingConstructorCollection();
364+
PendingConstructorCreation lastHandledCreation = null;
365+
363366
DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
364367
ResultSet resultSet = rsw.getResultSet();
365368
skipRows(resultSet, rowBounds);
366369
while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
367370
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
368371
Object rowValue = getRowValue(rsw, discriminatedResultMap, null, null);
369-
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
372+
if (!useCollectionConstructorInjection) {
373+
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
374+
} else {
375+
if (!(rowValue instanceof PendingConstructorCreation)) {
376+
throw new ExecutorException("Expected result object to be a pending constructor creation!");
377+
}
378+
379+
createAndStorePendingCreation(resultHandler, resultSet, resultContext, (PendingConstructorCreation) rowValue,
380+
lastHandledCreation == null);
381+
lastHandledCreation = (PendingConstructorCreation) rowValue;
382+
}
370383
}
371384
}
372385

@@ -422,6 +435,17 @@ private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String col
422435
foundValues = lazyLoader.size() > 0 || foundValues;
423436
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
424437
}
438+
439+
if (parentRowKey != null) {
440+
// found a simple object/primitive in pending constructor creation that will need linking later
441+
final CacheKey rowKey = createRowKey(resultMap, rsw, columnPrefix);
442+
final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);
443+
444+
if (combinedKey != CacheKey.NULL_CACHE_KEY) {
445+
nestedResultObjects.put(combinedKey, rowValue);
446+
}
447+
}
448+
425449
return rowValue;
426450
}
427451

@@ -1101,6 +1125,11 @@ && shouldProcessMoreRows(resultContext, rowBounds)) {
11011125
private void linkNestedPendingCreations(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix,
11021126
CacheKey parentRowKey, PendingConstructorCreation pendingCreation, List<Object> constructorArgs)
11031127
throws SQLException {
1128+
if (parentRowKey == null) {
1129+
// nothing to link, possibly due to simple (non-nested) result map
1130+
return;
1131+
}
1132+
11041133
final CacheKey rowKey = createRowKey(resultMap, rsw, columnPrefix);
11051134
final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);
11061135

@@ -1157,7 +1186,7 @@ private void linkNestedPendingCreations(ResultSetWrapper rsw, ResultMap resultMa
11571186
// we will fill this collection when building the final object
11581187
constructorArgs.set(index, value);
11591188
// link the creation for building later
1160-
pendingCreation.linkCreation(nestedResultMap, innerCreation);
1189+
pendingCreation.linkCreation(constructorMapping, innerCreation);
11611190
}
11621191
}
11631192
}
@@ -1190,6 +1219,9 @@ private boolean applyNestedPendingConstructorCreations(ResultSetWrapper rsw, Res
11901219
PendingConstructorCreation pendingConstructorCreation = null;
11911220
if (rowValue instanceof PendingConstructorCreation) {
11921221
pendingConstructorCreation = (PendingConstructorCreation) rowValue;
1222+
} else if (rowValue != null) {
1223+
// found a simple object that was already linked/handled
1224+
continue;
11931225
}
11941226

11951227
final boolean newValueForNestedResultMap = pendingConstructorCreation == null;
@@ -1213,7 +1245,7 @@ private boolean applyNestedPendingConstructorCreations(ResultSetWrapper rsw, Res
12131245
if (rowValue instanceof PendingConstructorCreation) {
12141246
if (newValueForNestedResultMap) {
12151247
// we created a brand new pcc. this is a new collection value
1216-
pendingConstructorCreation.linkCreation(nestedResultMap, (PendingConstructorCreation) rowValue);
1248+
pendingConstructorCreation.linkCreation(constructorMapping, (PendingConstructorCreation) rowValue);
12171249
foundValues = true;
12181250
}
12191251
} else {
@@ -1225,10 +1257,11 @@ private boolean applyNestedPendingConstructorCreations(ResultSetWrapper rsw, Res
12251257
}
12261258
}
12271259
} catch (SQLException e) {
1228-
throw new ExecutorException("Error getting experimental nested result map values for '"
1260+
throw new ExecutorException("Error getting constructor collection nested result map values for '"
12291261
+ constructorMapping.getProperty() + "'. Cause: " + e, e);
12301262
}
12311263
}
1264+
12321265
return foundValues;
12331266
}
12341267

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

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.apache.ibatis.mapping.ResultMap;
3232
import org.apache.ibatis.mapping.ResultMapping;
3333
import org.apache.ibatis.reflection.ReflectionException;
34+
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
3435
import org.apache.ibatis.reflection.factory.ObjectFactory;
3536

3637
/**
@@ -43,16 +44,19 @@ final class PendingConstructorCreation {
4344
private final Class<?> resultType;
4445
private final List<Class<?>> constructorArgTypes;
4546
private final List<Object> constructorArgs;
47+
4648
private final Map<Integer, PendingCreationMetaInfo> linkedCollectionMetaInfo;
47-
private final Map<String, Collection<Object>> linkedCollectionsByResultMapId;
48-
private final Map<String, List<PendingConstructorCreation>> linkedCreationsByResultMapId;
49+
private final Map<PendingCreationKey, Collection<Object>> linkedCollectionsByKey;
50+
private final Map<PendingCreationKey, List<PendingConstructorCreation>> linkedCreationsByKey;
4951

5052
PendingConstructorCreation(Class<?> resultType, List<Class<?>> types, List<Object> args) {
5153
// since all our keys are based on result map id, we know we will never go over args size
5254
final int maxSize = types.size();
55+
5356
this.linkedCollectionMetaInfo = new HashMap<>(maxSize);
54-
this.linkedCollectionsByResultMapId = new HashMap<>(maxSize);
55-
this.linkedCreationsByResultMapId = new HashMap<>(maxSize);
57+
this.linkedCollectionsByKey = new HashMap<>(maxSize);
58+
this.linkedCreationsByKey = new HashMap<>(maxSize);
59+
5660
this.resultType = resultType;
5761
this.constructorArgTypes = types;
5862
this.constructorArgs = args;
@@ -63,55 +67,63 @@ Collection<Object> initializeCollectionForResultMapping(ObjectFactory objectFact
6367
ResultMapping constructorMapping, Integer index) {
6468
final Class<?> parameterType = constructorMapping.getJavaType();
6569
if (!objectFactory.isCollection(parameterType)) {
66-
throw new ExecutorException(
70+
throw new ReflectionException(
6771
"Cannot add a collection result to non-collection based resultMapping: " + constructorMapping);
6872
}
6973

70-
final String resultMapId = constructorMapping.getNestedResultMapId();
71-
return linkedCollectionsByResultMapId.computeIfAbsent(resultMapId, (k) -> {
74+
final PendingCreationKey creationKey = new PendingCreationKey(constructorMapping);
75+
return linkedCollectionsByKey.computeIfAbsent(creationKey, (k) -> {
7276
// 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));
77+
linkedCollectionMetaInfo.put(index, new PendingCreationMetaInfo(resultMap.getType(), creationKey));
7478

7579
// will be checked before we finally create the object) as we cannot reliably do that here
7680
return (Collection<Object>) objectFactory.create(parameterType);
7781
});
7882
}
7983

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+
void linkCreation(ResultMapping constructorMapping, PendingConstructorCreation pcc) {
85+
final PendingCreationKey creationKey = new PendingCreationKey(constructorMapping);
86+
final List<PendingConstructorCreation> pendingConstructorCreations = linkedCreationsByKey
87+
.computeIfAbsent(creationKey, (k) -> new ArrayList<>());
8488

8589
if (pendingConstructorCreations.contains(pcc)) {
86-
throw new ExecutorException("Cannot link inner pcc with same value, MyBatis programming error!");
90+
throw new ExecutorException("Cannot link inner constructor creation with same value, MyBatis internal error!");
8791
}
8892

8993
pendingConstructorCreations.add(pcc);
9094
}
9195

9296
void linkCollectionValue(ResultMapping constructorMapping, Object value) {
93-
// not necessary to add null results to the collection (is this a config flag?)
97+
// not necessary to add null results to the collection
9498
if (value == null) {
9599
return;
96100
}
97101

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+
final PendingCreationKey creationKey = new PendingCreationKey(constructorMapping);
103+
if (!linkedCollectionsByKey.containsKey(creationKey)) {
104+
throw new ExecutorException("Cannot link collection value for key: " + constructorMapping
105+
+ ", resultMap has not been seen/initialized yet! Mybatis internal error!");
102106
}
103107

104-
linkedCollectionsByResultMapId.get(resultMapId).add(value);
108+
linkedCollectionsByKey.get(creationKey).add(value);
105109
}
106110

107111
/**
108112
* Verifies preconditions before we can actually create the result object, this is more of a sanity check to ensure
109113
* all the mappings are as we expect them to be.
114+
* <p>
115+
* And if anything went wrong, provide the user with more information as to what went wrong
110116
*
111117
* @param objectFactory
112118
* the object factory
113119
*/
114120
private void verifyCanCreate(ObjectFactory objectFactory) {
121+
// if a custom object factory was supplied, we cannot reasionably verify that creation will work
122+
// thus, we disable verification and leave it up to the end user.
123+
if (!DefaultObjectFactory.class.equals(objectFactory.getClass())) {
124+
return;
125+
}
126+
115127
// before we create, we need to get the constructor to be used and verify our types match
116128
// since we added to the collection completely unchecked
117129
final Constructor<?> resolvedConstructor = resolveConstructor(resultType, constructorArgTypes);
@@ -125,24 +137,24 @@ private void verifyCanCreate(ObjectFactory objectFactory) {
125137
final Class<?> resolvedItemType = checkResolvedItemType(creationMetaInfo, genericParameterTypes[i]);
126138

127139
// 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)) {
140+
final PendingCreationKey pendingCreationKey = creationMetaInfo.getPendingCreationKey();
141+
if (linkedCreationsByKey.containsKey(pendingCreationKey)) {
130142
final Object emptyCollection = constructorArgs.get(i);
131143
if (emptyCollection == null || !objectFactory.isCollection(emptyCollection.getClass())) {
132144
throw new ExecutorException(
133-
"Expected empty collection for '" + resolvedItemType + "', this is a MyBatis internal error!");
145+
"Expected empty collection for '" + resolvedItemType + "', MyBatis internal error!");
134146
}
135147
} else {
136148
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!");
149+
if (!linkedCollectionsByKey.containsKey(pendingCreationKey)) {
150+
throw new ExecutorException(
151+
"Expected linked collection for key '" + pendingCreationKey + "', not found! MyBatis internal error!");
140152
}
141153

142154
// comparing memory locations here (we rely on that fact)
143-
if (linkedCollection != linkedCollectionsByResultMapId.get(resultMapId)) {
155+
if (linkedCollection != linkedCollectionsByKey.get(pendingCreationKey)) {
144156
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!");
157+
+ pendingCreationKey + "', not equal! MyBatis internal error!");
146158
}
147159
}
148160
}
@@ -209,11 +221,11 @@ Object create(ObjectFactory objectFactory, boolean verifyCreate) {
209221
}
210222

211223
// time to finally build this collection
212-
final String resultMapId = creationMetaInfo.getResultMapId();
213-
if (linkedCreationsByResultMapId.containsKey(resultMapId)) {
224+
final PendingCreationKey pendingCreationKey = creationMetaInfo.getPendingCreationKey();
225+
if (linkedCreationsByKey.containsKey(pendingCreationKey)) {
214226
@SuppressWarnings("unchecked")
215227
final Collection<Object> emptyCollection = (Collection<Object>) existingArg;
216-
final List<PendingConstructorCreation> linkedCreations = linkedCreationsByResultMapId.get(resultMapId);
228+
final List<PendingConstructorCreation> linkedCreations = linkedCreationsByKey.get(pendingCreationKey);
217229

218230
for (PendingConstructorCreation linkedCreation : linkedCreations) {
219231
emptyCollection.add(linkedCreation.create(objectFactory, verifyCreate));
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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.util.Objects;
19+
20+
import org.apache.ibatis.mapping.ResultMapping;
21+
22+
/**
23+
* A unique identifier for a pending constructor creation, prefix is used to distinguish between equal result maps for
24+
* different columns
25+
*
26+
* @author Willie Scholtz
27+
*/
28+
final class PendingCreationKey {
29+
private final String resultMapId;
30+
private final String constructorColumnPrefix;
31+
32+
PendingCreationKey(ResultMapping constructorMapping) {
33+
this.resultMapId = constructorMapping.getNestedResultMapId();
34+
this.constructorColumnPrefix = constructorMapping.getColumnPrefix();
35+
}
36+
37+
String getConstructorColumnPrefix() {
38+
return constructorColumnPrefix;
39+
}
40+
41+
String getResultMapId() {
42+
return resultMapId;
43+
}
44+
45+
@Override
46+
public boolean equals(Object o) {
47+
if (this == o)
48+
return true;
49+
if (o == null || getClass() != o.getClass())
50+
return false;
51+
52+
PendingCreationKey that = (PendingCreationKey) o;
53+
return Objects.equals(resultMapId, that.resultMapId)
54+
&& Objects.equals(constructorColumnPrefix, that.constructorColumnPrefix);
55+
}
56+
57+
@Override
58+
public int hashCode() {
59+
return Objects.hash(resultMapId, constructorColumnPrefix);
60+
}
61+
62+
@Override
63+
public String toString() {
64+
return "PendingCreationKey{" + "resultMapId='" + resultMapId + '\'' + ", constructorColumnPrefix='"
65+
+ constructorColumnPrefix + '\'' + '}';
66+
}
67+
}

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,31 @@
1616
package org.apache.ibatis.executor.resultset;
1717

1818
/**
19+
* Used to keep track of specific argument types for pending creations
20+
*
1921
* @author Willie Scholtz
2022
*/
2123
final class PendingCreationMetaInfo {
24+
2225
private final Class<?> argumentType;
23-
private final String resultMapId;
26+
private final PendingCreationKey pendingCreationKey;
2427

25-
PendingCreationMetaInfo(Class<?> argumentType, String resultMapId) {
28+
PendingCreationMetaInfo(Class<?> argumentType, PendingCreationKey pendingCreationKey) {
2629
this.argumentType = argumentType;
27-
this.resultMapId = resultMapId;
30+
this.pendingCreationKey = pendingCreationKey;
2831
}
2932

3033
Class<?> getArgumentType() {
3134
return argumentType;
3235
}
3336

34-
String getResultMapId() {
35-
return resultMapId;
37+
PendingCreationKey getPendingCreationKey() {
38+
return pendingCreationKey;
3639
}
3740

3841
@Override
3942
public String toString() {
40-
return "PendingCreationMetaInfo{" + "argumentType=" + argumentType + ", resultMapId='" + resultMapId + '\'' + '}';
43+
return "PendingCreationMetaInfo{" + "argumentType=" + argumentType + ", pendingCreationKey='" + pendingCreationKey
44+
+ '\'' + '}';
4145
}
4246
}

src/test/java/org/apache/ibatis/submitted/collection_in_constructor/CollectionInConstructorTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,19 @@ void testCollectionArgWithTypeHandler() {
165165
}
166166

167167
@Test
168+
@Disabled
168169
void testImmutableNestedObjects() {
170+
/*
171+
* This resultmap contains mixed property and constructor mappings, the logic assumes the entire chain will be
172+
* immutable when we have mixed mappings, we don't know when to create the final object, as property mappings could
173+
* still be modified at any point in time This brings us to a design question, is this really what we want from this
174+
* functionality, as the point was to create immutable objects in my opinion, supporting this defeats the purpose;
175+
* for example propery mapping -> immutable collection -> immutable object -> mapped by property mapping. we cannot
176+
* build the final object if it can still be modified; i.e, the signal to build the immutable object is lost. the
177+
* code in this pr assumes and relies on the base object also being immutable, i.e: constructor mapping -> immutable
178+
* collection -> immutable object -> mapped by constructor mapping. Imo, there is only one option here, it should be
179+
* added in the documentation; as doing (and supporting this, will be extremely complex)
180+
*/
169181
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
170182
Mapper mapper = sqlSession.getMapper(Mapper.class);
171183
Container container = mapper.getAContainer();

src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store4.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public class Store4 {
2424
private final List<Isle> isles;
2525

2626
// Using different arg order than the <constructor> definition
27-
// to ensure the builder is used
27+
// to ensure the builder is used, see CollectionInConstructorObjectFactory.create
2828
Store4(List<Isle> isles, Integer id) {
2929
super();
3030
this.isles = isles;

0 commit comments

Comments
 (0)