diff --git a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java index c089302a254..81ca5429f58 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java @@ -24,8 +24,10 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -73,6 +75,7 @@ * @author Eduardo Macarron * @author Iwao AVE! * @author Kazuki Shimizu + * @author Willie Scholtz */ public class DefaultResultSetHandler implements ResultSetHandler { @@ -89,6 +92,9 @@ public class DefaultResultSetHandler implements ResultSetHandler { private final ObjectFactory objectFactory; private final ReflectorFactory reflectorFactory; + // pending creations property tracker + private final Map pendingPccRelations = new IdentityHashMap<>(); + // nested resultmaps private final Map nestedResultObjects = new HashMap<>(); private final Map ancestorObjects = new HashMap<>(); @@ -358,13 +364,23 @@ protected void checkResultHandler() { private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { + final boolean useCollectionConstructorInjection = resultMap.hasResultMapsUsingConstructorCollection(); + DefaultResultContext resultContext = new DefaultResultContext<>(); ResultSet resultSet = rsw.getResultSet(); skipRows(resultSet, rowBounds); while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null); - Object rowValue = getRowValue(rsw, discriminatedResultMap, null); - storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); + Object rowValue = getRowValue(rsw, discriminatedResultMap, null, null); + if (!useCollectionConstructorInjection) { + storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); + } else { + if (!(rowValue instanceof PendingConstructorCreation)) { + throw new ExecutorException("Expected result object to be a pending constructor creation!"); + } + + createAndStorePendingCreation(resultHandler, resultSet, resultContext, (PendingConstructorCreation) rowValue); + } } } @@ -372,9 +388,14 @@ private void storeObject(ResultHandler resultHandler, DefaultResultContext is always ResultHandler */) @@ -406,9 +427,10 @@ private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException { // GET VALUE FROM ROW FOR SIMPLE RESULT MAP // - private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException { + private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix, CacheKey parentRowKey) + throws SQLException { final ResultLoaderMap lazyLoader = new ResultLoaderMap(); - Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix); + Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix, parentRowKey); if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { final MetaObject metaObject = configuration.newMetaObject(rowValue); boolean foundValues = this.useConstructorMappings; @@ -419,6 +441,17 @@ private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String col foundValues = lazyLoader.size() > 0 || foundValues; rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null; } + + if (parentRowKey != null) { + // found a simple object/primitive in pending constructor creation that will need linking later + final CacheKey rowKey = createRowKey(resultMap, rsw, columnPrefix); + final CacheKey combinedKey = combineKeys(rowKey, parentRowKey); + + if (combinedKey != CacheKey.NULL_CACHE_KEY) { + nestedResultObjects.put(combinedKey, rowValue); + } + } + return rowValue; } @@ -437,7 +470,7 @@ private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, CacheKey c ancestorObjects.remove(resultMapId); } else { final ResultLoaderMap lazyLoader = new ResultLoaderMap(); - rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix); + rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix, combinedKey); if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { final MetaObject metaObject = configuration.newMetaObject(rowValue); boolean foundValues = this.useConstructorMappings; @@ -652,11 +685,13 @@ private CacheKey createKeyForMultipleResults(ResultSet rs, ResultMapping resultM // private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, - String columnPrefix) throws SQLException { + String columnPrefix, CacheKey parentRowKey) throws SQLException { this.useConstructorMappings = false; // reset previous mapping result final List> constructorArgTypes = new ArrayList<>(); final List constructorArgs = new ArrayList<>(); - Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix); + + Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix, + parentRowKey); if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { final List propertyMappings = resultMap.getPropertyResultMappings(); for (ResultMapping propertyMapping : propertyMappings) { @@ -667,13 +702,21 @@ private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, Res break; } } + + // (issue #101) + if (resultMap.hasResultMapsUsingConstructorCollection() && resultObject instanceof PendingConstructorCreation) { + linkNestedPendingCreations(rsw, resultMap, columnPrefix, parentRowKey, + (PendingConstructorCreation) resultObject, constructorArgs); + } } + this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result return resultObject; } private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List> constructorArgTypes, - List constructorArgs, String columnPrefix) throws SQLException { + List constructorArgs, String columnPrefix, CacheKey parentRowKey) throws SQLException { + final Class resultType = resultMap.getType(); final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory); final List constructorMappings = resultMap.getConstructorResultMappings(); @@ -682,7 +725,7 @@ private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, Lis } if (!constructorMappings.isEmpty()) { return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, - columnPrefix); + columnPrefix, resultMap.hasResultMapsUsingConstructorCollection(), parentRowKey); } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) { return objectFactory.create(resultType); } else if (shouldApplyAutomaticMappings(resultMap, false)) { @@ -694,8 +737,9 @@ private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, Lis Object createParameterizedResultObject(ResultSetWrapper rsw, Class resultType, List constructorMappings, List> constructorArgTypes, List constructorArgs, - String columnPrefix) { + String columnPrefix, boolean useCollectionConstructorInjection, CacheKey parentRowKey) { boolean foundValues = false; + for (ResultMapping constructorMapping : constructorMappings) { final Class parameterType = constructorMapping.getJavaType(); final String column = constructorMapping.getColumn(); @@ -704,10 +748,11 @@ Object createParameterizedResultObject(ResultSetWrapper rsw, Class resultType if (constructorMapping.getNestedQueryId() != null) { value = getNestedQueryConstructorValue(rsw.getResultSet(), constructorMapping, columnPrefix); } else if (constructorMapping.getNestedResultMapId() != null) { - String constructorColumnPrefix = getColumnPrefix(columnPrefix, constructorMapping); + final String constructorColumnPrefix = getColumnPrefix(columnPrefix, constructorMapping); final ResultMap resultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), configuration.getResultMap(constructorMapping.getNestedResultMapId()), constructorColumnPrefix); - value = getRowValue(rsw, resultMap, constructorColumnPrefix); + value = getRowValue(rsw, resultMap, constructorColumnPrefix, + useCollectionConstructorInjection ? parentRowKey : null); } else { final TypeHandler typeHandler = constructorMapping.getTypeHandler(); value = typeHandler.getResult(rsw.getResultSet(), prependPrefix(column, columnPrefix)); @@ -715,11 +760,23 @@ Object createParameterizedResultObject(ResultSetWrapper rsw, Class resultType } catch (ResultMapException | SQLException e) { throw new ExecutorException("Could not process result for mapping: " + constructorMapping, e); } + constructorArgTypes.add(parameterType); constructorArgs.add(value); + foundValues = value != null || foundValues; } - return foundValues ? objectFactory.create(resultType, constructorArgTypes, constructorArgs) : null; + + if (!foundValues) { + return null; + } + + if (useCollectionConstructorInjection) { + // at least one of the nestedResultMaps contained a collection, we have to defer until later + return new PendingConstructorCreation(resultType, constructorArgTypes, constructorArgs); + } + + return objectFactory.create(resultType, constructorArgTypes, constructorArgs); } private Object createByConstructorSignature(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix, @@ -1015,29 +1072,53 @@ private String prependPrefix(String columnName, String prefix) { private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { + final boolean useCollectionConstructorInjection = resultMap.hasResultMapsUsingConstructorCollection(); + PendingConstructorCreation lastHandledCreation = null; + if (useCollectionConstructorInjection) { + verifyPendingCreationPreconditions(parentMapping); + } + final DefaultResultContext resultContext = new DefaultResultContext<>(); ResultSet resultSet = rsw.getResultSet(); skipRows(resultSet, rowBounds); Object rowValue = previousRowValue; + while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { final ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null); final CacheKey rowKey = createRowKey(discriminatedResultMap, rsw, null); - Object partialObject = nestedResultObjects.get(rowKey); - // issue #577 && #542 - if (mappedStatement.isResultOrdered()) { - if (partialObject == null && rowValue != null) { + + final Object partialObject = nestedResultObjects.get(rowKey); + final boolean foundNewUniqueRow = partialObject == null; + + // issue #577, #542 && #101 + if (useCollectionConstructorInjection) { + if (foundNewUniqueRow && lastHandledCreation != null) { + createAndStorePendingCreation(resultHandler, resultSet, resultContext, lastHandledCreation); + lastHandledCreation = null; + } + + rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject); + if (rowValue instanceof PendingConstructorCreation) { + lastHandledCreation = (PendingConstructorCreation) rowValue; + } + } else if (mappedStatement.isResultOrdered()) { + if (foundNewUniqueRow && rowValue != null) { nestedResultObjects.clear(); storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); } rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject); } else { rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject); - if (partialObject == null) { + if (foundNewUniqueRow) { storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); } } } - if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) { + + if (useCollectionConstructorInjection && lastHandledCreation != null) { + createAndStorePendingCreation(resultHandler, resultSet, resultContext, lastHandledCreation); + } else if (rowValue != null && mappedStatement.isResultOrdered() + && shouldProcessMoreRows(resultContext, rowBounds)) { storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); previousRowValue = null; } else if (rowValue != null) { @@ -1045,6 +1126,201 @@ private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap r } } + // + // NESTED RESULT MAP (PENDING CONSTRUCTOR CREATIONS) + // + private void linkNestedPendingCreations(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix, + CacheKey parentRowKey, PendingConstructorCreation pendingCreation, List constructorArgs) + throws SQLException { + if (parentRowKey == null) { + // nothing to link, possibly due to simple (non-nested) result map + return; + } + + final CacheKey rowKey = createRowKey(resultMap, rsw, columnPrefix); + final CacheKey combinedKey = combineKeys(rowKey, parentRowKey); + + if (combinedKey != CacheKey.NULL_CACHE_KEY) { + nestedResultObjects.put(combinedKey, pendingCreation); + } + + final List constructorMappings = resultMap.getConstructorResultMappings(); + for (int index = 0; index < constructorMappings.size(); index++) { + final ResultMapping constructorMapping = constructorMappings.get(index); + final String nestedResultMapId = constructorMapping.getNestedResultMapId(); + + if (nestedResultMapId == null) { + continue; + } + + final Class javaType = constructorMapping.getJavaType(); + if (javaType == null || !objectFactory.isCollection(javaType)) { + continue; + } + + final String constructorColumnPrefix = getColumnPrefix(columnPrefix, constructorMapping); + final ResultMap nestedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), + configuration.getResultMap(constructorMapping.getNestedResultMapId()), constructorColumnPrefix); + + final Object actualValue = constructorArgs.get(index); + final boolean hasValue = actualValue != null; + final boolean isInnerCreation = actualValue instanceof PendingConstructorCreation; + final boolean alreadyCreatedCollection = hasValue && objectFactory.isCollection(actualValue.getClass()); + + if (!isInnerCreation) { + final Collection value = pendingCreation.initializeCollectionForResultMapping(objectFactory, + nestedResultMap, constructorMapping, index); + if (!alreadyCreatedCollection) { + // override values with empty collection + constructorArgs.set(index, value); + } + + // since we are linking a new value, we need to let nested objects know we did that + final CacheKey nestedRowKey = createRowKey(nestedResultMap, rsw, constructorColumnPrefix); + final CacheKey nestedCombinedKey = combineKeys(nestedRowKey, combinedKey); + + if (nestedCombinedKey != CacheKey.NULL_CACHE_KEY) { + nestedResultObjects.put(nestedCombinedKey, pendingCreation); + } + + if (hasValue) { + pendingCreation.linkCollectionValue(constructorMapping, actualValue); + } + } else { + final PendingConstructorCreation innerCreation = (PendingConstructorCreation) actualValue; + final Collection value = pendingCreation.initializeCollectionForResultMapping(objectFactory, + nestedResultMap, constructorMapping, index); + // we will fill this collection when building the final object + constructorArgs.set(index, value); + // link the creation for building later + pendingCreation.linkCreation(constructorMapping, innerCreation); + } + } + } + + private boolean applyNestedPendingConstructorCreations(ResultSetWrapper rsw, ResultMap resultMap, + MetaObject metaObject, String parentPrefix, CacheKey parentRowKey, boolean newObject, boolean foundValues) { + if (newObject) { + // new objects are linked by createResultObject + return false; + } + + for (ResultMapping constructorMapping : resultMap.getConstructorResultMappings()) { + final String nestedResultMapId = constructorMapping.getNestedResultMapId(); + final Class parameterType = constructorMapping.getJavaType(); + if (nestedResultMapId == null || constructorMapping.getResultSet() != null || parameterType == null + || !objectFactory.isCollection(parameterType)) { + continue; + } + + try { + final String columnPrefix = getColumnPrefix(parentPrefix, constructorMapping); + final ResultMap nestedResultMap = getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix); + + final CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix); + final CacheKey combinedKey = combineKeys(rowKey, parentRowKey); + + // should have inserted already as a nested result object + Object rowValue = nestedResultObjects.get(combinedKey); + + PendingConstructorCreation pendingConstructorCreation = null; + if (rowValue instanceof PendingConstructorCreation) { + pendingConstructorCreation = (PendingConstructorCreation) rowValue; + } else if (rowValue != null) { + // found a simple object that was already linked/handled + continue; + } + + final boolean newValueForNestedResultMap = pendingConstructorCreation == null; + if (newValueForNestedResultMap) { + final Object parentObject = metaObject.getOriginalObject(); + if (!(parentObject instanceof PendingConstructorCreation)) { + throw new ExecutorException( + "parentObject is not a pending creation, cannot continue linking! MyBatis internal error!"); + } + + pendingConstructorCreation = (PendingConstructorCreation) parentObject; + } + + rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, + newValueForNestedResultMap ? null : pendingConstructorCreation); + + if (rowValue == null) { + continue; + } + + if (rowValue instanceof PendingConstructorCreation) { + if (newValueForNestedResultMap) { + // we created a brand new pcc. this is a new collection value + pendingConstructorCreation.linkCreation(constructorMapping, (PendingConstructorCreation) rowValue); + foundValues = true; + } + } else { + pendingConstructorCreation.linkCollectionValue(constructorMapping, rowValue); + foundValues = true; + + if (combinedKey != CacheKey.NULL_CACHE_KEY) { + nestedResultObjects.put(combinedKey, pendingConstructorCreation); + } + } + } catch (SQLException e) { + throw new ExecutorException("Error getting constructor collection nested result map values for '" + + constructorMapping.getProperty() + "'. Cause: " + e, e); + } + } + + return foundValues; + } + + private void createPendingConstructorCreations(Object rowValue) { + // handle possible pending creations within this object + // by now, the property mapping has been completely built, we can reconstruct it + final PendingRelation pendingRelation = pendingPccRelations.remove(rowValue); + final MetaObject metaObject = pendingRelation.metaObject; + final ResultMapping resultMapping = pendingRelation.propertyMapping; + + // get the list to be built + Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); + if (collectionProperty != null) { + // we expect pending creations now + final Collection pendingCreations = (Collection) collectionProperty; + + // remove the link to the old collection + metaObject.setValue(resultMapping.getProperty(), null); + + // create new collection property + collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); + final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty); + + // create the pending objects + for (Object pendingCreation : pendingCreations) { + if (pendingCreation instanceof PendingConstructorCreation) { + final PendingConstructorCreation pendingConstructorCreation = (PendingConstructorCreation) pendingCreation; + targetMetaObject.add(pendingConstructorCreation.create(objectFactory)); + } + } + } + } + + private void verifyPendingCreationPreconditions(ResultMapping parentMapping) { + if (parentMapping != null) { + throw new ExecutorException( + "Cannot construct objects with collections in constructors using multiple result sets yet!"); + } + + if (!mappedStatement.isResultOrdered()) { + throw new ExecutorException("Cannot reliably construct result if we are not sure the results are ordered " + + "so that no new previous rows would occur, set resultOrdered on your mapped statement if you have verified this"); + } + } + + private void createAndStorePendingCreation(ResultHandler resultHandler, ResultSet resultSet, + DefaultResultContext resultContext, PendingConstructorCreation pendingCreation) throws SQLException { + final Object result = pendingCreation.create(objectFactory); + storeObject(resultHandler, resultContext, result, null, resultSet); + nestedResultObjects.clear(); + } + // // NESTED RESULT MAP (JOIN MAPPING) // @@ -1087,6 +1363,13 @@ private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap result } } } + + // (issue #101) + if (resultMap.hasResultMapsUsingConstructorCollection()) { + foundValues = applyNestedPendingConstructorCreations(rsw, resultMap, metaObject, parentPrefix, parentRowKey, + newObject, foundValues); + } + return foundValues; } @@ -1234,6 +1517,17 @@ private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Obj if (collectionProperty != null) { final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty); targetMetaObject.add(rowValue); + + // it is possible for pending creations to get set via property mappings, + // keep track of these, so we can rebuild them. + final Object originalObject = metaObject.getOriginalObject(); + if (rowValue instanceof PendingConstructorCreation && !pendingPccRelations.containsKey(originalObject)) { + PendingRelation pendingRelation = new PendingRelation(); + pendingRelation.propertyMapping = resultMapping; + pendingRelation.metaObject = metaObject; + + pendingPccRelations.put(originalObject, pendingRelation); + } } else { metaObject.setValue(resultMapping.getProperty(), rowValue); } diff --git a/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java b/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java new file mode 100644 index 00000000000..3c0f6b3f4b6 --- /dev/null +++ b/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java @@ -0,0 +1,146 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.executor.resultset; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.ibatis.executor.ExecutorException; +import org.apache.ibatis.mapping.ResultMap; +import org.apache.ibatis.mapping.ResultMapping; +import org.apache.ibatis.reflection.ReflectionException; +import org.apache.ibatis.reflection.factory.ObjectFactory; + +/** + * Represents an object that is still to be created once all nested results with collection values have been gathered + * + * @author Willie Scholtz + */ +final class PendingConstructorCreation { + + private final Class resultType; + private final List> constructorArgTypes; + private final List constructorArgs; + + private final Map linkedCollectionMetaInfo; + private final Map> linkedCollectionsByKey; + private final Map> linkedCreationsByKey; + + PendingConstructorCreation(Class resultType, List> types, List args) { + // since all our keys are based on result map id, we know we will never go over args size + final int maxSize = types.size(); + + this.linkedCollectionMetaInfo = new HashMap<>(maxSize); + this.linkedCollectionsByKey = new HashMap<>(maxSize); + this.linkedCreationsByKey = new HashMap<>(maxSize); + + this.resultType = resultType; + this.constructorArgTypes = types; + this.constructorArgs = args; + } + + @SuppressWarnings("unchecked") + Collection initializeCollectionForResultMapping(ObjectFactory objectFactory, ResultMap resultMap, + ResultMapping constructorMapping, Integer index) { + final Class parameterType = constructorMapping.getJavaType(); + if (!objectFactory.isCollection(parameterType)) { + throw new ReflectionException( + "Cannot add a collection result to non-collection based resultMapping: " + constructorMapping); + } + + return linkedCollectionsByKey.computeIfAbsent(new PendingCreationKey(constructorMapping), k -> { + // this will allow us to verify the types of the collection before creating the final object + linkedCollectionMetaInfo.put(index, new PendingCreationMetaInfo(resultMap.getType(), k)); + + // will be checked before we finally create the object) as we cannot reliably do that here + return (Collection) objectFactory.create(parameterType); + }); + } + + void linkCreation(ResultMapping constructorMapping, PendingConstructorCreation pcc) { + final PendingCreationKey creationKey = new PendingCreationKey(constructorMapping); + final List pendingConstructorCreations = linkedCreationsByKey + .computeIfAbsent(creationKey, k -> new ArrayList<>()); + + if (pendingConstructorCreations.contains(pcc)) { + throw new ExecutorException("Cannot link inner constructor creation with same value, MyBatis internal error!"); + } + + pendingConstructorCreations.add(pcc); + } + + void linkCollectionValue(ResultMapping constructorMapping, Object value) { + // not necessary to add null results to the collection + if (value == null) { + return; + } + + linkedCollectionsByKey.computeIfAbsent(new PendingCreationKey(constructorMapping), k -> { + throw new ExecutorException("Cannot link collection value for key: " + constructorMapping + + ", resultMap has not been seen/initialized yet! Mybatis internal error!"); + }).add(value); + } + + @Override + public String toString() { + return "PendingConstructorCreation(" + this.hashCode() + "){" + "resultType=" + resultType + '}'; + } + + /** + * Recursively creates the final result of this creation. + * + * @param objectFactory + * the object factory + * + * @return the new immutable result + */ + Object create(ObjectFactory objectFactory) { + final List newArguments = new ArrayList<>(constructorArgs.size()); + for (int i = 0; i < constructorArgs.size(); i++) { + final PendingCreationMetaInfo creationMetaInfo = linkedCollectionMetaInfo.get(i); + final Object existingArg = constructorArgs.get(i); + + if (creationMetaInfo == null) { + // we are not aware of this argument wrt pending creations + newArguments.add(existingArg); + continue; + } + + // time to finally build this collection + final PendingCreationKey pendingCreationKey = creationMetaInfo.getPendingCreationKey(); + final List linkedCreations = linkedCreationsByKey.get(pendingCreationKey); + if (linkedCreations != null) { + @SuppressWarnings("unchecked") + final Collection emptyCollection = (Collection) existingArg; + + for (PendingConstructorCreation linkedCreation : linkedCreations) { + emptyCollection.add(linkedCreation.create(objectFactory)); + } + + newArguments.add(emptyCollection); + continue; + } + + // handle the base collection (it was built inline already) + newArguments.add(existingArg); + } + + return objectFactory.create(resultType, constructorArgTypes, newArguments); + } +} diff --git a/src/main/java/org/apache/ibatis/executor/resultset/PendingCreationKey.java b/src/main/java/org/apache/ibatis/executor/resultset/PendingCreationKey.java new file mode 100644 index 00000000000..49040247a98 --- /dev/null +++ b/src/main/java/org/apache/ibatis/executor/resultset/PendingCreationKey.java @@ -0,0 +1,67 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.executor.resultset; + +import java.util.Objects; + +import org.apache.ibatis.mapping.ResultMapping; + +/** + * A unique identifier for a pending constructor creation, prefix is used to distinguish between equal result maps for + * different columns + * + * @author Willie Scholtz + */ +final class PendingCreationKey { + private final String resultMapId; + private final String constructorColumnPrefix; + + PendingCreationKey(ResultMapping constructorMapping) { + this.resultMapId = constructorMapping.getNestedResultMapId(); + this.constructorColumnPrefix = constructorMapping.getColumnPrefix(); + } + + String getConstructorColumnPrefix() { + return constructorColumnPrefix; + } + + String getResultMapId() { + return resultMapId; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + PendingCreationKey that = (PendingCreationKey) o; + return Objects.equals(resultMapId, that.resultMapId) + && Objects.equals(constructorColumnPrefix, that.constructorColumnPrefix); + } + + @Override + public int hashCode() { + return Objects.hash(resultMapId, constructorColumnPrefix); + } + + @Override + public String toString() { + return "PendingCreationKey{" + "resultMapId='" + resultMapId + '\'' + ", constructorColumnPrefix='" + + constructorColumnPrefix + '\'' + '}'; + } +} diff --git a/src/main/java/org/apache/ibatis/executor/resultset/PendingCreationMetaInfo.java b/src/main/java/org/apache/ibatis/executor/resultset/PendingCreationMetaInfo.java new file mode 100644 index 00000000000..a172f3ce152 --- /dev/null +++ b/src/main/java/org/apache/ibatis/executor/resultset/PendingCreationMetaInfo.java @@ -0,0 +1,46 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.executor.resultset; + +/** + * Used to keep track of specific argument types for pending creations + * + * @author Willie Scholtz + */ +final class PendingCreationMetaInfo { + + private final Class argumentType; + private final PendingCreationKey pendingCreationKey; + + PendingCreationMetaInfo(Class argumentType, PendingCreationKey pendingCreationKey) { + this.argumentType = argumentType; + this.pendingCreationKey = pendingCreationKey; + } + + Class getArgumentType() { + return argumentType; + } + + PendingCreationKey getPendingCreationKey() { + return pendingCreationKey; + } + + @Override + public String toString() { + return "PendingCreationMetaInfo{" + "argumentType=" + argumentType + ", pendingCreationKey='" + pendingCreationKey + + '\'' + '}'; + } +} diff --git a/src/main/java/org/apache/ibatis/mapping/ResultMap.java b/src/main/java/org/apache/ibatis/mapping/ResultMap.java index 3386fefb523..58ce5dd6f6a 100644 --- a/src/main/java/org/apache/ibatis/mapping/ResultMap.java +++ b/src/main/java/org/apache/ibatis/mapping/ResultMap.java @@ -46,6 +46,7 @@ public class ResultMap { private Set mappedColumns; private Set mappedProperties; private Discriminator discriminator; + private boolean hasResultMapsUsingConstructorCollection; private boolean hasNestedResultMaps; private boolean hasNestedQueries; private Boolean autoMapping; @@ -111,6 +112,13 @@ public ResultMap build() { } if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) { resultMap.constructorResultMappings.add(resultMapping); + + // #101 + Class javaType = resultMapping.getJavaType(); + resultMap.hasResultMapsUsingConstructorCollection = resultMap.hasResultMapsUsingConstructorCollection + || (resultMapping.getNestedQueryId() == null && javaType != null + && resultMap.configuration.getObjectFactory().isCollection(javaType)); + if (resultMapping.getProperty() != null) { constructorArgNames.add(resultMapping.getProperty()); } @@ -210,6 +218,10 @@ public String getId() { return id; } + public boolean hasResultMapsUsingConstructorCollection() { + return hasResultMapsUsingConstructorCollection; + } + public boolean hasNestedResultMaps() { return hasNestedResultMaps; } diff --git a/src/site/es/xdoc/sqlmap-xml.xml b/src/site/es/xdoc/sqlmap-xml.xml index 3e9ff8f0711..64b80f8c0fa 100644 --- a/src/site/es/xdoc/sqlmap-xml.xml +++ b/src/site/es/xdoc/sqlmap-xml.xml @@ -1038,6 +1038,123 @@ public class User { +
Nested Results for association or collection
+ +

While the following sections describe how to use association and collection for both Nested selects and Nested results, Since 3.6.0 we can now inject both using constructor mapping.

+ +

Considering the following:

+ + userRoles) { + //... + } +} + +public class UserRole { + // ... + public UserRole(Integer id, String role) { + // ... + } +} +]]> + +

We can map UserRole as a nested result, MyBatis will wait until the row has been fully ‘completed’ before creating the object, this means that by the time the User gets created, userRoles will be complete and cannot be modified anymore.

+ + + + + + + + +]]> + +

