Skip to content

Commit d6425e0

Browse files
committed
mybatis#101: Add support for collection constructor creation
1 parent 3beed1d commit d6425e0

28 files changed

+1421
-261
lines changed

src/main/java/org/apache/ibatis/annotations/Select.java

Lines changed: 27 additions & 25 deletions
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.
@@ -25,34 +25,33 @@
2525
/**
2626
* The annotation that specify an SQL for retrieving record(s).
2727
* <p>
28-
* <b>How to use:</b>
29-
* <br/>
28+
* <b>How to use:</b> <br/>
3029
* <ul>
31-
* <li>
32-
* Simple:
33-
* <pre>
34-
* public interface UserMapper {
35-
* &#064;Select("SELECT id, name FROM users WHERE id = #{id}")
36-
* User selectById(int id);
37-
* }
38-
* </pre>
39-
* </li>
40-
* <li>
41-
* Dynamic SQL:
42-
* <pre>
43-
* public interface UserMapper {
44-
* &#064;Select({"&lt;script>",
45-
* "select * from users",
46-
* "where name = #{name}",
47-
* "&lt;if test=\"age != null\"> age = #{age} &lt;/if>",
48-
* "&lt;/script>"})
49-
* User select(@NotNull String name, @Nullable Intger age);
50-
* }
51-
* </pre>
52-
* </li>
30+
* <li>Simple:
31+
*
32+
* <pre>
33+
* public interface UserMapper {
34+
* &#064;Select("SELECT id, name FROM users WHERE id = #{id}")
35+
* User selectById(int id);
36+
* }
37+
* </pre>
38+
*
39+
* </li>
40+
* <li>Dynamic SQL:
41+
*
42+
* <pre>
43+
* public interface UserMapper {
44+
* &#064;Select({ "&lt;script>", "select * from users", "where name = #{name}",
45+
* "&lt;if test=\"age != null\"> age = #{age} &lt;/if>", "&lt;/script>" })
46+
* User select(@NotNull String name, @Nullable Intger age);
47+
* }
48+
* </pre>
49+
*
50+
* </li>
5351
* </ul>
5452
*
5553
* @author Clinton Begin
54+
*
5655
* @see <a href="https://mybatis.org/mybatis-3/dynamic-sql.html">How to use Dynamic SQL</a>
5756
*/
5857
@Documented
@@ -69,6 +68,7 @@
6968

7069
/**
7170
* @return A database id that correspond this statement
71+
*
7272
* @since 3.5.5
7373
*/
7474
String databaseId() default "";
@@ -78,6 +78,7 @@
7878
* e.g. RETURNING of PostgreSQL or OUTPUT of MS SQL Server.
7979
*
8080
* @return {@code true} if this select affects DB data; {@code false} if otherwise
81+
*
8182
* @since 3.5.12
8283
*/
8384
boolean affectData() default false;
@@ -86,6 +87,7 @@
8687
* The container annotation for {@link Select}.
8788
*
8889
* @author Kazuki Shimizu
90+
*
8991
* @since 3.5.5
9092
*/
9193
@Documented

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

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

213+
public boolean hasResultMapsUsingConstructorCollection() {
214+
return configuration.isExperimentalConstructorCollectionMappingEnabled()
215+
&& this.constructorResultMappings.stream().filter(crm -> crm.getNestedQueryId() == null)
216+
.map(ResultMapping::getJavaType).anyMatch(configuration.getObjectFactory()::isCollection);
217+
}
218+
213219
public boolean hasNestedResultMaps() {
214220
return hasNestedResultMaps;
215221
}

src/main/java/org/apache/ibatis/reflection/factory/DefaultObjectFactory.java

Lines changed: 17 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.
@@ -53,6 +53,22 @@ public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Obje
5353
return (T) instantiateClass(classToCreate, constructorArgTypes, constructorArgs);
5454
}
5555

56+
@Override
57+
public <T> Constructor<T> resolveConstructor(Class<T> type, List<Class<?>> constructorArgTypes) {
58+
try {
59+
if (constructorArgTypes == null) {
60+
return type.getDeclaredConstructor();
61+
}
62+
63+
return type.getDeclaredConstructor(constructorArgTypes.toArray(new Class[0]));
64+
} catch (Exception e) {
65+
String argTypes = Optional.ofNullable(constructorArgTypes).orElseGet(Collections::emptyList).stream()
66+
.map(Class::getSimpleName).collect(Collectors.joining(","));
67+
throw new ReflectionException(
68+
"Error resolving constructor for " + type + " with invalid types (" + argTypes + ") . Cause: " + e, e);
69+
}
70+
}
71+
5672
private <T> T instantiateClass(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
5773
try {
5874
Constructor<T> constructor;

0 commit comments

Comments
 (0)