Skip to content

Commit 1c747a9

Browse files
authored
Merge pull request #3108 from epochcoder/feature/101-support-constructor-collection-injection
101: Add support for immutable collection constructor creation
2 parents a3d1d14 + fd69cf0 commit 1c747a9

File tree

64 files changed

+4545
-130
lines changed

Some content is hidden

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

64 files changed

+4545
-130
lines changed

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

+315-21
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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.ArrayList;
19+
import java.util.Collection;
20+
import java.util.HashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
24+
import org.apache.ibatis.executor.ExecutorException;
25+
import org.apache.ibatis.mapping.ResultMap;
26+
import org.apache.ibatis.mapping.ResultMapping;
27+
import org.apache.ibatis.reflection.ReflectionException;
28+
import org.apache.ibatis.reflection.factory.ObjectFactory;
29+
30+
/**
31+
* Represents an object that is still to be created once all nested results with collection values have been gathered
32+
*
33+
* @author Willie Scholtz
34+
*/
35+
final class PendingConstructorCreation {
36+
37+
private final Class<?> resultType;
38+
private final List<Class<?>> constructorArgTypes;
39+
private final List<Object> constructorArgs;
40+
41+
private final Map<Integer, PendingCreationMetaInfo> linkedCollectionMetaInfo;
42+
private final Map<PendingCreationKey, Collection<Object>> linkedCollectionsByKey;
43+
private final Map<PendingCreationKey, List<PendingConstructorCreation>> linkedCreationsByKey;
44+
45+
PendingConstructorCreation(Class<?> resultType, List<Class<?>> types, List<Object> args) {
46+
// since all our keys are based on result map id, we know we will never go over args size
47+
final int maxSize = types.size();
48+
49+
this.linkedCollectionMetaInfo = new HashMap<>(maxSize);
50+
this.linkedCollectionsByKey = new HashMap<>(maxSize);
51+
this.linkedCreationsByKey = new HashMap<>(maxSize);
52+
53+
this.resultType = resultType;
54+
this.constructorArgTypes = types;
55+
this.constructorArgs = args;
56+
}
57+
58+
@SuppressWarnings("unchecked")
59+
Collection<Object> initializeCollectionForResultMapping(ObjectFactory objectFactory, ResultMap resultMap,
60+
ResultMapping constructorMapping, Integer index) {
61+
final Class<?> parameterType = constructorMapping.getJavaType();
62+
if (!objectFactory.isCollection(parameterType)) {
63+
throw new ReflectionException(
64+
"Cannot add a collection result to non-collection based resultMapping: " + constructorMapping);
65+
}
66+
67+
return linkedCollectionsByKey.computeIfAbsent(new PendingCreationKey(constructorMapping), k -> {
68+
// this will allow us to verify the types of the collection before creating the final object
69+
linkedCollectionMetaInfo.put(index, new PendingCreationMetaInfo(resultMap.getType(), k));
70+
71+
// will be checked before we finally create the object) as we cannot reliably do that here
72+
return (Collection<Object>) objectFactory.create(parameterType);
73+
});
74+
}
75+
76+
void linkCreation(ResultMapping constructorMapping, PendingConstructorCreation pcc) {
77+
final PendingCreationKey creationKey = new PendingCreationKey(constructorMapping);
78+
final List<PendingConstructorCreation> pendingConstructorCreations = linkedCreationsByKey
79+
.computeIfAbsent(creationKey, k -> new ArrayList<>());
80+
81+
if (pendingConstructorCreations.contains(pcc)) {
82+
throw new ExecutorException("Cannot link inner constructor creation with same value, MyBatis internal error!");
83+
}
84+
85+
pendingConstructorCreations.add(pcc);
86+
}
87+
88+
void linkCollectionValue(ResultMapping constructorMapping, Object value) {
89+
// not necessary to add null results to the collection
90+
if (value == null) {
91+
return;
92+
}
93+
94+
linkedCollectionsByKey.computeIfAbsent(new PendingCreationKey(constructorMapping), k -> {
95+
throw new ExecutorException("Cannot link collection value for key: " + constructorMapping
96+
+ ", resultMap has not been seen/initialized yet! Mybatis internal error!");
97+
}).add(value);
98+
}
99+
100+
@Override
101+
public String toString() {
102+
return "PendingConstructorCreation(" + this.hashCode() + "){" + "resultType=" + resultType + '}';
103+
}
104+
105+
/**
106+
* Recursively creates the final result of this creation.
107+
*
108+
* @param objectFactory
109+
* the object factory
110+
*
111+
* @return the new immutable result
112+
*/
113+
Object create(ObjectFactory objectFactory) {
114+
final List<Object> newArguments = new ArrayList<>(constructorArgs.size());
115+
for (int i = 0; i < constructorArgs.size(); i++) {
116+
final PendingCreationMetaInfo creationMetaInfo = linkedCollectionMetaInfo.get(i);
117+
final Object existingArg = constructorArgs.get(i);
118+
119+
if (creationMetaInfo == null) {
120+
// we are not aware of this argument wrt pending creations
121+
newArguments.add(existingArg);
122+
continue;
123+
}
124+
125+
// time to finally build this collection
126+
final PendingCreationKey pendingCreationKey = creationMetaInfo.getPendingCreationKey();
127+
final List<PendingConstructorCreation> linkedCreations = linkedCreationsByKey.get(pendingCreationKey);
128+
if (linkedCreations != null) {
129+
@SuppressWarnings("unchecked")
130+
final Collection<Object> emptyCollection = (Collection<Object>) existingArg;
131+
132+
for (PendingConstructorCreation linkedCreation : linkedCreations) {
133+
emptyCollection.add(linkedCreation.create(objectFactory));
134+
}
135+
136+
newArguments.add(emptyCollection);
137+
continue;
138+
}
139+
140+
// handle the base collection (it was built inline already)
141+
newArguments.add(existingArg);
142+
}
143+
144+
return objectFactory.create(resultType, constructorArgTypes, newArguments);
145+
}
146+
}
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
* Used to keep track of specific argument types for pending creations
20+
*
21+
* @author Willie Scholtz
22+
*/
23+
final class PendingCreationMetaInfo {
24+
25+
private final Class<?> argumentType;
26+
private final PendingCreationKey pendingCreationKey;
27+
28+
PendingCreationMetaInfo(Class<?> argumentType, PendingCreationKey pendingCreationKey) {
29+
this.argumentType = argumentType;
30+
this.pendingCreationKey = pendingCreationKey;
31+
}
32+
33+
Class<?> getArgumentType() {
34+
return argumentType;
35+
}
36+
37+
PendingCreationKey getPendingCreationKey() {
38+
return pendingCreationKey;
39+
}
40+
41+
@Override
42+
public String toString() {
43+
return "PendingCreationMetaInfo{" + "argumentType=" + argumentType + ", pendingCreationKey='" + pendingCreationKey
44+
+ '\'' + '}';
45+
}
46+
}

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

+12
Original file line numberDiff line numberDiff line change
@@ -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,13 @@ public ResultMap build() {
111112
}
112113
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
113114
resultMap.constructorResultMappings.add(resultMapping);
115+
116+
// #101
117+
Class<?> javaType = resultMapping.getJavaType();
118+
resultMap.hasResultMapsUsingConstructorCollection = resultMap.hasResultMapsUsingConstructorCollection
119+
|| (resultMapping.getNestedQueryId() == null && javaType != null
120+
&& resultMap.configuration.getObjectFactory().isCollection(javaType));
121+
114122
if (resultMapping.getProperty() != null) {
115123
constructorArgNames.add(resultMapping.getProperty());
116124
}
@@ -210,6 +218,10 @@ public String getId() {
210218
return id;
211219
}
212220

221+
public boolean hasResultMapsUsingConstructorCollection() {
222+
return hasResultMapsUsingConstructorCollection;
223+
}
224+
213225
public boolean hasNestedResultMaps() {
214226
return hasNestedResultMaps;
215227
}

0 commit comments

Comments
 (0)