To achieve fully immutable objects in this example, we can also use constructor injection for UserRole

+ + + + + + + +]]> + +

MyBatis needs to be explicitly told that the results have been ordered in such a way, that when a new main row is retrieved from the result set, no previous row results will be retrieved again. This can be set on the statement with the resultOrdered attribute:

+ + + select + u.id, + u.username, + r.id as role_id, + r.role as role_role, + from user u + left join user_role ur on u.id = ur.user_id + inner join role r on r.id = ur.role_id + order by u.id, r.id + +]]> + +

Note that order by is specified to order the results correctly. We can imagine the output to look something like:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
row_nru.idu.usernamer.idr.role
11John1Admins
21John2Users
32Jacknullnull
43Peter2Users
53Peter3Maintainers
63Peter4Approvers
+ +

After this query is run, we would have the following results:

+ + + +

If the 5th row here would have somehow appeared below the first row (via some ORDER BY), MyBatis would not be able to fully construct the John user correctly using constructor collection mapping.

+ +

It is important to note that mixed mappings have limited support, i.e. property mappings combined with nested constructor mappings are likely to fail. +When using this functionality, it is preferable for the entire mapping hierarchy to use immutable constructor mappings.

+

association

diff --git a/src/site/ja/xdoc/sqlmap-xml.xml b/src/site/ja/xdoc/sqlmap-xml.xml index 46ddd1853c5..cb65b4c87c2 100644 --- a/src/site/ja/xdoc/sqlmap-xml.xml +++ b/src/site/ja/xdoc/sqlmap-xml.xml @@ -1176,6 +1176,123 @@ public class User { +
コレクションや複雑なオブジェクトを引数に含むコンストラクタに対して、ネストされた結果をマッピングする
+ +

以降のセクションで associationcollection について説明しますが、バージョン 3.6.0 以降ではコンストラクタの引数としてコレクションを指定できるようになりました。

+ +

次のクラスを例にして説明します。

+ + userRoles) { + //... + } +} + +public class UserRole { + // ... + public UserRole(Integer id, String role) { + // ... + } +} +]]> + +

クエリがネストされた結果を返す場合、User のコンストラクタ引数に含まれているコレクション List<UserRole> をマッピングするためには、下記のような ResultMap を定義します。

+

MyBatis は UserRole が全て読み込まれてから User のコンストラクタを呼び出します。つまり、User をイミュータブルなオブジェクトとして定義することもできるということです。

+ + + + + + + + +]]> + +

上記の ResultMap で参照している UserRole 用の ResultMap でもコンストラクタマッピングを使用すれば、UserRole もイミュータブルにできます。

+ + + + + + + +]]> + +

このマッピングを実現するためには、結果セットが正しい順番でソートされていることが前提となるので、クエリで適切な ORDER BY 句を指定した上で、<select> 要素の resultOrdered 属性に true を設定してください。

+ + + select + u.id, + u.username, + r.id as role_id, + r.role as role_role, + from user u + left join user_role ur on u.id = ur.user_id + inner join role r on r.id = ur.role_id + order by u.id, r.id + +]]> + +

下記のように正しくソートされた結果セットが返ってくれば、期待通りにマッピングすることができます。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
row_nru.idu.usernamer.idr.role
11John1Admins
21John2Users
32Jacknullnull
43Peter2Users
53Peter3Maintainers
63Peter4Approvers
+ +

作成される User オブジェクトは下記のようになります。

+ + + +

仮に、誤ったソート条件が指定されていて5行目のデータが1行目の直後に出現するようなクエリを実行してしまうと、全ての UserRole を読み込む前に John ユーザのコンストラクタが呼び出されてしまうので、期待通りの結果を得ることはできません。

+ +

コンストラクタ引数にコレクションを含むオブジェクトを使う場合は、完全にイミュータブルなオブジェクトとして定義してください。ResultMap にコンストラクタマッピングとプロパティマッピングが混在する ResultMap は正しく動作しない可能性があります。

+

association

diff --git a/src/site/ko/xdoc/sqlmap-xml.xml b/src/site/ko/xdoc/sqlmap-xml.xml index 981a007def7..57a187248ba 100644 --- a/src/site/ko/xdoc/sqlmap-xml.xml +++ b/src/site/ko/xdoc/sqlmap-xml.xml @@ -1047,6 +1047,124 @@ public class User { +
Nested Results for association or collection
+ +

While the following sections describe how to use association and collection for both Nested selects and Nested results, Since 3.6.0 we can now inject both using constructor mapping.

+ +

Considering the following:

+ + userRoles) { + //... + } +} + +public class UserRole { + // ... + public UserRole(Integer id, String role) { + // ... + } +} +]]> + +

We can map UserRole as a nested result, MyBatis will wait until the row has been fully ‘completed’ before creating the object, this means that by the time the User gets created, userRoles will be complete and cannot be modified anymore.

+ + + + + + + + +]]> + +

To achieve fully immutable objects in this example, we can also use constructor injection for UserRole

+ + + + + + + +]]> + +

MyBatis needs to be explicitly told that the results have been ordered in such a way, that when a new main row is retrieved from the result set, no previous row results will be retrieved again. This can be set on the statement with the resultOrdered attribute:

+ + + select + u.id, + u.username, + r.id as role_id, + r.role as role_role, + from user u + left join user_role ur on u.id = ur.user_id + inner join role r on r.id = ur.role_id + order by u.id, r.id + +]]> + +

Note that order by is specified to order the results correctly. We can imagine the output to look something like:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
row_nru.idu.usernamer.idr.role
11John1Admins
21John2Users
32Jacknullnull
43Peter2Users
53Peter3Maintainers
63Peter4Approvers
+ +

After this query is run, we would have the following results:

+ + + +

If the 5th row here would have somehow appeared below the first row (via some ORDER BY), MyBatis would not be able to fully construct the John user correctly using constructor collection mapping.

+ +

It is important to note that mixed mappings have limited support, i.e. property mappings combined with nested constructor mappings are likely to fail. +When using this functionality, it is preferable for the entire mapping hierarchy to use immutable constructor mappings.

+ +

association

diff --git a/src/site/markdown/sqlmap-xml.md b/src/site/markdown/sqlmap-xml.md index ae784ecf254..cf496a8fcdb 100644 --- a/src/site/markdown/sqlmap-xml.md +++ b/src/site/markdown/sqlmap-xml.md @@ -640,7 +640,7 @@ public class User { } ``` -In order to inject the results into the constructor, MyBatis needs to identify the constructor for somehow. In the following example, MyBatis searches a constructor declared with three parameters: `java.lang.Integer`, `java.lang.String` and `int` in this order. +In order to inject the results into the constructor, MyBatis needs to identify the constructor somehow. In the following example, MyBatis searches a constructor declared with three parameters: `java.lang.Integer`, `java.lang.String` and `int` in this order. ```xml @@ -675,6 +675,90 @@ The rest of the attributes and rules are the same as for the regular id and resu | `resultMap` | This is the ID of a ResultMap that can map the nested results of this argument into an appropriate object graph. This is an alternative to using a call to another select statement. It allows you to join multiple tables together into a single `ResultSet`. Such a `ResultSet` will contain duplicated, repeating groups of data that needs to be decomposed and mapped properly to a nested object graph. To facilitate this, MyBatis lets you "chain" result maps together, to deal with the nested results. See the Association element below for more. | | `name` | The name of the constructor parameter. Specifying name allows you to write arg elements in any order. See the above explanation. Since 3.4.3. | +##### Nested Results for association or collection + +While the following sections describe how to use `association` and `collection` for both Nested selects and Nested results, Since 3.6.0 we can now inject both using `constructor` mapping. + +Considering the following: + +```java +public class User { + //... + public User(Integer id, String username, List userRoles) { + //... + } +} + +public class UserRole { + // ... + public UserRole(Integer id, String role) { + // ... + } +} +``` + +We can map `UserRole` as a nested result, MyBatis will wait until the row has been fully 'completed' before creating the object, this means that by the time the `User` gets created, `userRoles` will be complete and cannot be modified anymore. + +```xml + + + + + + + +``` + +To achieve fully immutable objects in this example, we can also use constructor injection for `UserRole` + +```xml + + + + + + +``` + +MyBatis needs to be explicitly told that the results have been ordered in such a way, that when a new main row is retrieved from the result set, no previous row results will be retrieved again. This can be set on the statement with the `resultOrdered` attribute: + +```xml + +``` + +Note that `order by` is specified to order the results correctly. We can imagine the output to look something like: + +| row_nr | u.id | u.username | r.id | r.role | +|--------|------|------------|------|-------------| +| 1 | 1 | John | 1 | Admins | +| 2 | 1 | John | 2 | Users | +| 3 | 2 | Jack | null | null | +| 4 | 3 | Peter | 2 | Users | +| 5 | 3 | Peter | 3 | Maintainers | +| 6 | 3 | Peter | 4 | Approvers | + +After this query is run, we would have the following results: + +``` +User{username=John, roles=[Admins, Users]} +User{username=Jack, roles=[]} +User{username=Peter, roles=[Users, Maintainers, Approvers]} +``` + +If the 5th row here would have somehow appeared below the first row (via some `ORDER BY`), MyBatis would not be able to fully construct the `John` user correctly using constructor collection mapping. + +It is important to note that mixed mappings have limited support, i.e. property mappings combined with nested constructor mappings are likely to fail. +When using this functionality, it is preferable for the entire mapping hierarchy to use immutable constructor mappings. #### association @@ -701,7 +785,6 @@ First, let's examine the properties of the element. As you'll see, it differs fr | `jdbcType` | The JDBC Type from the list of supported types that follows this table. The JDBC type is only required for nullable columns upon insert, update or delete. This is a JDBC requirement, not an MyBatis one. So even if you were coding JDBC directly, you'd need to specify this type – but only for nullable values. | | `typeHandler` | We discussed default type handlers previously in this documentation. Using this property you can override the default type handler on a mapping-by-mapping basis. The value is either a fully qualified class name of a TypeHandler implementation, or a type alias. | - #### Nested Select for Association | Attribute | Description | diff --git a/src/site/zh_CN/xdoc/sqlmap-xml.xml b/src/site/zh_CN/xdoc/sqlmap-xml.xml index 5ec3ea52037..7a7c7864f35 100644 --- a/src/site/zh_CN/xdoc/sqlmap-xml.xml +++ b/src/site/zh_CN/xdoc/sqlmap-xml.xml @@ -1208,6 +1208,124 @@ public class User { +
Nested Results for association or collection
+ +

While the following sections describe how to use association and collection for both Nested selects and Nested results, Since 3.6.0 we can now inject both using constructor mapping.

+ +

Considering the following:

+ + userRoles) { + //... + } +} + +public class UserRole { + // ... + public UserRole(Integer id, String role) { + // ... + } +} +]]> + +

We can map UserRole as a nested result, MyBatis will wait until the row has been fully ‘completed’ before creating the object, this means that by the time the User gets created, userRoles will be complete and cannot be modified anymore.

+ + + + + + + + +]]> + +

To achieve fully immutable objects in this example, we can also use constructor injection for UserRole

+ + + + + + + +]]> + +

MyBatis needs to be explicitly told that the results have been ordered in such a way, that when a new main row is retrieved from the result set, no previous row results will be retrieved again. This can be set on the statement with the resultOrdered attribute:

+ + + select + u.id, + u.username, + r.id as role_id, + r.role as role_role, + from user u + left join user_role ur on u.id = ur.user_id + inner join role r on r.id = ur.role_id + order by u.id, r.id + +]]> + +

Note that order by is specified to order the results correctly. We can imagine the output to look something like:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
row_nru.idu.usernamer.idr.role
11John1Admins
21John2Users
32Jacknullnull
43Peter2Users
53Peter3Maintainers
63Peter4Approvers
+ +

After this query is run, we would have the following results:

+ + + +

If the 5th row here would have somehow appeared below the first row (via some ORDER BY), MyBatis would not be able to fully construct the John user correctly using constructor collection mapping.

+ +

It is important to note that mixed mappings have limited support, i.e. property mappings combined with nested constructor mappings are likely to fail. +When using this functionality, it is preferable for the entire mapping hierarchy to use immutable constructor mappings.

+ +

关联

diff --git a/src/test/java/org/apache/ibatis/binding/BindingTest.java b/src/test/java/org/apache/ibatis/binding/BindingTest.java index a033c843c66..c20f51f2eec 100644 --- a/src/test/java/org/apache/ibatis/binding/BindingTest.java +++ b/src/test/java/org/apache/ibatis/binding/BindingTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2024 the original author or authors. + * Copyright 2009-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ import static com.googlecode.catchexception.apis.BDDCatchException.caughtException; import static com.googlecode.catchexception.apis.BDDCatchException.when; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.BDDAssertions.then; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -64,7 +65,6 @@ import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; class BindingTest { @@ -399,7 +399,6 @@ void shouldExecuteBoundSelectBlogUsingConstructorWithResultMapAndProperties() { } } - @Disabled @Test // issue #480 and #101 void shouldExecuteBoundSelectBlogUsingConstructorWithResultMapCollection() { try (SqlSession session = sqlSessionFactory.openSession()) { @@ -409,7 +408,7 @@ void shouldExecuteBoundSelectBlogUsingConstructorWithResultMapCollection() { assertEquals("Jim Business", blog.getTitle()); assertNotNull(blog.getAuthor(), "author should not be null"); List posts = blog.getPosts(); - assertTrue(posts != null && !posts.isEmpty(), "posts should not be empty"); + assertThat(posts).isNotNull().hasSize(2); } } diff --git a/src/test/java/org/apache/ibatis/domain/blog/ImmutableAuthor.java b/src/test/java/org/apache/ibatis/domain/blog/ImmutableAuthor.java deleted file mode 100644 index ebdefb4f7ea..00000000000 --- a/src/test/java/org/apache/ibatis/domain/blog/ImmutableAuthor.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2009-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.ibatis.domain.blog; - -import java.io.Serializable; - -public class ImmutableAuthor implements Serializable { - private static final long serialVersionUID = 1L; - protected final int id; - protected final String username; - protected final String password; - protected final String email; - protected final String bio; - protected final Section favouriteSection; - - public ImmutableAuthor(int id, String username, String password, String email, String bio, Section section) { - this.id = id; - this.username = username; - this.password = password; - this.email = email; - this.bio = bio; - this.favouriteSection = section; - } - - public int getId() { - return id; - } - - public String getUsername() { - return username; - } - - public String getPassword() { - return password; - } - - public String getEmail() { - return email; - } - - public String getBio() { - return bio; - } - - public Section getFavouriteSection() { - return favouriteSection; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof Author author)) { - return false; - } - - if (id != author.id || (bio != null ? !bio.equals(author.bio) : author.bio != null) - || (email != null ? !email.equals(author.email) : author.email != null) - || (password != null ? !password.equals(author.password) : author.password != null)) { - return false; - } - return username != null ? username.equals(author.username) : author.username == null && favouriteSection != null - ? favouriteSection.equals(author.favouriteSection) : author.favouriteSection == null; - } - - @Override - public int hashCode() { - int result = id; - result = 31 * result + (username != null ? username.hashCode() : 0); - result = 31 * result + (password != null ? password.hashCode() : 0); - result = 31 * result + (email != null ? email.hashCode() : 0); - result = 31 * result + (bio != null ? bio.hashCode() : 0); - return 31 * result + (favouriteSection != null ? favouriteSection.hashCode() : 0); - } - - @Override - public String toString() { - return id + " " + username + " " + password + " " + email; - } -} diff --git a/src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutableAuthor.java b/src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutableAuthor.java new file mode 100644 index 00000000000..0a6c1f78c75 --- /dev/null +++ b/src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutableAuthor.java @@ -0,0 +1,67 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.domain.blog.immutable; + +import org.apache.ibatis.domain.blog.Section; + +public class ImmutableAuthor { + + private final int id; + private final String username; + private final String password; + private final String email; + private final String bio; + private final Section favouriteSection; + + public ImmutableAuthor(int id, String username, String password, String email, String bio, Section section) { + this.id = id; + this.username = username; + this.password = password; + this.email = email; + this.bio = bio; + this.favouriteSection = section; + } + + public int getId() { + return id; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getEmail() { + return email; + } + + public String getBio() { + return bio; + } + + public Section getFavouriteSection() { + return favouriteSection; + } + + @Override + public String toString() { + return "ImmutableAuthor{" + "id=" + id + ", username='" + username + '\'' + ", password='" + password + '\'' + + ", email='" + email + '\'' + ", bio='" + bio + '\'' + ", favouriteSection=" + favouriteSection + '}'; + } +} diff --git a/src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutableBlog.java b/src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutableBlog.java new file mode 100644 index 00000000000..ece455a3547 --- /dev/null +++ b/src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutableBlog.java @@ -0,0 +1,62 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.domain.blog.immutable; + +import java.util.ArrayList; +import java.util.List; + +public class ImmutableBlog { + + private final int id; + private final String title; + private final ImmutableAuthor author; + private final List posts; + + public ImmutableBlog(int id, String title, ImmutableAuthor author, List posts) { + this.id = id; + this.title = title; + this.author = author; + this.posts = posts; + } + + public ImmutableBlog(int id, String title, ImmutableAuthor author) { + this.id = id; + this.title = title; + this.author = author; + this.posts = new ArrayList<>(); + } + + public int getId() { + return id; + } + + public String getTitle() { + return title; + } + + public ImmutableAuthor getAuthor() { + return author; + } + + public List getPosts() { + return posts; + } + + @Override + public String toString() { + return "ImmutableBlog{" + "id=" + id + ", title='" + title + '\'' + ", author=" + author + ", posts=" + posts + '}'; + } +} diff --git a/src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutableComment.java b/src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutableComment.java new file mode 100644 index 00000000000..bf734bad814 --- /dev/null +++ b/src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutableComment.java @@ -0,0 +1,46 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.domain.blog.immutable; + +public class ImmutableComment { + + private final int id; + private final String name; + private final String comment; + + public ImmutableComment(int id, String name, String comment) { + this.id = id; + this.name = name; + this.comment = comment; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public String getComment() { + return comment; + } + + @Override + public String toString() { + return "ImmutableComment{" + "id=" + id + ", name='" + name + '\'' + ", comment='" + comment + '\'' + '}'; + } +} diff --git a/src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutablePost.java b/src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutablePost.java new file mode 100644 index 00000000000..0762be84db3 --- /dev/null +++ b/src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutablePost.java @@ -0,0 +1,94 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.domain.blog.immutable; + +import java.util.Date; +import java.util.List; + +import org.apache.ibatis.domain.blog.Section; + +public class ImmutablePost { + + private final int id; + private final ImmutableAuthor author; + private final Date createdOn; + private final Section section; + private final String subject; + private final String body; + private final List comments; + private final List tags; + + public ImmutablePost(int id, ImmutableAuthor author, Date createdOn, Section section, String subject, String body, + List comments, List tags) { + this.id = id; + this.author = author; + this.createdOn = createdOn; + this.section = section; + this.subject = subject; + this.body = body; + this.comments = comments; + this.tags = tags; + } + + public ImmutablePost(int id, ImmutableAuthor author, Date createdOn, Section section, String subject, String body) { + this.id = id; + this.author = author; + this.createdOn = createdOn; + this.section = section; + this.subject = subject; + this.body = body; + this.comments = List.of(); + this.tags = List.of(); + } + + public List getTags() { + return tags; + } + + public int getId() { + return id; + } + + public ImmutableAuthor getAuthor() { + return author; + } + + public Date getCreatedOn() { + return createdOn; + } + + public Section getSection() { + return section; + } + + public String getSubject() { + return subject; + } + + public String getBody() { + return body; + } + + public List getComments() { + return comments; + } + + @Override + public String toString() { + return "ImmutablePost{" + "id=" + id + ", author=" + author + ", createdOn=" + createdOn + ", section=" + section + + ", subject='" + subject + '\'' + ", body='" + body + '\'' + ", comments=" + comments + ", tags=" + tags + '}'; + } +} diff --git a/src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutableTag.java b/src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutableTag.java new file mode 100644 index 00000000000..9eec367ddb0 --- /dev/null +++ b/src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutableTag.java @@ -0,0 +1,40 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.domain.blog.immutable; + +public class ImmutableTag { + + private final int id; + private final String name; + + public ImmutableTag(int id, String name) { + this.id = id; + this.name = name; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "ImmutableTag{" + "id=" + id + ", name='" + name + '\'' + '}'; + } +} diff --git a/src/test/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandlerTest.java b/src/test/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandlerTest.java index a0311464e03..734cdb21b7d 100644 --- a/src/test/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandlerTest.java +++ b/src/test/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandlerTest.java @@ -121,7 +121,8 @@ void shouldThrowExceptionWithColumnName() throws Exception { try { defaultResultSetHandler.createParameterizedResultObject(rsw, null/* resultType */, constructorMappings, - null/* constructorArgTypes */, null/* constructorArgs */, null/* columnPrefix */); + null/* constructorArgTypes */, null/* constructorArgs */, null/* columnPrefix */, false, + /* useCollectionConstructorInjection */ null/* parentRowKey */); Assertions.fail("Should have thrown ExecutorException"); } catch (Exception e) { Assertions.assertTrue(e instanceof ExecutorException, "Expected ExecutorException"); diff --git a/src/test/java/org/apache/ibatis/immutable/ImmutableBlogMapper.java b/src/test/java/org/apache/ibatis/immutable/ImmutableBlogMapper.java new file mode 100644 index 00000000000..3fd6ef4c22a --- /dev/null +++ b/src/test/java/org/apache/ibatis/immutable/ImmutableBlogMapper.java @@ -0,0 +1,36 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.immutable; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.domain.blog.immutable.ImmutableBlog; + +@Mapper +public interface ImmutableBlogMapper { + + ImmutableBlog retrieveFullImmutableBlog(int i); + + List retrieveAllBlogsWithoutPosts(); + + List retrieveAllBlogsWithPostsButNoCommentsOrTags(); + + List retrieveAllBlogsWithMissingConstructor(); + + List retrieveAllBlogsAndPostsJoined(); + +} diff --git a/src/test/java/org/apache/ibatis/immutable/ImmutableConstructorTest.java b/src/test/java/org/apache/ibatis/immutable/ImmutableConstructorTest.java new file mode 100644 index 00000000000..709f72b014a --- /dev/null +++ b/src/test/java/org/apache/ibatis/immutable/ImmutableConstructorTest.java @@ -0,0 +1,151 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.immutable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.tuple; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import javax.sql.DataSource; + +import org.apache.ibatis.BaseDataTest; +import org.apache.ibatis.domain.blog.Section; +import org.apache.ibatis.domain.blog.immutable.ImmutableAuthor; +import org.apache.ibatis.domain.blog.immutable.ImmutableBlog; +import org.apache.ibatis.domain.blog.immutable.ImmutablePost; +import org.apache.ibatis.exceptions.PersistenceException; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.reflection.ReflectionException; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.TransactionFactory; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ImmutableConstructorTest { + + private SqlSessionFactory sqlSessionFactory; + + @BeforeAll + void setup() throws Exception { + final DataSource dataSource = BaseDataTest.createBlogDataSource(); + BaseDataTest.runScript(dataSource, BaseDataTest.BLOG_DDL); + BaseDataTest.runScript(dataSource, BaseDataTest.BLOG_DATA); + + final TransactionFactory transactionFactory = new JdbcTransactionFactory(); + final Environment environment = new Environment("Production", transactionFactory, dataSource); + final Configuration configuration = new Configuration(environment); + + configuration.addMapper(ImmutableBlogMapper.class); + + sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); + } + + @Test + void shouldSelectImmutableBlogUsingCollectionInConstructor() { + try (SqlSession session = sqlSessionFactory.openSession()) { + final ImmutableBlogMapper mapper = session.getMapper(ImmutableBlogMapper.class); + final ImmutableBlog blog = mapper.retrieveFullImmutableBlog(1); + + assertEquals(1, blog.getId()); + assertEquals("Jim Business", blog.getTitle()); + + final ImmutableAuthor author = blog.getAuthor(); + assertThat(author).isNotNull().isInstanceOf(ImmutableAuthor.class); + assertThat(author.getEmail()).isEqualTo("jim@ibatis.apache.org"); + assertThat(author.getFavouriteSection()).isNotNull().isEqualTo(Section.NEWS); + assertThat(author.getUsername()).isEqualTo("jim"); + assertThat(author.getPassword()).isNotEmpty(); + assertThat(author.getId()).isEqualTo(101); + + final List posts = blog.getPosts(); + assertThat(posts).isNotNull().hasSize(2); + + final ImmutablePost postOne = posts.get(0); + assertThat(postOne).isNotNull().isInstanceOf(ImmutablePost.class); + assertThat(postOne.getCreatedOn()).isNotNull(); + assertThat(postOne.getAuthor()).isNotNull(); + assertThat(postOne.getSection()).isEqualTo(Section.NEWS); + assertThat(postOne.getSubject()).isEqualTo("Corn nuts"); + assertThat(postOne.getBody()).isEqualTo("I think if I never smelled another corn nut it would be too soon..."); + assertThat(postOne.getComments()).isNotNull().extracting("name", "comment").containsExactly( + tuple("troll", "I disagree and think..."), tuple("anonymous", "I agree and think troll is an...")); + assertThat(postOne.getTags()).isNotNull().extracting("name").containsExactly("funny", "cool", "food"); + + final ImmutablePost postTwo = posts.get(1); + assertThat(postTwo).isNotNull().isInstanceOf(ImmutablePost.class); + assertThat(postTwo.getCreatedOn()).isNotNull(); + assertThat(postTwo.getAuthor()).isNotNull(); + assertThat(postTwo.getSection()).isEqualTo(Section.VIDEOS); + assertThat(postTwo.getSubject()).isEqualTo("Paul Hogan on Toy Dogs"); + assertThat(postTwo.getBody()).isEqualTo("That's not a dog. THAT's a dog!"); + assertThat(postTwo.getComments()).isNotNull().isEmpty(); + + assertThat(postTwo.getTags()).isNotNull().extracting("name").containsExactly("funny"); + } + } + + @Test + void shouldSelectAllImmutableBlogsUsingCollectionInConstructor() { + try (SqlSession session = sqlSessionFactory.openSession()) { + ImmutableBlogMapper mapper = session.getMapper(ImmutableBlogMapper.class); + List blogs = mapper.retrieveAllBlogsAndPostsJoined(); + + assertThat(blogs).isNotNull().hasSize(2); + for (ImmutableBlog blog : blogs) { + assertThat(blog).isNotNull().isInstanceOf(ImmutableBlog.class).extracting(ImmutableBlog::getPosts).isNotNull(); + } + } + } + + @Test + void shouldSelectBlogWithoutPosts() { + try (SqlSession session = sqlSessionFactory.openSession()) { + ImmutableBlogMapper mapper = session.getMapper(ImmutableBlogMapper.class); + List blogs = mapper.retrieveAllBlogsWithoutPosts(); + + assertThat(blogs).isNotNull().hasSize(2); + } + } + + @Test + void shouldSelectBlogWithPostsButNoCommentsOrTags() { + try (SqlSession session = sqlSessionFactory.openSession()) { + ImmutableBlogMapper mapper = session.getMapper(ImmutableBlogMapper.class); + List blogs = mapper.retrieveAllBlogsWithPostsButNoCommentsOrTags(); + + assertThat(blogs).isNotNull().hasSize(2); + } + } + + @Test + void shouldFailToSelectBlogWithMissingConstructorForPostComments() { + try (SqlSession session = sqlSessionFactory.openSession()) { + ImmutableBlogMapper mapper = session.getMapper(ImmutableBlogMapper.class); + assertThatThrownBy(mapper::retrieveAllBlogsWithMissingConstructor).isInstanceOf(PersistenceException.class) + .hasCauseInstanceOf(ReflectionException.class).hasMessageContaining( + "Error instantiating class org.apache.ibatis.domain.blog.immutable.ImmutablePost with invalid types"); + } + } +} diff --git a/src/test/java/org/apache/ibatis/session/SqlSessionTest.java b/src/test/java/org/apache/ibatis/session/SqlSessionTest.java index 9d6e0ae7085..9c6178aa665 100644 --- a/src/test/java/org/apache/ibatis/session/SqlSessionTest.java +++ b/src/test/java/org/apache/ibatis/session/SqlSessionTest.java @@ -38,10 +38,10 @@ import org.apache.ibatis.domain.blog.Blog; import org.apache.ibatis.domain.blog.Comment; import org.apache.ibatis.domain.blog.DraftPost; -import org.apache.ibatis.domain.blog.ImmutableAuthor; import org.apache.ibatis.domain.blog.Post; import org.apache.ibatis.domain.blog.Section; import org.apache.ibatis.domain.blog.Tag; +import org.apache.ibatis.domain.blog.immutable.ImmutableAuthor; import org.apache.ibatis.domain.blog.mappers.AuthorMapper; import org.apache.ibatis.domain.blog.mappers.AuthorMapperWithMultipleHandlers; import org.apache.ibatis.domain.blog.mappers.AuthorMapperWithRowBounds; diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Aisle.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Aisle.java new file mode 100644 index 00000000000..8c23e7f8c30 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Aisle.java @@ -0,0 +1,72 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.util.Objects; + +public class Aisle { + + private Integer id; + private String name; + + public Aisle() { + super(); + } + + public Aisle(Integer id, String name) { + super(); + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Aisle)) { + return false; + } + Aisle other = (Aisle) obj; + return Objects.equals(id, other.id) && Objects.equals(name, other.name); + } + + @Override + public String toString() { + return "Aisle [id=" + id + ", name=" + name + "]"; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Clerk.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Clerk.java new file mode 100644 index 00000000000..8faff9e1333 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Clerk.java @@ -0,0 +1,72 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.util.Objects; + +public class Clerk { + + private Integer id; + private String name; + + public Clerk() { + super(); + } + + public Clerk(Integer id, String name) { + super(); + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Clerk)) { + return false; + } + Clerk other = (Clerk) obj; + return Objects.equals(id, other.id) && Objects.equals(name, other.name); + } + + @Override + public String toString() { + return "Clerk [id=" + id + ", name=" + name + "]"; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/CollectionInConstructorObjectFactory.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/CollectionInConstructorObjectFactory.java new file mode 100644 index 00000000000..befbd75bc46 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/CollectionInConstructorObjectFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.util.List; + +import org.apache.ibatis.reflection.factory.DefaultObjectFactory; + +public class CollectionInConstructorObjectFactory extends DefaultObjectFactory { + + private static final long serialVersionUID = -5912469844471984785L; + + @SuppressWarnings("unchecked") + @Override + public T create(Class type, List> constructorArgTypes, List constructorArgs) { + if (type == Store4.class) { + return (T) Store4.builder().id((Integer) constructorArgs.get(0)).isles((List) constructorArgs.get(1)) + .build(); + } + return super.create(type, constructorArgTypes, constructorArgs); + } + +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/CollectionInConstructorTest.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/CollectionInConstructorTest.java new file mode 100644 index 00000000000..20aa7db18b2 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/CollectionInConstructorTest.java @@ -0,0 +1,219 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.io.Reader; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.ibatis.BaseDataTest; +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +class CollectionInConstructorTest { + + private static SqlSessionFactory sqlSessionFactory; + + @BeforeAll + static void setUp() throws Exception { + // create an SqlSessionFactory + try (Reader reader = Resources + .getResourceAsReader("org/apache/ibatis/submitted/collection_in_constructor/mybatis-config.xml")) { + sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); + } + + // populate in-memory database + BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), + "org/apache/ibatis/submitted/collection_in_constructor/CreateDB.sql"); + } + + @Test + void testSimple() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + Store store = mapper.getAStore(1); + List aisles = store.getAisles(); + Assertions.assertIterableEquals( + Arrays.asList(new Aisle(101, "Aisle 101"), new Aisle(102, "Aisle 102"), new Aisle(103, "Aisle 103")), aisles); + } + } + + @Test + void testSimpleList() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + List stores = mapper.getStores(); + Assertions.assertIterableEquals( + Arrays.asList(new Aisle(101, "Aisle 101"), new Aisle(102, "Aisle 102"), new Aisle(103, "Aisle 103")), + stores.get(0).getAisles()); + Assertions.assertTrue(stores.get(1).getAisles().isEmpty()); + Assertions.assertIterableEquals(Arrays.asList(new Aisle(104, "Aisle 104"), new Aisle(105, "Aisle 105")), + stores.get(2).getAisles()); + } + } + + @Test + void shouldEmptyListBeReturned() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + Assertions.assertTrue(mapper.getAStore(2).getAisles().isEmpty()); + } + } + + @Test + void testTwoLists() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + Store2 store = mapper.getAStore2(1); + List clerks = store.getClerks(); + List aisles = store.getAisles(); + Assertions.assertIterableEquals(Arrays.asList(new Clerk(1001, "Clerk 1001"), new Clerk(1002, "Clerk 1002"), + new Clerk(1003, "Clerk 1003"), new Clerk(1004, "Clerk 1004"), new Clerk(1005, "Clerk 1005")), clerks); + Assertions.assertIterableEquals( + Arrays.asList(new Aisle(101, "Aisle 101"), new Aisle(102, "Aisle 102"), new Aisle(103, "Aisle 103")), aisles); + } + } + + @Test + void testListOfStrings() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + Store3 store = mapper.getAStore3(1); + List aisleNames = store.getAisleNames(); + Assertions.assertEquals(3, aisleNames.size()); + Assertions.assertIterableEquals(Arrays.asList("Aisle 101", "Aisle 102", "Aisle 103"), aisleNames); + } + } + + @Test + void testObjectWithBuilder() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + Store4 store = mapper.getAStore4(1); + List aisles = store.getAisles(); + Assertions.assertIterableEquals( + Arrays.asList(new Aisle(101, "Aisle 101"), new Aisle(102, "Aisle 102"), new Aisle(103, "Aisle 103")), aisles); + } + } + + @Test + void testTwoListsOfSameResultMap() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + Store5 store = mapper.getAStore5(1); + List clerks = store.getClerks(); + List managers = store.getManagers(); + Assertions.assertIterableEquals(Arrays.asList(new Clerk(1001, "Clerk 1001"), new Clerk(1002, "Clerk 1002"), + new Clerk(1003, "Clerk 1003"), new Clerk(1004, "Clerk 1004"), new Clerk(1005, "Clerk 1005")), clerks); + Assertions.assertIterableEquals(Arrays.asList(new Clerk(1002, "Clerk 1002"), new Clerk(1005, "Clerk 1005")), + managers); + } + } + + @Disabled("Not sure if there is a need for this usage.") + @Test + void testPartiallyImmutableObject() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + Store6 store = mapper.getAStore6(1); + List aisles = store.getAisles(); + Assertions.assertEquals("Store 1", store.getName()); + Assertions.assertEquals(3, aisles.size()); + } + } + + @Test + void testTwoListsOfString() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + Store7 store = mapper.getAStore7(1); + List aisleNames = store.getAisleNames(); + List clerkNames = store.getClerkNames(); + Assertions.assertIterableEquals(Arrays.asList("Aisle 101", "Aisle 102", "Aisle 103"), aisleNames); + Assertions.assertIterableEquals( + Arrays.asList("Clerk 1001", "Clerk 1002", "Clerk 1003", "Clerk 1004", "Clerk 1005"), clerkNames); + } + } + + @Test + void testCollectionArgWithTypeHandler() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + Store8 store = mapper.getAStore8(1); + Assertions.assertIterableEquals(Arrays.asList("a", "b", "c"), store.getStrings()); + } + } + + @Test + void testImmutableNestedObjects() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + Container container = mapper.getAContainer(); + Assertions + .assertEquals( + Arrays.asList( + new Store(1, "Store 1", + Arrays.asList(new Aisle(101, "Aisle 101"), new Aisle(102, "Aisle 102"), + new Aisle(103, "Aisle 103"))), + new Store(2, "Store 2", Collections.emptyList()), + new Store(3, "Store 3", Arrays.asList(new Aisle(104, "Aisle 104"), new Aisle(105, "Aisle 105")))), + container.getStores()); + } + } + + @Test + void testImmutableNestedObjectsWithBadEquals() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + List containers = mapper.getContainers(); + + Container1 expectedContainer1 = new Container1(); + expectedContainer1.setNum(1); + expectedContainer1.setType("storesWithClerks"); + expectedContainer1.setStores(Arrays.asList( + new Store9(1, "Store 1", + Arrays.asList(new Clerk(1001, "Clerk 1001"), new Clerk(1003, "Clerk 1003"), + new Clerk(1004, "Clerk 1004"))), + new Store9(2, "Store 2", Arrays.asList()), new Store9(3, "Store 3", Arrays.asList()))); + + Container1 expectedContainer2 = new Container1(); + expectedContainer2.setNum(1); + expectedContainer2.setType("storesWithManagers"); + expectedContainer2.setStores(Arrays.asList( + new Store9(1, "Store 1", Arrays.asList(new Clerk(1002, "Clerk 1002"), new Clerk(1005, "Clerk 1005"))))); + + // cannot use direct equals as we overwrote it with a bad impl on purpose + org.assertj.core.api.Assertions.assertThat(containers).isNotNull().hasSize(2); + assertContainer1(containers.get(0), expectedContainer1); + assertContainer1(containers.get(1), expectedContainer2); + } + } + + private static void assertContainer1(Container1 container1, Container1 expectedContainer1) { + org.assertj.core.api.Assertions.assertThat(container1).isNotNull().satisfies(c -> { + org.assertj.core.api.Assertions.assertThat(c.getNum()).isEqualTo(expectedContainer1.getNum()); + org.assertj.core.api.Assertions.assertThat(c.getType()).isEqualTo(expectedContainer1.getType()); + org.assertj.core.api.Assertions.assertThat(c.getStores()).isEqualTo(expectedContainer1.getStores()); + }); + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Container.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Container.java new file mode 100644 index 00000000000..620e3bc2506 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Container.java @@ -0,0 +1,41 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.util.List; + +public class Container { + private Integer num; + + private List stores; + + public Integer getNum() { + return num; + } + + public void setNum(Integer num) { + this.num = num; + } + + public List getStores() { + return stores; + } + + public void setStores(List stores) { + this.stores = stores; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Container1.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Container1.java new file mode 100644 index 00000000000..638e8bd8ca4 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Container1.java @@ -0,0 +1,67 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.util.List; +import java.util.Objects; + +public class Container1 { + + private Integer num; + private String type; + private List stores; + + public Integer getNum() { + return num; + } + + public void setNum(Integer num) { + this.num = num; + } + + public List getStores() { + return stores; + } + + public void setStores(List stores) { + this.stores = stores; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @Override + // simulate a misbehaving object with a bad equals override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Container1 that = (Container1) o; + return Objects.equals(num, that.num); + } + + @Override + public int hashCode() { + return Objects.hash(num); + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/CsvToListTypeHandler.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/CsvToListTypeHandler.java new file mode 100644 index 00000000000..3169c728d8a --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/CsvToListTypeHandler.java @@ -0,0 +1,59 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.assertj.core.util.Arrays; + +public class CsvToListTypeHandler extends BaseTypeHandler> { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, List parameter, JdbcType jdbcType) + throws SQLException { + // not relevant for this test + } + + @Override + public List getNullableResult(ResultSet rs, String columnName) throws SQLException { + return stringToList(rs.getString(columnName)); + } + + @Override + public List getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return stringToList(rs.getString(columnIndex)); + } + + @Override + public List getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + return stringToList(cs.getString(columnIndex)); + } + + private List stringToList(String s) { + if (s == null) { + return new ArrayList<>(); + } + return Arrays.asList(s.split(",")); + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Mapper.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Mapper.java new file mode 100644 index 00000000000..0aa4db76a2e --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Mapper.java @@ -0,0 +1,44 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.util.List; + +public interface Mapper { + + Store getAStore(Integer id); + + List getStores(); + + Store2 getAStore2(Integer id); + + Store3 getAStore3(Integer id); + + Store4 getAStore4(Integer id); + + Store5 getAStore5(Integer id); + + Store6 getAStore6(Integer id); + + Store7 getAStore7(Integer id); + + Store8 getAStore8(Integer id); + + Container getAContainer(); + + List getContainers(); + +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store.java new file mode 100644 index 00000000000..0f8dd7a434f --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store.java @@ -0,0 +1,67 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.util.List; +import java.util.Objects; + +public class Store { + + private final Integer id; + private final String name; + private final List aisles; + + public Store(Integer id, String name, List aisles) { + super(); + this.id = id; + this.name = name; + this.aisles = aisles; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public List getAisles() { + return aisles; + } + + @Override + public int hashCode() { + return Objects.hash(id, aisles, name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Store)) { + return false; + } + Store other = (Store) obj; + return Objects.equals(id, other.id) && Objects.equals(aisles, other.aisles) && Objects.equals(name, other.name); + } + + @Override + public String toString() { + return "Store [id=" + id + ", name=" + name + ", aisles=" + aisles + "]"; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store2.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store2.java new file mode 100644 index 00000000000..ca2b782136d --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store2.java @@ -0,0 +1,67 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.util.List; +import java.util.Objects; + +public class Store2 { + + private final Integer id; + private final List clerks; + private final List aisles; + + public Store2(Integer id, List clerks, List aisles) { + super(); + this.id = id; + this.clerks = clerks; + this.aisles = aisles; + } + + public Integer getId() { + return id; + } + + public List getClerks() { + return clerks; + } + + public List getAisles() { + return aisles; + } + + @Override + public int hashCode() { + return Objects.hash(clerks, id, aisles); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Store2)) { + return false; + } + Store2 other = (Store2) obj; + return Objects.equals(clerks, other.clerks) && Objects.equals(id, other.id) && Objects.equals(aisles, other.aisles); + } + + @Override + public String toString() { + return "Store2 [id=" + id + ", clerks=" + clerks + ", aisles=" + aisles + "]"; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store3.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store3.java new file mode 100644 index 00000000000..7866f618797 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store3.java @@ -0,0 +1,61 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.util.List; +import java.util.Objects; + +public class Store3 { + + private final Integer id; + private final List aisleNames; + + public Store3(Integer id, List aisleNames) { + super(); + this.id = id; + this.aisleNames = aisleNames; + } + + public Integer getId() { + return id; + } + + public List getAisleNames() { + return aisleNames; + } + + @Override + public int hashCode() { + return Objects.hash(id, aisleNames); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Store3)) { + return false; + } + Store3 other = (Store3) obj; + return Objects.equals(id, other.id) && Objects.equals(aisleNames, other.aisleNames); + } + + @Override + public String toString() { + return "Store3 [id=" + id + ", aisleNames=" + aisleNames + "]"; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store4.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store4.java new file mode 100644 index 00000000000..3857ff8268c --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store4.java @@ -0,0 +1,86 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.util.List; +import java.util.Objects; + +public class Store4 { + + private final Integer id; + private final List aisles; + + // Using different arg order than the definition + // to ensure the builder is used, see CollectionInConstructorObjectFactory.create + Store4(List aisles, Integer id) { + super(); + this.aisles = aisles; + this.id = id; + } + + public Integer getId() { + return id; + } + + public List getAisles() { + return aisles; + } + + @Override + public int hashCode() { + return Objects.hash(id, aisles); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Store4)) { + return false; + } + Store4 other = (Store4) obj; + return Objects.equals(id, other.id) && Objects.equals(aisles, other.aisles); + } + + @Override + public String toString() { + return "Store4 [id=" + id + ", aisles=" + aisles + "]"; + } + + public static Store4Builder builder() { + return new Store4Builder(); + } + + public static class Store4Builder { + private Integer id; + private List isles; + + public Store4Builder id(Integer id) { + this.id = id; + return this; + } + + public Store4Builder isles(List isles) { + this.isles = isles; + return this; + } + + public Store4 build() { + return new Store4(isles, id); + } + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store5.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store5.java new file mode 100644 index 00000000000..5342a3cb323 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store5.java @@ -0,0 +1,68 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.util.List; +import java.util.Objects; + +public class Store5 { + + private final Integer id; + private final List clerks; + private final List managers; + + public Store5(Integer id, List clerks, List managers) { + super(); + this.id = id; + this.clerks = clerks; + this.managers = managers; + } + + public Integer getId() { + return id; + } + + public List getClerks() { + return clerks; + } + + public List getManagers() { + return managers; + } + + @Override + public int hashCode() { + return Objects.hash(clerks, id, managers); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Store5)) { + return false; + } + Store5 other = (Store5) obj; + return Objects.equals(clerks, other.clerks) && Objects.equals(id, other.id) + && Objects.equals(managers, other.managers); + } + + @Override + public String toString() { + return "Store5 [id=" + id + ", clerks=" + clerks + ", managers=" + managers + "]"; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store6.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store6.java new file mode 100644 index 00000000000..82add491225 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store6.java @@ -0,0 +1,70 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.util.List; +import java.util.Objects; + +public class Store6 { + + private final Integer id; + private String name; + private final List aisles; + + public Store6(Integer id, List aisles) { + super(); + this.id = id; + this.aisles = aisles; + } + + public Integer getId() { + return id; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public List getAisles() { + return aisles; + } + + @Override + public int hashCode() { + return Objects.hash(id, aisles, name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Store6)) { + return false; + } + Store6 other = (Store6) obj; + return Objects.equals(id, other.id) && Objects.equals(aisles, other.aisles) && Objects.equals(name, other.name); + } + + @Override + public String toString() { + return "Store [id=" + id + ", name=" + name + ", aisles=" + aisles + "]"; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store7.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store7.java new file mode 100644 index 00000000000..6df3c459d33 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store7.java @@ -0,0 +1,68 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.util.List; +import java.util.Objects; + +public class Store7 { + + private final Integer id; + private final List aisleNames; + private final List clerkNames; + + public Store7(Integer id, List aisleNames, List clerkNames) { + super(); + this.id = id; + this.aisleNames = aisleNames; + this.clerkNames = clerkNames; + } + + public Integer getId() { + return id; + } + + public List getAisleNames() { + return aisleNames; + } + + public List getClerkNames() { + return clerkNames; + } + + @Override + public int hashCode() { + return Objects.hash(clerkNames, id, aisleNames); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Store7)) { + return false; + } + Store7 other = (Store7) obj; + return Objects.equals(clerkNames, other.clerkNames) && Objects.equals(id, other.id) + && Objects.equals(aisleNames, other.aisleNames); + } + + @Override + public String toString() { + return "Store7 [id=" + id + ", aisleNames=" + aisleNames + ", clerkNames=" + clerkNames + "]"; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store8.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store8.java new file mode 100644 index 00000000000..5c6d3a786ba --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store8.java @@ -0,0 +1,67 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.util.List; +import java.util.Objects; + +public class Store8 { + + private final Integer id; + private final String name; + private final List strings; + + public Store8(Integer id, String name, List strings) { + super(); + this.id = id; + this.name = name; + this.strings = strings; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public List getStrings() { + return strings; + } + + @Override + public int hashCode() { + return Objects.hash(id, name, strings); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Store8)) { + return false; + } + Store8 other = (Store8) obj; + return Objects.equals(id, other.id) && Objects.equals(name, other.name) && Objects.equals(strings, other.strings); + } + + @Override + public String toString() { + return "Store8 [id=" + id + ", name=" + name + ", strings=" + strings + "]"; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store9.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store9.java new file mode 100644 index 00000000000..46981f3491b --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store9.java @@ -0,0 +1,66 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_in_constructor; + +import java.util.List; +import java.util.Objects; + +public class Store9 { + + private final Integer id; + private final String name; + private final List clerks; + + public Store9(Integer id, String name, List clerks) { + this.id = id; + this.name = name; + this.clerks = clerks; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public List getClerks() { + return clerks; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Store9)) { + return false; + } + Store9 other = (Store9) obj; + return Objects.equals(id, other.id); + } + + @Override + public String toString() { + return "Store9 [id=" + id + ", name=" + name + ", clerks=" + clerks + "]"; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_injection/CollectionInjectionTest.java b/src/test/java/org/apache/ibatis/submitted/collection_injection/CollectionInjectionTest.java new file mode 100644 index 00000000000..2fd006805c3 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/CollectionInjectionTest.java @@ -0,0 +1,130 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_injection; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.Reader; + +import org.apache.ibatis.BaseDataTest; +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.submitted.collection_injection.immutable.HousePortfolio; +import org.apache.ibatis.submitted.collection_injection.immutable.ImmutableDefect; +import org.apache.ibatis.submitted.collection_injection.immutable.ImmutableFurniture; +import org.apache.ibatis.submitted.collection_injection.immutable.ImmutableHouse; +import org.apache.ibatis.submitted.collection_injection.immutable.ImmutableHouseMapper; +import org.apache.ibatis.submitted.collection_injection.immutable.ImmutableRoom; +import org.apache.ibatis.submitted.collection_injection.immutable.ImmutableRoomDetail; +import org.apache.ibatis.submitted.collection_injection.property.Defect; +import org.apache.ibatis.submitted.collection_injection.property.Furniture; +import org.apache.ibatis.submitted.collection_injection.property.House; +import org.apache.ibatis.submitted.collection_injection.property.HouseMapper; +import org.apache.ibatis.submitted.collection_injection.property.Room; +import org.apache.ibatis.submitted.collection_injection.property.RoomDetail; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class CollectionInjectionTest { + + private static SqlSessionFactory sqlSessionFactory; + + @BeforeAll + static void setUp() throws Exception { + try (Reader reader = Resources + .getResourceAsReader("org/apache/ibatis/submitted/collection_injection/mybatis_config.xml")) { + sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); + } + + BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), + "org/apache/ibatis/submitted/collection_injection/create_db.sql"); + BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), + "org/apache/ibatis/submitted/collection_injection/data_load_small.sql"); + } + + @Test + void shouldSelectAllHousesUsingConstructorInjection() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + final ImmutableHouseMapper mapper = sqlSession.getMapper(ImmutableHouseMapper.class); + ImmutableHouse house = mapper.getHouse(1); + Assertions.assertNotNull(house); + + final StringBuilder builder = new StringBuilder(); + builder.append("\n").append(house.getName()); + for (ImmutableRoom room : house.getRooms()) { + ImmutableRoomDetail roomDetail = room.getRoomDetail(); + String detailString = String.format(" (size=%d, height=%d, type=%s)", roomDetail.getRoomSize(), + roomDetail.getWallHeight(), roomDetail.getWallType()); + builder.append("\n").append("\t").append(room.getName()).append(detailString); + for (ImmutableFurniture furniture : room.getFurniture()) { + builder.append("\n").append("\t\t").append(furniture.getDescription()); + for (ImmutableDefect defect : furniture.getDefects()) { + builder.append("\n").append("\t\t\t").append(defect.getDefect()); + } + } + } + + assertResult(builder); + } + } + + @Test + void shouldSelectAllHousesUsingPropertyInjection() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + final HouseMapper mapper = sqlSession.getMapper(HouseMapper.class); + final House house = mapper.getHouse(1); + Assertions.assertNotNull(house); + + final StringBuilder builder = new StringBuilder(); + builder.append("\n").append(house.getName()); + for (Room room : house.getRooms()) { + RoomDetail roomDetail = room.getRoomDetail(); + String detailString = String.format(" (size=%d, height=%d, type=%s)", roomDetail.getRoomSize(), + roomDetail.getWallHeight(), roomDetail.getWallType()); + builder.append("\n").append("\t").append(room.getName()).append(detailString); + for (Furniture furniture : room.getFurniture()) { + builder.append("\n").append("\t\t").append(furniture.getDescription()); + for (Defect defect : furniture.getDefects()) { + builder.append("\n").append("\t\t\t").append(defect.getDefect()); + } + } + } + + assertResult(builder); + } + } + + private static void assertResult(StringBuilder builder) { + String expected = "\nMyBatis Headquarters" + "\n\tKitchen (size=25, height=20, type=Brick)" + "\n\t\tCoffee machine" + + "\n\t\t\tDoes not work" + "\n\t\tFridge" + "\n\tDining room (size=100, height=10, type=Wood)" + "\n\t\tTable" + + "\n\tProgramming room (size=200, height=15, type=Steel)" + "\n\t\tBig screen" + "\n\t\tLaptop" + + "\n\t\t\tCannot run intellij"; + + assertThat(builder.toString()).isNotEmpty().isEqualTo(expected); + } + + @Test + void getHousePortfolio() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + final ImmutableHouseMapper mapper = sqlSession.getMapper(ImmutableHouseMapper.class); + final HousePortfolio portfolio = mapper.getHousePortfolio(1); + Assertions.assertNotNull(portfolio); + } + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/HousePortfolio.java b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/HousePortfolio.java new file mode 100644 index 00000000000..409160cdcdd --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/HousePortfolio.java @@ -0,0 +1,40 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_injection.immutable; + +import java.util.List; + +public class HousePortfolio { + + private int portfolioId; + private List houses; + + public int getPortfolioId() { + return portfolioId; + } + + public void setPortfolioId(int portfolioId) { + this.portfolioId = portfolioId; + } + + public List getHouses() { + return houses; + } + + public void setHouses(List houses) { + this.houses = houses; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableDefect.java b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableDefect.java new file mode 100644 index 00000000000..1c74eaced2f --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableDefect.java @@ -0,0 +1,39 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_injection.immutable; + +public class ImmutableDefect { + private final int id; + private final String defect; + + public ImmutableDefect(int id, String defect) { + this.id = id; + this.defect = defect; + } + + public int getId() { + return id; + } + + public String getDefect() { + return defect; + } + + @Override + public String toString() { + return "ImmutableDefect{" + "id=" + id + ", defect='" + defect + '\'' + '}'; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableFurniture.java b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableFurniture.java new file mode 100644 index 00000000000..31b245b4640 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableFurniture.java @@ -0,0 +1,48 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_injection.immutable; + +import java.util.List; + +public class ImmutableFurniture { + private final int id; + private final String description; + private final List defects; + + public ImmutableFurniture(int id, String description, List defects) { + this.id = id; + this.description = description; + this.defects = defects; + } + + public int getId() { + return id; + } + + public String getDescription() { + return description; + } + + public List getDefects() { + return defects; + } + + @Override + public String toString() { + return "ImmutableFurniture{" + "id=" + id + ", description='" + description + '\'' + ", defects='" + defects + '\'' + + '}'; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableHouse.java b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableHouse.java new file mode 100644 index 00000000000..7eb819a3efe --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableHouse.java @@ -0,0 +1,47 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_injection.immutable; + +import java.util.List; + +public class ImmutableHouse { + private final int id; + private final String name; + private final List rooms; + + public ImmutableHouse(int id, String name, List rooms) { + this.id = id; + this.name = name; + this.rooms = rooms; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public List getRooms() { + return rooms; + } + + @Override + public String toString() { + return "ImmutableHouse{" + "id=" + id + ", name='" + name + '\'' + ", rooms=" + rooms + '}'; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableHouseMapper.java b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableHouseMapper.java new file mode 100644 index 00000000000..5a97da27ae9 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableHouseMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_injection.immutable; + +import java.util.List; + +public interface ImmutableHouseMapper { + + List getAllHouses(); + + ImmutableHouse getHouse(int it); + + HousePortfolio getHousePortfolio(int id); +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableRoom.java b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableRoom.java new file mode 100644 index 00000000000..43c3c419212 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableRoom.java @@ -0,0 +1,54 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_injection.immutable; + +import java.util.List; + +public class ImmutableRoom { + private final int id; + private final String name; + private final ImmutableRoomDetail roomDetail; + private final List furniture; + + public ImmutableRoom(int id, String name, ImmutableRoomDetail roomDetail, List furniture) { + this.id = id; + this.name = name; + this.roomDetail = roomDetail; + this.furniture = furniture; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public ImmutableRoomDetail getRoomDetail() { + return roomDetail; + } + + public List getFurniture() { + return furniture; + } + + @Override + public String toString() { + return "ImmutableRoom{" + "id=" + id + ", name='" + name + '\'' + ", roomDetail=" + roomDetail + ", furniture=" + + furniture + '}'; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableRoomDetail.java b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableRoomDetail.java new file mode 100644 index 00000000000..63a0f04dff9 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableRoomDetail.java @@ -0,0 +1,47 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_injection.immutable; + +public class ImmutableRoomDetail { + + private final String wallType; + private final int wallHeight; + private final int roomSize; + + public ImmutableRoomDetail(final String wallType, final int wallHeight, final int roomSize) { + this.wallType = wallType; + this.wallHeight = wallHeight; + this.roomSize = roomSize; + } + + public String getWallType() { + return wallType; + } + + public int getWallHeight() { + return wallHeight; + } + + public int getRoomSize() { + return roomSize; + } + + @Override + public String toString() { + return "ImmutableRoomDetail{" + "wallType='" + wallType + '\'' + ", wallHeight=" + wallHeight + ", roomSize=" + + roomSize + '}'; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_injection/property/Defect.java b/src/test/java/org/apache/ibatis/submitted/collection_injection/property/Defect.java new file mode 100644 index 00000000000..73a2624e68f --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/property/Defect.java @@ -0,0 +1,43 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_injection.property; + +public class Defect { + private int id; + private String defect; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getDefect() { + return defect; + } + + public void setDefect(String defect) { + this.defect = defect; + } + + @Override + public String toString() { + return "Defect{" + "id=" + id + ", defect='" + defect + '\'' + '}'; + + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_injection/property/Furniture.java b/src/test/java/org/apache/ibatis/submitted/collection_injection/property/Furniture.java new file mode 100644 index 00000000000..0253a36dddb --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/property/Furniture.java @@ -0,0 +1,53 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_injection.property; + +import java.util.List; + +public class Furniture { + private int id; + private String description; + private List defects; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getDefects() { + return defects; + } + + public void setDefects(List defects) { + this.defects = defects; + } + + @Override + public String toString() { + return "Furniture{" + "id=" + id + ", description='" + description + '\'' + ", defects='" + defects + '\'' + '}'; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_injection/property/House.java b/src/test/java/org/apache/ibatis/submitted/collection_injection/property/House.java new file mode 100644 index 00000000000..bcdb3bcc20b --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/property/House.java @@ -0,0 +1,53 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_injection.property; + +import java.util.List; + +public class House { + private int id; + private String name; + private List rooms; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getRooms() { + return rooms; + } + + public void setRooms(List rooms) { + this.rooms = rooms; + } + + @Override + public String toString() { + return "House{" + "id=" + id + ", name='" + name + '\'' + ", rooms=" + rooms + '}'; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_injection/property/HouseMapper.java b/src/test/java/org/apache/ibatis/submitted/collection_injection/property/HouseMapper.java new file mode 100644 index 00000000000..5921ce6f87a --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/property/HouseMapper.java @@ -0,0 +1,26 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_injection.property; + +import java.util.List; + +public interface HouseMapper { + + List getAllHouses(); + + House getHouse(int it); + +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_injection/property/Room.java b/src/test/java/org/apache/ibatis/submitted/collection_injection/property/Room.java new file mode 100644 index 00000000000..d22f396d048 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/property/Room.java @@ -0,0 +1,63 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_injection.property; + +import java.util.List; + +public class Room { + private int id; + private String name; + private RoomDetail roomDetail; + private List furniture; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public RoomDetail getRoomDetail() { + return roomDetail; + } + + public void setRoomDetail(RoomDetail roomDetail) { + this.roomDetail = roomDetail; + } + + public List getFurniture() { + return furniture; + } + + public void setFurniture(List furniture) { + this.furniture = furniture; + } + + @Override + public String toString() { + return "Room{" + "id=" + id + ", name='" + name + '\'' + ", roomDetail=" + roomDetail + ", furniture=" + furniture + + '}'; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/collection_injection/property/RoomDetail.java b/src/test/java/org/apache/ibatis/submitted/collection_injection/property/RoomDetail.java new file mode 100644 index 00000000000..a0a8e55e9ec --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/property/RoomDetail.java @@ -0,0 +1,53 @@ +/* + * Copyright 2009-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ibatis.submitted.collection_injection.property; + +public class RoomDetail { + + private String wallType; + private int wallHeight; + private int roomSize; + + public String getWallType() { + return wallType; + } + + public void setWallType(String wallType) { + this.wallType = wallType; + } + + public int getWallHeight() { + return wallHeight; + } + + public void setWallHeight(int wallHeight) { + this.wallHeight = wallHeight; + } + + public int getRoomSize() { + return roomSize; + } + + public void setRoomSize(int roomSize) { + this.roomSize = roomSize; + } + + @Override + public String toString() { + return "RoomDetail{" + "wallType='" + wallType + '\'' + ", wallHeight=" + wallHeight + ", roomSize=" + roomSize + + '}'; + } +} diff --git a/src/test/resources/org/apache/ibatis/binding/BoundBlogMapper.xml b/src/test/resources/org/apache/ibatis/binding/BoundBlogMapper.xml index 9a62131333c..8a3e4f8cd58 100644 --- a/src/test/resources/org/apache/ibatis/binding/BoundBlogMapper.xml +++ b/src/test/resources/org/apache/ibatis/binding/BoundBlogMapper.xml @@ -1,7 +1,7 @@ + + + + + + + + + + + @@ -71,7 +85,7 @@ - + @@ -122,7 +136,7 @@ + + where b.id = #{id} + + + + + + + + + + + diff --git a/src/test/resources/org/apache/ibatis/submitted/collection_in_constructor/CreateDB.sql b/src/test/resources/org/apache/ibatis/submitted/collection_in_constructor/CreateDB.sql new file mode 100644 index 00000000000..aff22949d4b --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/collection_in_constructor/CreateDB.sql @@ -0,0 +1,53 @@ +-- +-- Copyright 2009-2024 the original author or authors. +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +drop table store if exists; +drop table aisle if exists; +drop table clerk if exists; + +create table store ( + id int, + name varchar(20) +); + +create table aisle ( + id int, + name varchar(20), + store_id int +); + +create table clerk ( + id int, + name varchar(20), + is_manager boolean, + store_id int +); + +insert into store (id, name) values(1, 'Store 1'); +insert into store (id, name) values(2, 'Store 2'); +insert into store (id, name) values(3, 'Store 3'); + +insert into aisle (id, name, store_id) values(101, 'Aisle 101', 1); +insert into aisle (id, name, store_id) values(102, 'Aisle 102', 1); +insert into aisle (id, name, store_id) values(103, 'Aisle 103', 1); +insert into aisle (id, name, store_id) values(104, 'Aisle 104', 3); +insert into aisle (id, name, store_id) values(105, 'Aisle 105', 3); + +insert into clerk (id, name, is_manager, store_id) values (1001, 'Clerk 1001', 0, 1); +insert into clerk (id, name, is_manager, store_id) values (1002, 'Clerk 1002', 1, 1); +insert into clerk (id, name, is_manager, store_id) values (1003, 'Clerk 1003', 0, 1); +insert into clerk (id, name, is_manager, store_id) values (1004, 'Clerk 1004', 0, 1); +insert into clerk (id, name, is_manager, store_id) values (1005, 'Clerk 1005', 1, 1); diff --git a/src/test/resources/org/apache/ibatis/submitted/collection_in_constructor/Mapper.xml b/src/test/resources/org/apache/ibatis/submitted/collection_in_constructor/Mapper.xml new file mode 100644 index 00000000000..47aa2887c88 --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/collection_in_constructor/Mapper.xml @@ -0,0 +1,280 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/org/apache/ibatis/submitted/collection_in_constructor/mybatis-config.xml b/src/test/resources/org/apache/ibatis/submitted/collection_in_constructor/mybatis-config.xml new file mode 100644 index 00000000000..d58b4ba49e0 --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/collection_in_constructor/mybatis-config.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/org/apache/ibatis/submitted/collection_injection/create_db.sql b/src/test/resources/org/apache/ibatis/submitted/collection_injection/create_db.sql new file mode 100644 index 00000000000..7fc3bb99933 --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/collection_injection/create_db.sql @@ -0,0 +1,55 @@ +-- +-- Copyright 2009-2024 the original author or authors. +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +drop table defect if exists; +drop table furniture if exists; +drop table room if exists; +drop table house if exists; + +create table house ( + id int not null primary key, + name varchar(255) +); + +create table room ( + id int not null primary key, + name varchar(255), + house_id int, + size_m2 int, + wall_type varchar(10), + wall_height int +); + +alter table room add constraint fk_room_house_id + foreign key (house_id) references house (id); + +create table furniture ( + id int not null primary key, + description varchar(255), + room_id int +); + +alter table furniture add constraint fk_furniture_room_id + foreign key (room_id) references room (id); + +create table defect ( + id int not null primary key, + defect varchar(255), + furniture_id int +); + +alter table defect add constraint fk_defects_furniture_id + foreign key (furniture_id) references furniture (id); diff --git a/src/test/resources/org/apache/ibatis/submitted/collection_injection/data_load_small.sql b/src/test/resources/org/apache/ibatis/submitted/collection_injection/data_load_small.sql new file mode 100644 index 00000000000..d076fbd745f --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/collection_injection/data_load_small.sql @@ -0,0 +1,30 @@ +-- +-- Copyright 2009-2024 the original author or authors. +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + +insert into house (id, name) values ( 1, 'MyBatis Headquarters' ); + +insert into room (id, name, house_id, size_m2, wall_type, wall_height) VALUES ( 1, 'Kitchen', 1, 25, 'Brick', 20 ); +insert into room (id, name, house_id, size_m2, wall_type, wall_height) VALUES ( 2, 'Dining room', 1, 100, 'Wood', 10 ); +insert into room (id, name, house_id, size_m2, wall_type, wall_height) VALUES ( 3, 'Programming room', 1, 200, 'Steel', 15 ); + +insert into furniture (id, description, room_id) VALUES ( 1, 'Coffee machine', 1); +insert into furniture (id, description, room_id) VALUES ( 2, 'Fridge', 1); +insert into furniture (id, description, room_id) VALUES ( 3, 'Table', 2); +insert into furniture (id, description, room_id) VALUES ( 4, 'Big screen', 3); +insert into furniture (id, description, room_id) VALUES ( 5, 'Laptop', 3); + +insert into defect (id, defect, furniture_id) VALUES ( 1, 'Does not work', 1 ); +insert into defect (id, defect, furniture_id) VALUES ( 2, 'Cannot run intellij', 5 ); diff --git a/src/test/resources/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableHouseMapper.xml b/src/test/resources/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableHouseMapper.xml new file mode 100644 index 00000000000..f504a62fb03 --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableHouseMapper.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select + + 1 as portfolioId + + , h.* + + , r.id as room_id + , r.name as room_name + , r.size_m2 as room_size_m2 + , r.wall_type as room_wall_type + , r.wall_height as room_wall_height + + , f.id as room_furniture_id + , f.description as room_furniture_description + + , d.id as room_furniture_defect_id + , d.defect as room_furniture_defect_defect + + from house h + left join room r on r.house_id = h.id + left join furniture f on f.room_id = r.id + left join defect d on d.furniture_id = f.id + + + + + + + + + + + + + diff --git a/src/test/resources/org/apache/ibatis/submitted/collection_injection/mybatis_config.xml b/src/test/resources/org/apache/ibatis/submitted/collection_injection/mybatis_config.xml new file mode 100644 index 00000000000..735a80163e2 --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/collection_injection/mybatis_config.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/org/apache/ibatis/submitted/collection_injection/property/HouseMapper.xml b/src/test/resources/org/apache/ibatis/submitted/collection_injection/property/HouseMapper.xml new file mode 100644 index 00000000000..53b332513c0 --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/collection_injection/property/HouseMapper.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select h.* + + , r.id as room_id + , r.name as room_name + , r.size_m2 as room_size_m2 + , r.wall_type as room_wall_type + , r.wall_height as room_wall_height + + , f.id as room_furniture_id + , f.description as room_furniture_description + + , d.id as room_furniture_defect_id + , d.defect as room_furniture_defect_defect + + from house h + left join room r on r.house_id = h.id + left join furniture f on f.room_id = r.id + left join defect d on d.furniture_id = f.id + + + + + +