From 5c23022e850cd8b86596734482462f207d163210 Mon Sep 17 00:00:00 2001 From: Willie Scholtz Date: Fri, 8 Mar 2024 12:37:40 +0100 Subject: [PATCH 01/14] 101: Add support for immutable collection constructor creation - completely isolate new behaviour from existing via flag `experimentalConstructorCollectionMapping` - tested with multiple nested levels of mapping --- pom.xml | 4 + .../ibatis/builder/xml/XMLConfigBuilder.java | 2 + .../resultset/DefaultResultSetHandler.java | 257 ++++++++++++++++-- .../resultset/PendingConstructorCreation.java | 232 ++++++++++++++++ .../resultset/PendingCreationMetaInfo.java | 42 +++ .../org/apache/ibatis/mapping/ResultMap.java | 16 +- .../apache/ibatis/session/Configuration.java | 9 + src/site/markdown/configuration.md | 69 ++--- src/site/markdown/sqlmap-xml.md | 120 ++++++-- .../apache/ibatis/binding/BindingTest.java | 22 +- .../ibatis/binding/BoundBlogMapper.java | 2 +- .../ConstructorCollectionBindingTest.java | 76 ++++++ .../ibatis/domain/blog/ImmutableAuthor.java | 104 ------- .../blog/immutable/ImmutableAuthor.java | 67 +++++ .../domain/blog/immutable/ImmutableBlog.java | 62 +++++ .../blog/immutable/ImmutableComment.java | 46 ++++ .../domain/blog/immutable/ImmutablePost.java | 94 +++++++ .../domain/blog/immutable/ImmutableTag.java | 40 +++ .../DefaultResultSetHandlerTest.java | 5 +- .../ibatis/immutable/ImmutableBlogMapper.java | 36 +++ .../immutable/ImmutableConstructorTest.java | 152 +++++++++++ .../apache/ibatis/session/SqlSessionTest.java | 4 +- .../CollectionInjectionTest.java | 120 ++++++++ .../immutable/ImmutableDefect.java | 39 +++ .../immutable/ImmutableFurniture.java | 48 ++++ .../immutable/ImmutableHouse.java | 47 ++++ .../immutable/ImmutableHouseMapper.java | 26 ++ .../immutable/ImmutableRoom.java | 54 ++++ .../immutable/ImmutableRoomDetail.java | 47 ++++ .../collection_injection/property/Defect.java | 43 +++ .../property/Furniture.java | 53 ++++ .../collection_injection/property/House.java | 53 ++++ .../property/HouseMapper.java | 26 ++ .../collection_injection/property/Room.java | 63 +++++ .../property/RoomDetail.java | 53 ++++ .../apache/ibatis/binding/BoundBlogMapper.xml | 20 +- .../apache/ibatis/builder/AuthorMapper.xml | 4 +- .../ibatis/builder/xsd/AuthorMapper.xml | 4 +- .../ibatis/immutable/ImmutableBlogMapper.xml | 214 +++++++++++++++ .../collection_injection/create_db.sql | 55 ++++ .../collection_injection/data_load_small.sql | 30 ++ .../immutable/ImmutableHouseMapper.xml | 95 +++++++ .../collection_injection/mybatis_config.xml | 59 ++++ .../property/HouseMapper.xml | 84 ++++++ 44 files changed, 2499 insertions(+), 199 deletions(-) create mode 100644 src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java create mode 100644 src/main/java/org/apache/ibatis/executor/resultset/PendingCreationMetaInfo.java create mode 100644 src/test/java/org/apache/ibatis/binding/ConstructorCollectionBindingTest.java delete mode 100644 src/test/java/org/apache/ibatis/domain/blog/ImmutableAuthor.java create mode 100644 src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutableAuthor.java create mode 100644 src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutableBlog.java create mode 100644 src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutableComment.java create mode 100644 src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutablePost.java create mode 100644 src/test/java/org/apache/ibatis/domain/blog/immutable/ImmutableTag.java create mode 100644 src/test/java/org/apache/ibatis/immutable/ImmutableBlogMapper.java create mode 100644 src/test/java/org/apache/ibatis/immutable/ImmutableConstructorTest.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_injection/CollectionInjectionTest.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableDefect.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableFurniture.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableHouse.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableHouseMapper.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableRoom.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableRoomDetail.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_injection/property/Defect.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_injection/property/Furniture.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_injection/property/House.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_injection/property/HouseMapper.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_injection/property/Room.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_injection/property/RoomDetail.java create mode 100644 src/test/resources/org/apache/ibatis/immutable/ImmutableBlogMapper.xml create mode 100644 src/test/resources/org/apache/ibatis/submitted/collection_injection/create_db.sql create mode 100644 src/test/resources/org/apache/ibatis/submitted/collection_injection/data_load_small.sql create mode 100644 src/test/resources/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableHouseMapper.xml create mode 100644 src/test/resources/org/apache/ibatis/submitted/collection_injection/mybatis_config.xml create mode 100644 src/test/resources/org/apache/ibatis/submitted/collection_injection/property/HouseMapper.xml diff --git a/pom.xml b/pom.xml index 16c0fa2efae..08ce14ad0d3 100644 --- a/pom.xml +++ b/pom.xml @@ -104,6 +104,10 @@ Tomáš Neuberg neuberg@m-atelier.cz + + Willie Scholtz + williescholtz@gmail.com + diff --git a/src/main/java/org/apache/ibatis/builder/xml/XMLConfigBuilder.java b/src/main/java/org/apache/ibatis/builder/xml/XMLConfigBuilder.java index 7d53e55ac42..1a2bf839004 100644 --- a/src/main/java/org/apache/ibatis/builder/xml/XMLConfigBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/xml/XMLConfigBuilder.java @@ -292,6 +292,8 @@ private void settingsElement(Properties props) { booleanValueOf(props.getProperty("argNameBasedConstructorAutoMapping"), false)); configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType"))); configuration.setNullableOnForEach(booleanValueOf(props.getProperty("nullableOnForEach"), false)); + configuration.setExperimentalConstructorCollectionMapping( + booleanValueOf(props.getProperty("experimentalConstructorCollectionMapping"), false)); } private void environmentsElement(XNode context) throws Exception { 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 8cdc29d6377..9454c9f72bb 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java @@ -24,6 +24,7 @@ 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.List; @@ -73,6 +74,7 @@ * @author Eduardo Macarron * @author Iwao AVE! * @author Kazuki Shimizu + * @author Willie Scholtz */ public class DefaultResultSetHandler implements ResultSetHandler { @@ -363,7 +365,7 @@ private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap r skipRows(resultSet, rowBounds); while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null); - Object rowValue = getRowValue(rsw, discriminatedResultMap, null); + Object rowValue = getRowValue(rsw, discriminatedResultMap, null, null); storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); } } @@ -406,9 +408,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; @@ -437,7 +440,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 +655,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 +672,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 +695,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 +707,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 +718,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 +730,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, @@ -1008,29 +1035,59 @@ 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(); + boolean verifyPendingCreationResult = true; + 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, + verifyPendingCreationResult); + lastHandledCreation = null; + // we only need to verify the first the result for a given result set + // as we can assume the next result will look exactly the same w.r.t its mapping + verifyPendingCreationResult = false; + } + + 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, + verifyPendingCreationResult); + } else if (rowValue != null && mappedStatement.isResultOrdered() + && shouldProcessMoreRows(resultContext, rowBounds)) { storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); previousRowValue = null; } else if (rowValue != null) { @@ -1038,6 +1095,163 @@ 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 { + 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(nestedResultMap, 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; + } + + 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(nestedResultMap, (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 experimental nested result map values for '" + + constructorMapping.getProperty() + "'. Cause: " + e, e); + } + } + return foundValues; + } + + 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, boolean shouldVerify) + throws SQLException { + final Object result = pendingCreation.create(objectFactory, shouldVerify); + storeObject(resultHandler, resultContext, result, null, resultSet); + nestedResultObjects.clear(); + } + // // NESTED RESULT MAP (JOIN MAPPING) // @@ -1080,6 +1294,13 @@ private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap result } } } + + // (issue #101) + if (resultMap.hasResultMapsUsingConstructorCollection()) { + foundValues = applyNestedPendingConstructorCreations(rsw, resultMap, metaObject, parentPrefix, parentRowKey, + newObject, foundValues); + } + return foundValues; } 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..1859d7494ef --- /dev/null +++ b/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java @@ -0,0 +1,232 @@ +/* + * 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.lang.reflect.Constructor; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +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> linkedCollectionsByResultMapId; + private final Map> linkedCreationsByResultMapId; + + 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.linkedCollectionsByResultMapId = new HashMap<>(maxSize); + this.linkedCreationsByResultMapId = 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 ExecutorException( + "Cannot add a collection result to non-collection based resultMapping: " + constructorMapping); + } + + final String resultMapId = constructorMapping.getNestedResultMapId(); + return linkedCollectionsByResultMapId.computeIfAbsent(resultMapId, (k) -> { + // this will allow us to verify the types of the collection before creating the final object + linkedCollectionMetaInfo.put(index, new PendingCreationMetaInfo(resultMap.getType(), resultMapId)); + + // will be checked before we finally create the object) as we cannot reliably do that here + return (Collection) objectFactory.create(parameterType); + }); + } + + void linkCreation(ResultMap nestedResultMap, PendingConstructorCreation pcc) { + final String resultMapId = nestedResultMap.getId(); + final List pendingConstructorCreations = linkedCreationsByResultMapId + .computeIfAbsent(resultMapId, (k) -> new ArrayList<>()); + + if (pendingConstructorCreations.contains(pcc)) { + throw new ExecutorException("Cannot link inner pcc with same value, MyBatis programming error!"); + } + + pendingConstructorCreations.add(pcc); + } + + void linkCollectionValue(ResultMapping constructorMapping, Object value) { + // not necessary to add null results to the collection (is this a config flag?) + if (value == null) { + return; + } + + final String resultMapId = constructorMapping.getNestedResultMapId(); + if (!linkedCollectionsByResultMapId.containsKey(resultMapId)) { + throw new ExecutorException("Cannot link collection value for resultMapping: " + constructorMapping + + ", resultMap has not been seen/initialized yet! Internal error"); + } + + linkedCollectionsByResultMapId.get(resultMapId).add(value); + } + + /** + * Verifies preconditions before we can actually create the result object, this is more of a sanity check to ensure + * all the mappings are as we expect them to be. + * + * @param objectFactory + * the object factory + */ + private void verifyCanCreate(ObjectFactory objectFactory) { + // before we create, we need to get the constructor to be used and verify our types match + // since we added to the collection completely unchecked + final Constructor resolvedConstructor = resolveConstructor(resultType, constructorArgTypes); + final Type[] genericParameterTypes = resolvedConstructor.getGenericParameterTypes(); + for (int i = 0; i < genericParameterTypes.length; i++) { + if (!linkedCollectionMetaInfo.containsKey(i)) { + continue; + } + + final PendingCreationMetaInfo creationMetaInfo = linkedCollectionMetaInfo.get(i); + final Class resolvedItemType = checkResolvedItemType(creationMetaInfo, genericParameterTypes[i]); + + // ensure we have an empty collection if there are linked creations for this arg + final String resultMapId = creationMetaInfo.getResultMapId(); + if (linkedCreationsByResultMapId.containsKey(resultMapId)) { + final Object emptyCollection = constructorArgs.get(i); + if (emptyCollection == null || !objectFactory.isCollection(emptyCollection.getClass())) { + throw new ExecutorException( + "Expected empty collection for '" + resolvedItemType + "', this is a MyBatis internal error!"); + } + } else { + final Object linkedCollection = constructorArgs.get(i); + if (!linkedCollectionsByResultMapId.containsKey(resultMapId)) { + throw new ExecutorException("Expected linked collection for resultMap '" + resultMapId + + "', not found! this is a MyBatis internal error!"); + } + + // comparing memory locations here (we rely on that fact) + if (linkedCollection != linkedCollectionsByResultMapId.get(resultMapId)) { + throw new ExecutorException("Expected linked collection in creation to be the same as arg for resultMap '" + + resultMapId + "', not equal! this is a MyBatis internal error!"); + } + } + } + } + + private static Constructor resolveConstructor(Class type, List> constructorArgTypes) { + try { + if (constructorArgTypes == null) { + return type.getDeclaredConstructor(); + } + + return type.getDeclaredConstructor(constructorArgTypes.toArray(new Class[0])); + } catch (Exception e) { + String argTypes = Optional.ofNullable(constructorArgTypes).orElseGet(Collections::emptyList).stream() + .map(Class::getSimpleName).collect(Collectors.joining(",")); + throw new ReflectionException( + "Error resolving constructor for " + type + " with invalid types (" + argTypes + ") . Cause: " + e, e); + } + } + + private static Class checkResolvedItemType(PendingCreationMetaInfo creationMetaInfo, Type genericParameterTypes) { + final ParameterizedType genericParameterType = (ParameterizedType) genericParameterTypes; + final Class expectedType = (Class) genericParameterType.getActualTypeArguments()[0]; + final Class resolvedItemType = creationMetaInfo.getArgumentType(); + + if (!expectedType.isAssignableFrom(resolvedItemType)) { + throw new ReflectionException( + "Expected type '" + resolvedItemType + "', while the actual type of the collection was '" + expectedType + + "', ensure your resultMap matches the type of the collection you are trying to inject"); + } + + return resolvedItemType; + } + + @Override + public String toString() { + return "PendingConstructorCreation(" + this.hashCode() + "){" + "resultType=" + resultType + '}'; + } + + /** + * Recursively creates the final result of this creation. + * + * @param objectFactory + * the object factory + * @param verifyCreate + * should we verify this object can be created, should only be needed once + * + * @return the new immutable result + */ + Object create(ObjectFactory objectFactory, boolean verifyCreate) { + if (verifyCreate) { + verifyCanCreate(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 String resultMapId = creationMetaInfo.getResultMapId(); + if (linkedCreationsByResultMapId.containsKey(resultMapId)) { + @SuppressWarnings("unchecked") + final Collection emptyCollection = (Collection) existingArg; + final List linkedCreations = linkedCreationsByResultMapId.get(resultMapId); + + for (PendingConstructorCreation linkedCreation : linkedCreations) { + emptyCollection.add(linkedCreation.create(objectFactory, verifyCreate)); + } + + 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/PendingCreationMetaInfo.java b/src/main/java/org/apache/ibatis/executor/resultset/PendingCreationMetaInfo.java new file mode 100644 index 00000000000..b2abc43ca8e --- /dev/null +++ b/src/main/java/org/apache/ibatis/executor/resultset/PendingCreationMetaInfo.java @@ -0,0 +1,42 @@ +/* + * 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; + +/** + * @author Willie Scholtz + */ +final class PendingCreationMetaInfo { + private final Class argumentType; + private final String resultMapId; + + PendingCreationMetaInfo(Class argumentType, String resultMapId) { + this.argumentType = argumentType; + this.resultMapId = resultMapId; + } + + Class getArgumentType() { + return argumentType; + } + + String getResultMapId() { + return resultMapId; + } + + @Override + public String toString() { + return "PendingCreationMetaInfo{" + "argumentType=" + argumentType + ", resultMapId='" + resultMapId + '\'' + '}'; + } +} diff --git a/src/main/java/org/apache/ibatis/mapping/ResultMap.java b/src/main/java/org/apache/ibatis/mapping/ResultMap.java index a4f7d4ae57a..ac11e18df29 100644 --- a/src/main/java/org/apache/ibatis/mapping/ResultMap.java +++ b/src/main/java/org/apache/ibatis/mapping/ResultMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2023 the original author or authors. + * 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. @@ -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,15 @@ public ResultMap build() { } if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) { resultMap.constructorResultMappings.add(resultMapping); + + // #101 + if (resultMap.configuration.isExperimentalConstructorCollectionMappingEnabled()) { + 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 +220,10 @@ public String getId() { return id; } + public boolean hasResultMapsUsingConstructorCollection() { + return hasResultMapsUsingConstructorCollection; + } + public boolean hasNestedResultMaps() { return hasNestedResultMaps; } diff --git a/src/main/java/org/apache/ibatis/session/Configuration.java b/src/main/java/org/apache/ibatis/session/Configuration.java index dfc8dd0218c..df8a098689d 100644 --- a/src/main/java/org/apache/ibatis/session/Configuration.java +++ b/src/main/java/org/apache/ibatis/session/Configuration.java @@ -117,6 +117,7 @@ public class Configuration { protected boolean shrinkWhitespacesInSql; protected boolean nullableOnForEach; protected boolean argNameBasedConstructorAutoMapping; + protected boolean experimentalConstructorCollectionMapping; protected String logPrefix; protected Class logImpl; @@ -369,6 +370,14 @@ public boolean isSafeRowBoundsEnabled() { return safeRowBoundsEnabled; } + public void setExperimentalConstructorCollectionMapping(boolean experimentalConstructorCollectionMapping) { + this.experimentalConstructorCollectionMapping = experimentalConstructorCollectionMapping; + } + + public boolean isExperimentalConstructorCollectionMappingEnabled() { + return experimentalConstructorCollectionMapping; + } + public void setSafeRowBoundsEnabled(boolean safeRowBoundsEnabled) { this.safeRowBoundsEnabled = safeRowBoundsEnabled; } diff --git a/src/site/markdown/configuration.md b/src/site/markdown/configuration.md index 63fb4e65b7d..1cdd94a3c9e 100644 --- a/src/site/markdown/configuration.md +++ b/src/site/markdown/configuration.md @@ -103,40 +103,41 @@ This feature is disabled by default. If you specify a default value into placeho These are extremely important tweaks that modify the way that MyBatis behaves at runtime. The following table describes the settings, their meanings and their default values. -| Setting | Description | Valid Values | Default | -|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------| -| cacheEnabled | Globally enables or disables any caches configured in any mapper under this configuration. | true | false | true | -| lazyLoadingEnabled | Globally enables or disables lazy loading. When enabled, all relations will be lazily loaded. This value can be superseded for a specific relation by using the `fetchType` attribute on it. | true | false | false | -| aggressiveLazyLoading | When enabled, any method call will load all the lazy properties of the object. Otherwise, each property is loaded on demand (see also `lazyLoadTriggerMethods`). | true | false | false (true in ≤3.4.1) | -| ~~multipleResultSetsEnabled~~ | Deprecated. This option has no effect. | true | false | true | -| useColumnLabel | Uses the column label instead of the column name. Different drivers behave differently in this respect. Refer to the driver documentation, or test out both modes to determine how your driver behaves. | true | false | true | -| useGeneratedKeys | Allows JDBC support for generated keys. A compatible driver is required. This setting forces generated keys to be used if set to true, as some drivers deny compatibility but still work (e.g. Derby). | true | false | False | -| autoMappingBehavior | Specifies if and how MyBatis should automatically map columns to fields/properties. NONE disables auto-mapping. PARTIAL will only auto-map results with no nested result mappings defined inside. FULL will auto-map result mappings of any complexity (containing nested or otherwise). | NONE, PARTIAL, FULL | PARTIAL | -| autoMappingUnknownColumnBehavior | Specify the behavior when detects an unknown column (or unknown property type) of automatic mapping target.
  • `NONE`: Do nothing
  • `WARNING`: Output warning log (The log level of `'org.apache.ibatis.session.AutoMappingUnknownColumnBehavior'` must be set to `WARN`)
  • `FAILING`: Fail mapping (Throw `SqlSessionException`)
Note that there could be false-positives when `autoMappingBehavior` is set to `FULL`. | NONE, WARNING, FAILING | NONE | -| defaultExecutorType | Configures the default executor. SIMPLE executor does nothing special. REUSE executor reuses prepared statements. BATCH executor reuses statements and batches updates. | SIMPLE REUSE BATCH | SIMPLE | -| defaultStatementTimeout | Sets the number of seconds the driver will wait for a response from the database. | Any positive integer | Not Set (null) | -| defaultFetchSize | Sets the driver a hint as to control fetching size for return results. This parameter value can be override by a query setting. | Any positive integer | Not Set (null) | -| defaultResultSetType | Specifies a scroll strategy when omit it per statement settings. (Since: 3.5.2) | FORWARD_ONLY | SCROLL_SENSITIVE | SCROLL_INSENSITIVE | DEFAULT(same behavior with 'Not Set') | Not Set (null) | -| safeRowBoundsEnabled | Allows using RowBounds on nested statements. If allow, set the false. | true | false | False | -| safeResultHandlerEnabled | Allows using ResultHandler on nested statements. If allow, set the false. | true | false | True | -| mapUnderscoreToCamelCase | Enables automatic mapping from classic database column names A_COLUMN to camel case classic Java property names aColumn. | true | false | False | -| localCacheScope | MyBatis uses local cache to prevent circular references and speed up repeated nested queries. By default (SESSION) all queries executed during a session are cached. If localCacheScope=STATEMENT local session will be used just for statement execution, no data will be shared between two different calls to the same SqlSession. | SESSION | STATEMENT | SESSION | -| jdbcTypeForNull | Specifies the JDBC type for null values when no specific JDBC type was provided for the parameter. Some drivers require specifying the column JDBC type but others work with generic values like NULL, VARCHAR or OTHER. | JdbcType enumeration. Most common are: NULL, VARCHAR and OTHER | OTHER | -| lazyLoadTriggerMethods | Specifies which Object's methods trigger a lazy load | A method name list separated by commas | equals,clone,hashCode,toString | -| defaultScriptingLanguage | Specifies the language used by default for dynamic SQL generation. | A type alias or fully qualified class name. | org.apache.ibatis.scripting.xmltags.XMLLanguageDriver | -| defaultEnumTypeHandler | Specifies the `TypeHandler` used by default for Enum. (Since: 3.4.5) | A type alias or fully qualified class name. | org.apache.ibatis.type.EnumTypeHandler | -| callSettersOnNulls | Specifies if setters or map's put method will be called when a retrieved value is null. It is useful when you rely on Map.keySet() or null value initialization. Note primitives such as (int,boolean,etc.) will not be set to null. | true | false | false | -| returnInstanceForEmptyRow | MyBatis, by default, returns `null` when all the columns of a returned row are NULL. When this setting is enabled, MyBatis returns an empty instance instead. Note that it is also applied to nested results (i.e. collectioin and association). Since: 3.4.2 | true | false | false | -| logPrefix | Specifies the prefix string that MyBatis will add to the logger names. | Any String | Not set | -| logImpl | Specifies which logging implementation MyBatis should use. If this setting is not present logging implementation will be autodiscovered. | SLF4J | LOG4J(deprecated since 3.5.9) | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING | Not set | -| proxyFactory | Specifies the proxy tool that MyBatis will use for creating lazy loading capable objects. | CGLIB (deprecated since 3.5.10) | JAVASSIST | JAVASSIST (MyBatis 3.3 or above) | -| vfsImpl | Specifies VFS implementations | Fully qualified class names of custom VFS implementation separated by commas. | Not set | -| useActualParamName | Allow referencing statement parameters by their actual names declared in the method signature. To use this feature, your project must be compiled in Java 8 with `-parameters` option. (Since: 3.4.1) | true | false | true | -| configurationFactory | Specifies the class that provides an instance of `Configuration`. The returned Configuration instance is used to load lazy properties of deserialized objects. This class must have a method with a signature `static Configuration getConfiguration()`. (Since: 3.2.3) | A type alias or fully qualified class name. | Not set | -| shrinkWhitespacesInSql | Removes extra whitespace characters from the SQL. Note that this also affects literal strings in SQL. (Since 3.5.5) | true | false | false | -| defaultSqlProviderType | Specifies an sql provider class that holds provider method (Since 3.5.6). This class apply to the `type`(or `value`) attribute on sql provider annotation(e.g. `@SelectProvider`), when these attribute was omitted. | A type alias or fully qualified class name | Not set | -| nullableOnForEach | Specifies the default value of 'nullable' attribute on 'foreach' tag. (Since 3.5.9) | true | false | false | -| argNameBasedConstructorAutoMapping | When applying constructor auto-mapping, argument name is used to search the column to map instead of relying on the column order. (Since 3.5.10) | true | false | false | +| Setting | Description | Valid Values | Default | +|------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------| +| cacheEnabled | Globally enables or disables any caches configured in any mapper under this configuration. | true | false | true | +| lazyLoadingEnabled | Globally enables or disables lazy loading. When enabled, all relations will be lazily loaded. This value can be superseded for a specific relation by using the `fetchType` attribute on it. | true | false | false | +| aggressiveLazyLoading | When enabled, any method call will load all the lazy properties of the object. Otherwise, each property is loaded on demand (see also `lazyLoadTriggerMethods`). | true | false | false (true in ≤3.4.1) | +| ~~multipleResultSetsEnabled ~~ | Deprecated. This option has no effect. | true | false | true | +| useColumnLabel | Uses the column label instead of the column name. Different drivers behave differently in this respect. Refer to the driver documentation, or test out both modes to determine how your driver behaves. | true | false | true | +| useGeneratedKeys | Allows JDBC support for generated keys. A compatible driver is required. This setting forces generated keys to be used if set to true, as some drivers deny compatibility but still work (e.g. Derby). | true | false | False | +| autoMappingBehavior | Specifies if and how MyBatis should automatically map columns to fields/properties. NONE disables auto-mapping. PARTIAL will only auto-map results with no nested result mappings defined inside. FULL will auto-map result mappings of any complexity (containing nested or otherwise). | NONE, PARTIAL, FULL | PARTIAL | +| autoMappingUnknownColumnBehavior | Specify the behavior when detects an unknown column (or unknown property type) of automatic mapping target.
  • `NONE`: Do nothing
  • `WARNING`: Output warning log (The log level of `'org.apache.ibatis.session.AutoMappingUnknownColumnBehavior'` must be set to `WARN`)
  • `FAILING`: Fail mapping (Throw `SqlSessionException`)
Note that there could be false-positives when `autoMappingBehavior` is set to `FULL`. | NONE, WARNING, FAILING | NONE | +| defaultExecutorType | Configures the default executor. SIMPLE executor does nothing special. REUSE executor reuses prepared statements. BATCH executor reuses statements and batches updates. | SIMPLE REUSE BATCH | SIMPLE | +| defaultStatementTimeout | Sets the number of seconds the driver will wait for a response from the database. | Any positive integer | Not Set (null) | +| defaultFetchSize | Sets the driver a hint as to control fetching size for return results. This parameter value can be override by a query setting. | Any positive integer | Not Set (null) | +| defaultResultSetType | Specifies a scroll strategy when omit it per statement settings. (Since: 3.5.2) | FORWARD_ONLY | SCROLL_SENSITIVE | SCROLL_INSENSITIVE | DEFAULT(same behavior with 'Not Set') | Not Set (null) | +| safeRowBoundsEnabled | Allows using RowBounds on nested statements. If allow, set the false. | true | false | False | +| safeResultHandlerEnabled | Allows using ResultHandler on nested statements. If allow, set the false. | true | false | True | +| mapUnderscoreToCamelCase | Enables automatic mapping from classic database column names A_COLUMN to camel case classic Java property names aColumn. | true | false | False | +| localCacheScope | MyBatis uses local cache to prevent circular references and speed up repeated nested queries. By default (SESSION) all queries executed during a session are cached. If localCacheScope=STATEMENT local session will be used just for statement execution, no data will be shared between two different calls to the same SqlSession. | SESSION | STATEMENT | SESSION | +| jdbcTypeForNull | Specifies the JDBC type for null values when no specific JDBC type was provided for the parameter. Some drivers require specifying the column JDBC type but others work with generic values like NULL, VARCHAR or OTHER. | JdbcType enumeration. Most common are: NULL, VARCHAR and OTHER | OTHER | +| lazyLoadTriggerMethods | Specifies which Object's methods trigger a lazy load | A method name list separated by commas | equals,clone,hashCode,toString | +| defaultScriptingLanguage | Specifies the language used by default for dynamic SQL generation. | A type alias or fully qualified class name. | org.apache.ibatis.scripting.xmltags.XMLLanguageDriver | +| defaultEnumTypeHandler | Specifies the `TypeHandler` used by default for Enum. (Since: 3.4.5) | A type alias or fully qualified class name. | org.apache.ibatis.type.EnumTypeHandler | +| callSettersOnNulls | Specifies if setters or map's put method will be called when a retrieved value is null. It is useful when you rely on Map.keySet() or null value initialization. Note primitives such as (int,boolean,etc.) will not be set to null. | true | false | false | +| returnInstanceForEmptyRow | MyBatis, by default, returns `null` when all the columns of a returned row are NULL. When this setting is enabled, MyBatis returns an empty instance instead. Note that it is also applied to nested results (i.e. collectioin and association). Since: 3.4.2 | true | false | false | +| logPrefix | Specifies the prefix string that MyBatis will add to the logger names. | Any String | Not set | +| logImpl | Specifies which logging implementation MyBatis should use. If this setting is not present logging implementation will be autodiscovered. | SLF4J | LOG4J(deprecated since 3.5.9) | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING | Not set | +| proxyFactory | Specifies the proxy tool that MyBatis will use for creating lazy loading capable objects. | CGLIB (deprecated since 3.5.10) | JAVASSIST | JAVASSIST (MyBatis 3.3 or above) | +| vfsImpl | Specifies VFS implementations | Fully qualified class names of custom VFS implementation separated by commas. | Not set | +| useActualParamName | Allow referencing statement parameters by their actual names declared in the method signature. To use this feature, your project must be compiled in Java 8 with `-parameters` option. (Since: 3.4.1) | true | false | true | +| configurationFactory | Specifies the class that provides an instance of `Configuration`. The returned Configuration instance is used to load lazy properties of deserialized objects. This class must have a method with a signature `static Configuration getConfiguration()`. (Since: 3.2.3) | A type alias or fully qualified class name. | Not set | +| shrinkWhitespacesInSql | Removes extra whitespace characters from the SQL. Note that this also affects literal strings in SQL. (Since 3.5.5) | true | false | false | +| defaultSqlProviderType | Specifies an sql provider class that holds provider method (Since 3.5.6). This class apply to the `type`(or `value`) attribute on sql provider annotation(e.g. `@SelectProvider`), when these attribute was omitted. | A type alias or fully qualified class name | Not set | +| nullableOnForEach | Specifies the default value of 'nullable' attribute on 'foreach' tag. (Since 3.5.9) | true | false | false | +| argNameBasedConstructorAutoMapping | When applying constructor auto-mapping, argument name is used to search the column to map instead of relying on the column order. (Since 3.5.10) | true | false | false | +| experimentalConstructorCollectionMapping | When applying constructor mapping and any nested result mappings containing collections are found, they will now be filled (Since 3.6.0) | true | false | false | An example of the settings element fully configured is as follows: diff --git a/src/site/markdown/sqlmap-xml.md b/src/site/markdown/sqlmap-xml.md index ae784ecf254..d09a8ea926b 100644 --- a/src/site/markdown/sqlmap-xml.md +++ b/src/site/markdown/sqlmap-xml.md @@ -67,23 +67,23 @@ The select element has more attributes that allow you to configure the details o resultSetType="FORWARD_ONLY"> ``` -| Attribute | Description | -|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | A unique identifier in this namespace that can be used to reference this statement. | -| `parameterType` | The fully qualified class name or alias for the parameter that will be passed into this statement. This attribute is optional because MyBatis can calculate the TypeHandler to use out of the actual parameter passed to the statement. Default is `unset`. | -| `parameterMap` | This is a deprecated approach to referencing an external `parameterMap`. Use inline parameter mappings and the `parameterType` attribute. | -| `resultType` | The fully qualified class name or alias for the expected type that will be returned from this statement. Note that in the case of collections, this should be the type that the collection contains, not the type of the collection itself. Use `resultType` OR `resultMap`, not both. | -| `resultMap` | A named reference to an external `resultMap`. Result maps are the most powerful feature of MyBatis, and with a good understanding of them, many difficult mapping cases can be solved. Use `resultMap` OR `resultType`, not both. | -| `flushCache` | Setting this to true will cause the local and 2nd level caches to be flushed whenever this statement is called. Default: `false` for select statements. | -| `useCache` | Setting this to true will cause the results of this statement to be cached in 2nd level cache. Default: `true` for select statements. | -| `timeout` | This sets the number of seconds the driver will wait for the database to return from a request, before throwing an exception. Default is `unset` (driver dependent). | -| `fetchSize` | This is a driver hint that will attempt to cause the driver to return results in batches of rows numbering in size equal to this setting. Default is `unset` (driver dependent). | -| `statementType` | Any one of `STATEMENT`, `PREPARED` or `CALLABLE`. This causes MyBatis to use `Statement`, `PreparedStatement` or `CallableStatement` respectively. Default: `PREPARED`. | -| `resultSetType` | Any one of `FORWARD_ONLY`|`SCROLL_SENSITIVE`|`SCROLL_INSENSITIVE`|`DEFAULT`(same as unset). Default is `unset` (driver dependent). | -| `databaseId` | In case there is a configured databaseIdProvider, MyBatis will load all statements with no `databaseId` attribute or with a `databaseId` that matches the current one. If case the same statement if found with and without the `databaseId` the latter will be discarded. | -| `resultOrdered` | This is only applicable for nested result select statements: If this is true, it is assumed that nested results are contained or grouped together such that when a new main result row is returned, no references to a previous result row will occur anymore. This allows nested results to be filled much more memory friendly. Default: `false`. | -| `resultSets` | This is only applicable for multiple result sets. It lists the result sets that will be returned by the statement and gives a name to each one. Names are separated by commas. | -| `affectData` | Set this to true when writing a INSERT, UPDATE or DELETE statement that returns data so that the transaction is controlled properly. Also see [Transaction Control Method](./java-api.html#transaction-control-methods). Default: `false` (since 3.5.12) | +| Attribute | Description | +|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | A unique identifier in this namespace that can be used to reference this statement. | +| `parameterType` | The fully qualified class name or alias for the parameter that will be passed into this statement. This attribute is optional because MyBatis can calculate the TypeHandler to use out of the actual parameter passed to the statement. Default is `unset`. | +| `parameterMap` | This is a deprecated approach to referencing an external `parameterMap`. Use inline parameter mappings and the `parameterType` attribute. | +| `resultType` | The fully qualified class name or alias for the expected type that will be returned from this statement. Note that in the case of collections, this should be the type that the collection contains, not the type of the collection itself. Use `resultType` OR `resultMap`, not both. | +| `resultMap` | A named reference to an external `resultMap`. Result maps are the most powerful feature of MyBatis, and with a good understanding of them, many difficult mapping cases can be solved. Use `resultMap` OR `resultType`, not both. | +| `flushCache` | Setting this to true will cause the local and 2nd level caches to be flushed whenever this statement is called. Default: `false` for select statements. | +| `useCache` | Setting this to true will cause the results of this statement to be cached in 2nd level cache. Default: `true` for select statements. | +| `timeout` | This sets the number of seconds the driver will wait for the database to return from a request, before throwing an exception. Default is `unset` (driver dependent). | +| `fetchSize` | This is a driver hint that will attempt to cause the driver to return results in batches of rows numbering in size equal to this setting. Default is `unset` (driver dependent). | +| `statementType` | Any one of `STATEMENT`, `PREPARED` or `CALLABLE`. This causes MyBatis to use `Statement`, `PreparedStatement` or `CallableStatement` respectively. Default: `PREPARED`. | +| `resultSetType` | Any one of `FORWARD_ONLY`|`SCROLL_SENSITIVE`|`SCROLL_INSENSITIVE`|`DEFAULT`(same as unset). Default is `unset` (driver dependent). | +| `databaseId` | In case there is a configured databaseIdProvider, MyBatis will load all statements with no `databaseId` attribute or with a `databaseId` that matches the current one. If case the same statement if found with and without the `databaseId` the latter will be discarded. | +| `resultOrdered` | This is only applicable for nested result select statements: If this is true, it is assumed that nested results are contained or grouped together such that when a new main result row is returned, no references to a previous result row will occur anymore. This allows nested results to be filled much more memory friendly. Required for `experimentalConstructorCollectionMapping` Default: `false`. | +| `resultSets` | This is only applicable for multiple result sets. It lists the result sets that will be returned by the statement and gives a name to each one. Names are separated by commas. | +| `affectData` | Set this to true when writing a INSERT, UPDATE or DELETE statement that returns data so that the transaction is controlled properly. Also see [Transaction Control Method](./java-api.html#transaction-control-methods). Default: `false` (since 3.5.12) | [Select Attributes] ### insert, update and delete @@ -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,89 @@ 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. +This behaviour can be enabled by setting `experimentalConstructorCollectionMapping` to `true` in your configuration. + +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 + +``` + +In this case, the results are by ordered correctly by default. We can imagine the output to look somthing 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 | + +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. + +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]} +``` + +This functionality is still experimental, please report any issues you may find on the issue tracker. #### association @@ -701,7 +784,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/test/java/org/apache/ibatis/binding/BindingTest.java b/src/test/java/org/apache/ibatis/binding/BindingTest.java index 348ad18bb7f..85c23c48b1d 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-2023 the original author or authors. + * 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. @@ -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.assertThatThrownBy; 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 { @@ -80,6 +80,10 @@ static void setup() throws Exception { Configuration configuration = new Configuration(environment); configuration.setLazyLoadingEnabled(true); configuration.setUseActualParamName(false); // to test legacy style reference (#{0} #{1}) + + // # issue 101 (explicitly show the set to false to test backward compatibility for this issue) + configuration.setExperimentalConstructorCollectionMapping(false); // (false is the default) + configuration.getTypeAliasRegistry().registerAlias(Blog.class); configuration.getTypeAliasRegistry().registerAlias(Post.class); configuration.getTypeAliasRegistry().registerAlias(Author.class); @@ -399,17 +403,13 @@ void shouldExecuteBoundSelectBlogUsingConstructorWithResultMapAndProperties() { } } - @Disabled - @Test // issue #480 and #101 - void shouldExecuteBoundSelectBlogUsingConstructorWithResultMapCollection() { + @Test // issue #480 and #101 (negative case with flag disabled) + void shouldNotExecuteBoundSelectBlogUsingConstructorWithResultMapCollectionWhenFlagDisabled() { try (SqlSession session = sqlSessionFactory.openSession()) { BoundBlogMapper mapper = session.getMapper(BoundBlogMapper.class); - Blog blog = mapper.selectBlogUsingConstructorWithResultMapCollection(1); - assertEquals(1, blog.getId()); - 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"); + assertThatThrownBy(() -> mapper.selectBlogUsingConstructorWithResultMapCollection(1)) + .isInstanceOf(PersistenceException.class) + .hasMessageContaining("Error instantiating class org.apache.ibatis.domain.blog.Blog with invalid types"); } } diff --git a/src/test/java/org/apache/ibatis/binding/BoundBlogMapper.java b/src/test/java/org/apache/ibatis/binding/BoundBlogMapper.java index bc41486bbdf..d794c2cc676 100644 --- a/src/test/java/org/apache/ibatis/binding/BoundBlogMapper.java +++ b/src/test/java/org/apache/ibatis/binding/BoundBlogMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2023 the original author or authors. + * 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. diff --git a/src/test/java/org/apache/ibatis/binding/ConstructorCollectionBindingTest.java b/src/test/java/org/apache/ibatis/binding/ConstructorCollectionBindingTest.java new file mode 100644 index 00000000000..b82819b072a --- /dev/null +++ b/src/test/java/org/apache/ibatis/binding/ConstructorCollectionBindingTest.java @@ -0,0 +1,76 @@ +/* + * 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.binding; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import javax.sql.DataSource; + +import org.apache.ibatis.BaseDataTest; +import org.apache.ibatis.domain.blog.Author; +import org.apache.ibatis.domain.blog.Blog; +import org.apache.ibatis.domain.blog.Post; +import org.apache.ibatis.mapping.Environment; +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; + +class ConstructorCollectionBindingTest { + private static SqlSessionFactory sqlSessionFactory; + + @BeforeAll + static void setup() throws Exception { + DataSource dataSource = BaseDataTest.createBlogDataSource(); + BaseDataTest.runScript(dataSource, BaseDataTest.BLOG_DDL); + BaseDataTest.runScript(dataSource, BaseDataTest.BLOG_DATA); + TransactionFactory transactionFactory = new JdbcTransactionFactory(); + Environment environment = new Environment("Production", transactionFactory, dataSource); + Configuration configuration = new Configuration(environment); + configuration.setLazyLoadingEnabled(true); + configuration.setUseActualParamName(false); // to test legacy style reference (#{0} #{1}) + + configuration.setExperimentalConstructorCollectionMapping(true); // # issue 101 + + configuration.getTypeAliasRegistry().registerAlias(Blog.class); + configuration.getTypeAliasRegistry().registerAlias(Post.class); + configuration.getTypeAliasRegistry().registerAlias(Author.class); + configuration.addMapper(BoundBlogMapper.class); + configuration.addMapper(BoundAuthorMapper.class); + sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); + } + + @Test // issue #480 and #101 + void shouldExecuteBoundSelectBlogUsingConstructorWithResultMapCollection() { + try (SqlSession session = sqlSessionFactory.openSession()) { + BoundBlogMapper mapper = session.getMapper(BoundBlogMapper.class); + Blog blog = mapper.selectBlogUsingConstructorWithResultMapCollection(1); + assertEquals(1, blog.getId()); + assertEquals("Jim Business", blog.getTitle()); + assertNotNull(blog.getAuthor(), "author should not be null"); + List posts = blog.getPosts(); + 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 ae51d59fc98..00000000000 --- a/src/test/java/org/apache/ibatis/domain/blog/ImmutableAuthor.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2009-2023 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)) { - return false; - } - - Author author = (Author) o; - - 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; - } - if (username != null ? !username.equals(author.username) : author.username != null) { - return false; - } - if (favouriteSection != null ? !favouriteSection.equals(author.favouriteSection) - : author.favouriteSection != null) { - return false; - } - - return true; - } - - @Override - public int hashCode() { - int result; - 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 eaab2ec7536..96ba86c87ab 100644 --- a/src/test/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandlerTest.java +++ b/src/test/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandlerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2023 the original author or authors. + * 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. @@ -122,7 +122,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..1e9a030124f --- /dev/null +++ b/src/test/java/org/apache/ibatis/immutable/ImmutableConstructorTest.java @@ -0,0 +1,152 @@ +/* + * 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.setExperimentalConstructorCollectionMapping(true); // # issue 101 + 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 resolving constructor for 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 0b72e1d385e..b5a6f88fe64 100644 --- a/src/test/java/org/apache/ibatis/session/SqlSessionTest.java +++ b/src/test/java/org/apache/ibatis/session/SqlSessionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2023 the original author or authors. + * 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. @@ -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_injection/CollectionInjectionTest.java b/src/test/java/org/apache/ibatis/submitted/collection_injection/CollectionInjectionTest.java new file mode 100644 index 00000000000..c6079320884 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/CollectionInjectionTest.java @@ -0,0 +1,120 @@ +/* + * 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.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); + } +} 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..a9c85fc033b --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableHouseMapper.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.immutable; + +import java.util.List; + +public interface ImmutableHouseMapper { + + List getAllHouses(); + + ImmutableHouse getHouse(int it); + +} 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_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..dc8c52a7415 --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableHouseMapper.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + + + 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..60774338d9a --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/collection_injection/mybatis_config.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + + From 0497117cd8b2bb2bcfa1d7041ba25f17542843bd Mon Sep 17 00:00:00 2001 From: Iwao AVE! Date: Tue, 19 Nov 2024 02:43:59 +0900 Subject: [PATCH 02/14] Added tests covering basics with simpler objects --- .../collection_in_constructor/Clerk.java | 72 ++++++ .../CollectionInConstructorObjectFactory.java | 37 +++ .../CollectionInConstructorTest.java | 184 ++++++++++++++ .../collection_in_constructor/Container.java | 41 +++ .../CsvToListTypeHandler.java | 59 +++++ .../collection_in_constructor/Isle.java | 72 ++++++ .../collection_in_constructor/Mapper.java | 42 +++ .../collection_in_constructor/Store.java | 67 +++++ .../collection_in_constructor/Store2.java | 67 +++++ .../collection_in_constructor/Store3.java | 61 +++++ .../collection_in_constructor/Store4.java | 86 +++++++ .../collection_in_constructor/Store5.java | 68 +++++ .../collection_in_constructor/Store6.java | 70 +++++ .../collection_in_constructor/Store7.java | 68 +++++ .../collection_in_constructor/Store8.java | 67 +++++ .../collection_in_constructor/CreateDB.sql | 53 ++++ .../collection_in_constructor/Mapper.xml | 239 ++++++++++++++++++ .../mybatis-config.xml | 52 ++++ 18 files changed, 1405 insertions(+) create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Clerk.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/CollectionInConstructorObjectFactory.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/CollectionInConstructorTest.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Container.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/CsvToListTypeHandler.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Isle.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Mapper.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store2.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store3.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store4.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store5.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store6.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store7.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store8.java create mode 100644 src/test/resources/org/apache/ibatis/submitted/collection_in_constructor/CreateDB.sql create mode 100644 src/test/resources/org/apache/ibatis/submitted/collection_in_constructor/Mapper.xml create mode 100644 src/test/resources/org/apache/ibatis/submitted/collection_in_constructor/mybatis-config.xml 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..797e29b0bdd --- /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..4a9edb3f819 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/CollectionInConstructorTest.java @@ -0,0 +1,184 @@ +/* + * 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 isles = store.getIsles(); + Assertions.assertIterableEquals( + Arrays.asList(new Isle(101, "Isle 101"), new Isle(102, "Isle 102"), new Isle(103, "Isle 103")), isles); + } + } + + @Test + void testSimpleList() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + List stores = mapper.getStores(); + Assertions.assertIterableEquals( + Arrays.asList(new Isle(101, "Isle 101"), new Isle(102, "Isle 102"), new Isle(103, "Isle 103")), + stores.get(0).getIsles()); + Assertions.assertTrue(stores.get(1).getIsles().isEmpty()); + Assertions.assertIterableEquals(Arrays.asList(new Isle(104, "Isle 104"), new Isle(105, "Isle 105")), + stores.get(2).getIsles()); + } + } + + @Test + void shouldEmptyListBeReturned() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + Assertions.assertTrue(mapper.getAStore(2).getIsles().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 isles = store.getIsles(); + 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 Isle(101, "Isle 101"), new Isle(102, "Isle 102"), new Isle(103, "Isle 103")), isles); + } + } + + @Test + void testListOfStrings() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + Store3 store = mapper.getAStore3(1); + List isleNames = store.getIsleNames(); + Assertions.assertEquals(3, isleNames.size()); + Assertions.assertIterableEquals(Arrays.asList("Isle 101", "Isle 102", "Isle 103"), isleNames); + } + } + + @Test + void testObjectWithBuilder() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + Store4 store = mapper.getAStore4(1); + List isles = store.getIsles(); + Assertions.assertIterableEquals( + Arrays.asList(new Isle(101, "Isle 101"), new Isle(102, "Isle 102"), new Isle(103, "Isle 103")), isles); + } + } + + @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 isles = store.getIsles(); + Assertions.assertEquals("Store 1", store.getName()); + Assertions.assertEquals(3, isles.size()); + } + } + + @Test + void testTwoListsOfString() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + Store7 store = mapper.getAStore7(1); + List isleNames = store.getIsleNames(); + List clerkNames = store.getClerkNames(); + Assertions.assertIterableEquals(Arrays.asList("Isle 101", "Isle 102", "Isle 103"), isleNames); + 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 Isle(101, "Isle 101"), new Isle(102, "Isle 102"), + new Isle(103, "Isle 103"))), + new Store(2, "Store 2", Collections.emptyList()), + new Store(3, "Store 3", Arrays.asList(new Isle(104, "Isle 104"), new Isle(105, "Isle 105")))), + container.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/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/Isle.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Isle.java new file mode 100644 index 00000000000..6656893aa7c --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Isle.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 Isle { + + private Integer id; + private String name; + + public Isle() { + super(); + } + + public Isle(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 Isle)) { + return false; + } + Isle other = (Isle) obj; + return Objects.equals(id, other.id) && Objects.equals(name, other.name); + } + + @Override + public String toString() { + return "Isle [id=" + id + ", name=" + name + "]"; + } +} 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..54c8345ce47 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Mapper.java @@ -0,0 +1,42 @@ +/* + * 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(); + +} 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..1929d4c3b67 --- /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 isles; + + public Store(Integer id, String name, List isles) { + super(); + this.id = id; + this.name = name; + this.isles = isles; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public List getIsles() { + return isles; + } + + @Override + public int hashCode() { + return Objects.hash(id, isles, 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(isles, other.isles) && Objects.equals(name, other.name); + } + + @Override + public String toString() { + return "Store [id=" + id + ", name=" + name + ", isles=" + isles + "]"; + } +} 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..97306ae32b5 --- /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 isles; + + public Store2(Integer id, List clerks, List isles) { + super(); + this.id = id; + this.clerks = clerks; + this.isles = isles; + } + + public Integer getId() { + return id; + } + + public List getClerks() { + return clerks; + } + + public List getIsles() { + return isles; + } + + @Override + public int hashCode() { + return Objects.hash(clerks, id, isles); + } + + @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(isles, other.isles); + } + + @Override + public String toString() { + return "Store2 [id=" + id + ", clerks=" + clerks + ", isles=" + isles + "]"; + } +} 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..d28eec580b4 --- /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 isleNames; + + public Store3(Integer id, List isleNames) { + super(); + this.id = id; + this.isleNames = isleNames; + } + + public Integer getId() { + return id; + } + + public List getIsleNames() { + return isleNames; + } + + @Override + public int hashCode() { + return Objects.hash(id, isleNames); + } + + @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(isleNames, other.isleNames); + } + + @Override + public String toString() { + return "Store3 [id=" + id + ", isleNames=" + isleNames + "]"; + } +} 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..23eea2c06eb --- /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 isles; + + // Using different arg order than the definition + // to ensure the builder is used + Store4(List isles, Integer id) { + super(); + this.isles = isles; + this.id = id; + } + + public Integer getId() { + return id; + } + + public List getIsles() { + return isles; + } + + @Override + public int hashCode() { + return Objects.hash(id, isles); + } + + @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(isles, other.isles); + } + + @Override + public String toString() { + return "Store4 [id=" + id + ", isles=" + isles + "]"; + } + + 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..03768c01af4 --- /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 isles; + + public Store6(Integer id, List isles) { + super(); + this.id = id; + this.isles = isles; + } + + public Integer getId() { + return id; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public List getIsles() { + return isles; + } + + @Override + public int hashCode() { + return Objects.hash(id, isles, 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(isles, other.isles) && Objects.equals(name, other.name); + } + + @Override + public String toString() { + return "Store [id=" + id + ", name=" + name + ", isles=" + isles + "]"; + } +} 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..1451c3e874d --- /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 isleNames; + private final List clerkNames; + + public Store7(Integer id, List isleNames, List clerkNames) { + super(); + this.id = id; + this.isleNames = isleNames; + this.clerkNames = clerkNames; + } + + public Integer getId() { + return id; + } + + public List getIsleNames() { + return isleNames; + } + + public List getClerkNames() { + return clerkNames; + } + + @Override + public int hashCode() { + return Objects.hash(clerkNames, id, isleNames); + } + + @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(isleNames, other.isleNames); + } + + @Override + public String toString() { + return "Store7 [id=" + id + ", isleNames=" + isleNames + ", 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/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..741018f674f --- /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 isle if exists; +drop table clerk if exists; + +create table store ( + id int, + name varchar(20) +); + +create table isle ( + 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 isle (id, name, store_id) values(101, 'Isle 101', 1); +insert into isle (id, name, store_id) values(102, 'Isle 102', 1); +insert into isle (id, name, store_id) values(103, 'Isle 103', 1); +insert into isle (id, name, store_id) values(104, 'Isle 104', 3); +insert into isle (id, name, store_id) values(105, 'Isle 105', 3); + +insert into clerk (id, name, is_manager, store_id) values (1001, 'Cleark 1001', 0, 1); +insert into clerk (id, name, is_manager, store_id) values (1002, 'Cleark 1002', 1, 1); +insert into clerk (id, name, is_manager, store_id) values (1003, 'Cleark 1003', 0, 1); +insert into clerk (id, name, is_manager, store_id) values (1004, 'Cleark 1004', 0, 1); +insert into clerk (id, name, is_manager, store_id) values (1005, 'Cleark 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..5243e6a822e --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/collection_in_constructor/Mapper.xml @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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..dcb136eb11d --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/collection_in_constructor/mybatis-config.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 558c8a11a2926404290303618c16a707866d9573 Mon Sep 17 00:00:00 2001 From: Willie Scholtz Date: Tue, 19 Nov 2024 13:34:39 +0100 Subject: [PATCH 03/14] #101: Pass CollectionInConstructorTest - link simple objects - awareness of constructor mapping prefix for PendingConstructorCreation - don't verify creation when custom object factory is used --- .../resultset/DefaultResultSetHandler.java | 41 +++++++++-- .../resultset/PendingConstructorCreation.java | 72 +++++++++++-------- .../resultset/PendingCreationKey.java | 67 +++++++++++++++++ .../resultset/PendingCreationMetaInfo.java | 16 +++-- .../CollectionInConstructorTest.java | 12 ++++ .../collection_in_constructor/Store4.java | 2 +- .../collection_in_constructor/CreateDB.sql | 10 +-- 7 files changed, 174 insertions(+), 46 deletions(-) create mode 100644 src/main/java/org/apache/ibatis/executor/resultset/PendingCreationKey.java 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 9454c9f72bb..f7eed8e4159 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java @@ -360,13 +360,26 @@ protected void checkResultHandler() { private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { + final boolean useCollectionConstructorInjection = resultMap.hasResultMapsUsingConstructorCollection(); + PendingConstructorCreation lastHandledCreation = null; + 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, null); - storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); + 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, + lastHandledCreation == null); + lastHandledCreation = (PendingConstructorCreation) rowValue; + } } } @@ -422,6 +435,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; } @@ -1101,6 +1125,11 @@ && shouldProcessMoreRows(resultContext, rowBounds)) { 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); @@ -1157,7 +1186,7 @@ private void linkNestedPendingCreations(ResultSetWrapper rsw, ResultMap resultMa // we will fill this collection when building the final object constructorArgs.set(index, value); // link the creation for building later - pendingCreation.linkCreation(nestedResultMap, innerCreation); + pendingCreation.linkCreation(constructorMapping, innerCreation); } } } @@ -1190,6 +1219,9 @@ private boolean applyNestedPendingConstructorCreations(ResultSetWrapper rsw, Res 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; @@ -1213,7 +1245,7 @@ private boolean applyNestedPendingConstructorCreations(ResultSetWrapper rsw, Res if (rowValue instanceof PendingConstructorCreation) { if (newValueForNestedResultMap) { // we created a brand new pcc. this is a new collection value - pendingConstructorCreation.linkCreation(nestedResultMap, (PendingConstructorCreation) rowValue); + pendingConstructorCreation.linkCreation(constructorMapping, (PendingConstructorCreation) rowValue); foundValues = true; } } else { @@ -1225,10 +1257,11 @@ private boolean applyNestedPendingConstructorCreations(ResultSetWrapper rsw, Res } } } catch (SQLException e) { - throw new ExecutorException("Error getting experimental nested result map values for '" + throw new ExecutorException("Error getting constructor collection nested result map values for '" + constructorMapping.getProperty() + "'. Cause: " + e, e); } } + return foundValues; } diff --git a/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java b/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java index 1859d7494ef..1d32d391c3e 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java @@ -31,6 +31,7 @@ import org.apache.ibatis.mapping.ResultMap; import org.apache.ibatis.mapping.ResultMapping; import org.apache.ibatis.reflection.ReflectionException; +import org.apache.ibatis.reflection.factory.DefaultObjectFactory; import org.apache.ibatis.reflection.factory.ObjectFactory; /** @@ -43,16 +44,19 @@ final class PendingConstructorCreation { private final Class resultType; private final List> constructorArgTypes; private final List constructorArgs; + private final Map linkedCollectionMetaInfo; - private final Map> linkedCollectionsByResultMapId; - private final Map> linkedCreationsByResultMapId; + 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.linkedCollectionsByResultMapId = new HashMap<>(maxSize); - this.linkedCreationsByResultMapId = new HashMap<>(maxSize); + this.linkedCollectionsByKey = new HashMap<>(maxSize); + this.linkedCreationsByKey = new HashMap<>(maxSize); + this.resultType = resultType; this.constructorArgTypes = types; this.constructorArgs = args; @@ -63,55 +67,63 @@ Collection initializeCollectionForResultMapping(ObjectFactory objectFact ResultMapping constructorMapping, Integer index) { final Class parameterType = constructorMapping.getJavaType(); if (!objectFactory.isCollection(parameterType)) { - throw new ExecutorException( + throw new ReflectionException( "Cannot add a collection result to non-collection based resultMapping: " + constructorMapping); } - final String resultMapId = constructorMapping.getNestedResultMapId(); - return linkedCollectionsByResultMapId.computeIfAbsent(resultMapId, (k) -> { + final PendingCreationKey creationKey = new PendingCreationKey(constructorMapping); + return linkedCollectionsByKey.computeIfAbsent(creationKey, (k) -> { // this will allow us to verify the types of the collection before creating the final object - linkedCollectionMetaInfo.put(index, new PendingCreationMetaInfo(resultMap.getType(), resultMapId)); + linkedCollectionMetaInfo.put(index, new PendingCreationMetaInfo(resultMap.getType(), creationKey)); // will be checked before we finally create the object) as we cannot reliably do that here return (Collection) objectFactory.create(parameterType); }); } - void linkCreation(ResultMap nestedResultMap, PendingConstructorCreation pcc) { - final String resultMapId = nestedResultMap.getId(); - final List pendingConstructorCreations = linkedCreationsByResultMapId - .computeIfAbsent(resultMapId, (k) -> new ArrayList<>()); + 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 pcc with same value, MyBatis programming error!"); + 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 (is this a config flag?) + // not necessary to add null results to the collection if (value == null) { return; } - final String resultMapId = constructorMapping.getNestedResultMapId(); - if (!linkedCollectionsByResultMapId.containsKey(resultMapId)) { - throw new ExecutorException("Cannot link collection value for resultMapping: " + constructorMapping - + ", resultMap has not been seen/initialized yet! Internal error"); + final PendingCreationKey creationKey = new PendingCreationKey(constructorMapping); + if (!linkedCollectionsByKey.containsKey(creationKey)) { + throw new ExecutorException("Cannot link collection value for key: " + constructorMapping + + ", resultMap has not been seen/initialized yet! Mybatis internal error!"); } - linkedCollectionsByResultMapId.get(resultMapId).add(value); + linkedCollectionsByKey.get(creationKey).add(value); } /** * Verifies preconditions before we can actually create the result object, this is more of a sanity check to ensure * all the mappings are as we expect them to be. + *

+ * And if anything went wrong, provide the user with more information as to what went wrong * * @param objectFactory * the object factory */ private void verifyCanCreate(ObjectFactory objectFactory) { + // if a custom object factory was supplied, we cannot reasionably verify that creation will work + // thus, we disable verification and leave it up to the end user. + if (!DefaultObjectFactory.class.equals(objectFactory.getClass())) { + return; + } + // before we create, we need to get the constructor to be used and verify our types match // since we added to the collection completely unchecked final Constructor resolvedConstructor = resolveConstructor(resultType, constructorArgTypes); @@ -125,24 +137,24 @@ private void verifyCanCreate(ObjectFactory objectFactory) { final Class resolvedItemType = checkResolvedItemType(creationMetaInfo, genericParameterTypes[i]); // ensure we have an empty collection if there are linked creations for this arg - final String resultMapId = creationMetaInfo.getResultMapId(); - if (linkedCreationsByResultMapId.containsKey(resultMapId)) { + final PendingCreationKey pendingCreationKey = creationMetaInfo.getPendingCreationKey(); + if (linkedCreationsByKey.containsKey(pendingCreationKey)) { final Object emptyCollection = constructorArgs.get(i); if (emptyCollection == null || !objectFactory.isCollection(emptyCollection.getClass())) { throw new ExecutorException( - "Expected empty collection for '" + resolvedItemType + "', this is a MyBatis internal error!"); + "Expected empty collection for '" + resolvedItemType + "', MyBatis internal error!"); } } else { final Object linkedCollection = constructorArgs.get(i); - if (!linkedCollectionsByResultMapId.containsKey(resultMapId)) { - throw new ExecutorException("Expected linked collection for resultMap '" + resultMapId - + "', not found! this is a MyBatis internal error!"); + if (!linkedCollectionsByKey.containsKey(pendingCreationKey)) { + throw new ExecutorException( + "Expected linked collection for key '" + pendingCreationKey + "', not found! MyBatis internal error!"); } // comparing memory locations here (we rely on that fact) - if (linkedCollection != linkedCollectionsByResultMapId.get(resultMapId)) { + if (linkedCollection != linkedCollectionsByKey.get(pendingCreationKey)) { throw new ExecutorException("Expected linked collection in creation to be the same as arg for resultMap '" - + resultMapId + "', not equal! this is a MyBatis internal error!"); + + pendingCreationKey + "', not equal! MyBatis internal error!"); } } } @@ -209,11 +221,11 @@ Object create(ObjectFactory objectFactory, boolean verifyCreate) { } // time to finally build this collection - final String resultMapId = creationMetaInfo.getResultMapId(); - if (linkedCreationsByResultMapId.containsKey(resultMapId)) { + final PendingCreationKey pendingCreationKey = creationMetaInfo.getPendingCreationKey(); + if (linkedCreationsByKey.containsKey(pendingCreationKey)) { @SuppressWarnings("unchecked") final Collection emptyCollection = (Collection) existingArg; - final List linkedCreations = linkedCreationsByResultMapId.get(resultMapId); + final List linkedCreations = linkedCreationsByKey.get(pendingCreationKey); for (PendingConstructorCreation linkedCreation : linkedCreations) { emptyCollection.add(linkedCreation.create(objectFactory, verifyCreate)); 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 index b2abc43ca8e..a172f3ce152 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/PendingCreationMetaInfo.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/PendingCreationMetaInfo.java @@ -16,27 +16,31 @@ 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 String resultMapId; + private final PendingCreationKey pendingCreationKey; - PendingCreationMetaInfo(Class argumentType, String resultMapId) { + PendingCreationMetaInfo(Class argumentType, PendingCreationKey pendingCreationKey) { this.argumentType = argumentType; - this.resultMapId = resultMapId; + this.pendingCreationKey = pendingCreationKey; } Class getArgumentType() { return argumentType; } - String getResultMapId() { - return resultMapId; + PendingCreationKey getPendingCreationKey() { + return pendingCreationKey; } @Override public String toString() { - return "PendingCreationMetaInfo{" + "argumentType=" + argumentType + ", resultMapId='" + resultMapId + '\'' + '}'; + return "PendingCreationMetaInfo{" + "argumentType=" + argumentType + ", pendingCreationKey='" + pendingCreationKey + + '\'' + '}'; } } 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 index 4a9edb3f819..1f00f9a74a1 100644 --- 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 @@ -165,7 +165,19 @@ void testCollectionArgWithTypeHandler() { } @Test + @Disabled void testImmutableNestedObjects() { + /* + * This resultmap contains mixed property and constructor mappings, the logic assumes the entire chain will be + * immutable when we have mixed mappings, we don't know when to create the final object, as property mappings could + * still be modified at any point in time This brings us to a design question, is this really what we want from this + * functionality, as the point was to create immutable objects in my opinion, supporting this defeats the purpose; + * for example propery mapping -> immutable collection -> immutable object -> mapped by property mapping. we cannot + * build the final object if it can still be modified; i.e, the signal to build the immutable object is lost. the + * code in this pr assumes and relies on the base object also being immutable, i.e: constructor mapping -> immutable + * collection -> immutable object -> mapped by constructor mapping. Imo, there is only one option here, it should be + * added in the documentation; as doing (and supporting this, will be extremely complex) + */ try (SqlSession sqlSession = sqlSessionFactory.openSession()) { Mapper mapper = sqlSession.getMapper(Mapper.class); Container container = mapper.getAContainer(); 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 index 23eea2c06eb..03bd331b85d 100644 --- 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 @@ -24,7 +24,7 @@ public class Store4 { private final List isles; // Using different arg order than the definition - // to ensure the builder is used + // to ensure the builder is used, see CollectionInConstructorObjectFactory.create Store4(List isles, Integer id) { super(); this.isles = isles; 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 index 741018f674f..a67764472db 100644 --- 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 @@ -46,8 +46,8 @@ insert into isle (id, name, store_id) values(103, 'Isle 103', 1); insert into isle (id, name, store_id) values(104, 'Isle 104', 3); insert into isle (id, name, store_id) values(105, 'Isle 105', 3); -insert into clerk (id, name, is_manager, store_id) values (1001, 'Cleark 1001', 0, 1); -insert into clerk (id, name, is_manager, store_id) values (1002, 'Cleark 1002', 1, 1); -insert into clerk (id, name, is_manager, store_id) values (1003, 'Cleark 1003', 0, 1); -insert into clerk (id, name, is_manager, store_id) values (1004, 'Cleark 1004', 0, 1); -insert into clerk (id, name, is_manager, store_id) values (1005, 'Cleark 1005', 1, 1); +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); From a01b27ed5febbd9cd8c3b02ff7a41d5a70987f71 Mon Sep 17 00:00:00 2001 From: Willie Scholtz Date: Tue, 19 Nov 2024 13:47:48 +0100 Subject: [PATCH 04/14] #101: Remove experimental flag - We are sure that this path is not invoked by current versions of mybatis, and if it was, iut would have failed, it is thus safe to remove the flag --- .../ibatis/builder/xml/XMLConfigBuilder.java | 2 - .../org/apache/ibatis/mapping/ResultMap.java | 10 +-- .../apache/ibatis/session/Configuration.java | 9 --- src/site/markdown/configuration.md | 1 - src/site/markdown/sqlmap-xml.md | 38 +++++----- .../apache/ibatis/binding/BindingTest.java | 19 +++-- .../ConstructorCollectionBindingTest.java | 76 ------------------- .../immutable/ImmutableConstructorTest.java | 1 - .../mybatis-config.xml | 5 -- .../collection_injection/mybatis_config.xml | 3 - 10 files changed, 33 insertions(+), 131 deletions(-) delete mode 100644 src/test/java/org/apache/ibatis/binding/ConstructorCollectionBindingTest.java diff --git a/src/main/java/org/apache/ibatis/builder/xml/XMLConfigBuilder.java b/src/main/java/org/apache/ibatis/builder/xml/XMLConfigBuilder.java index 1a2bf839004..7d53e55ac42 100644 --- a/src/main/java/org/apache/ibatis/builder/xml/XMLConfigBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/xml/XMLConfigBuilder.java @@ -292,8 +292,6 @@ private void settingsElement(Properties props) { booleanValueOf(props.getProperty("argNameBasedConstructorAutoMapping"), false)); configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType"))); configuration.setNullableOnForEach(booleanValueOf(props.getProperty("nullableOnForEach"), false)); - configuration.setExperimentalConstructorCollectionMapping( - booleanValueOf(props.getProperty("experimentalConstructorCollectionMapping"), false)); } private void environmentsElement(XNode context) throws Exception { diff --git a/src/main/java/org/apache/ibatis/mapping/ResultMap.java b/src/main/java/org/apache/ibatis/mapping/ResultMap.java index ac11e18df29..58ce5dd6f6a 100644 --- a/src/main/java/org/apache/ibatis/mapping/ResultMap.java +++ b/src/main/java/org/apache/ibatis/mapping/ResultMap.java @@ -114,12 +114,10 @@ public ResultMap build() { resultMap.constructorResultMappings.add(resultMapping); // #101 - if (resultMap.configuration.isExperimentalConstructorCollectionMappingEnabled()) { - Class javaType = resultMapping.getJavaType(); - resultMap.hasResultMapsUsingConstructorCollection = resultMap.hasResultMapsUsingConstructorCollection - || (resultMapping.getNestedQueryId() == null && javaType != null - && resultMap.configuration.getObjectFactory().isCollection(javaType)); - } + 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()); diff --git a/src/main/java/org/apache/ibatis/session/Configuration.java b/src/main/java/org/apache/ibatis/session/Configuration.java index df8a098689d..dfc8dd0218c 100644 --- a/src/main/java/org/apache/ibatis/session/Configuration.java +++ b/src/main/java/org/apache/ibatis/session/Configuration.java @@ -117,7 +117,6 @@ public class Configuration { protected boolean shrinkWhitespacesInSql; protected boolean nullableOnForEach; protected boolean argNameBasedConstructorAutoMapping; - protected boolean experimentalConstructorCollectionMapping; protected String logPrefix; protected Class logImpl; @@ -370,14 +369,6 @@ public boolean isSafeRowBoundsEnabled() { return safeRowBoundsEnabled; } - public void setExperimentalConstructorCollectionMapping(boolean experimentalConstructorCollectionMapping) { - this.experimentalConstructorCollectionMapping = experimentalConstructorCollectionMapping; - } - - public boolean isExperimentalConstructorCollectionMappingEnabled() { - return experimentalConstructorCollectionMapping; - } - public void setSafeRowBoundsEnabled(boolean safeRowBoundsEnabled) { this.safeRowBoundsEnabled = safeRowBoundsEnabled; } diff --git a/src/site/markdown/configuration.md b/src/site/markdown/configuration.md index 1cdd94a3c9e..abf2f122d90 100644 --- a/src/site/markdown/configuration.md +++ b/src/site/markdown/configuration.md @@ -137,7 +137,6 @@ These are extremely important tweaks that modify the way that MyBatis behaves at | defaultSqlProviderType | Specifies an sql provider class that holds provider method (Since 3.5.6). This class apply to the `type`(or `value`) attribute on sql provider annotation(e.g. `@SelectProvider`), when these attribute was omitted. | A type alias or fully qualified class name | Not set | | nullableOnForEach | Specifies the default value of 'nullable' attribute on 'foreach' tag. (Since 3.5.9) | true | false | false | | argNameBasedConstructorAutoMapping | When applying constructor auto-mapping, argument name is used to search the column to map instead of relying on the column order. (Since 3.5.10) | true | false | false | -| experimentalConstructorCollectionMapping | When applying constructor mapping and any nested result mappings containing collections are found, they will now be filled (Since 3.6.0) | true | false | false | An example of the settings element fully configured is as follows: diff --git a/src/site/markdown/sqlmap-xml.md b/src/site/markdown/sqlmap-xml.md index d09a8ea926b..3089c5b9579 100644 --- a/src/site/markdown/sqlmap-xml.md +++ b/src/site/markdown/sqlmap-xml.md @@ -67,23 +67,23 @@ The select element has more attributes that allow you to configure the details o resultSetType="FORWARD_ONLY"> ``` -| Attribute | Description | -|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | A unique identifier in this namespace that can be used to reference this statement. | -| `parameterType` | The fully qualified class name or alias for the parameter that will be passed into this statement. This attribute is optional because MyBatis can calculate the TypeHandler to use out of the actual parameter passed to the statement. Default is `unset`. | -| `parameterMap` | This is a deprecated approach to referencing an external `parameterMap`. Use inline parameter mappings and the `parameterType` attribute. | -| `resultType` | The fully qualified class name or alias for the expected type that will be returned from this statement. Note that in the case of collections, this should be the type that the collection contains, not the type of the collection itself. Use `resultType` OR `resultMap`, not both. | -| `resultMap` | A named reference to an external `resultMap`. Result maps are the most powerful feature of MyBatis, and with a good understanding of them, many difficult mapping cases can be solved. Use `resultMap` OR `resultType`, not both. | -| `flushCache` | Setting this to true will cause the local and 2nd level caches to be flushed whenever this statement is called. Default: `false` for select statements. | -| `useCache` | Setting this to true will cause the results of this statement to be cached in 2nd level cache. Default: `true` for select statements. | -| `timeout` | This sets the number of seconds the driver will wait for the database to return from a request, before throwing an exception. Default is `unset` (driver dependent). | -| `fetchSize` | This is a driver hint that will attempt to cause the driver to return results in batches of rows numbering in size equal to this setting. Default is `unset` (driver dependent). | -| `statementType` | Any one of `STATEMENT`, `PREPARED` or `CALLABLE`. This causes MyBatis to use `Statement`, `PreparedStatement` or `CallableStatement` respectively. Default: `PREPARED`. | -| `resultSetType` | Any one of `FORWARD_ONLY`|`SCROLL_SENSITIVE`|`SCROLL_INSENSITIVE`|`DEFAULT`(same as unset). Default is `unset` (driver dependent). | -| `databaseId` | In case there is a configured databaseIdProvider, MyBatis will load all statements with no `databaseId` attribute or with a `databaseId` that matches the current one. If case the same statement if found with and without the `databaseId` the latter will be discarded. | -| `resultOrdered` | This is only applicable for nested result select statements: If this is true, it is assumed that nested results are contained or grouped together such that when a new main result row is returned, no references to a previous result row will occur anymore. This allows nested results to be filled much more memory friendly. Required for `experimentalConstructorCollectionMapping` Default: `false`. | -| `resultSets` | This is only applicable for multiple result sets. It lists the result sets that will be returned by the statement and gives a name to each one. Names are separated by commas. | -| `affectData` | Set this to true when writing a INSERT, UPDATE or DELETE statement that returns data so that the transaction is controlled properly. Also see [Transaction Control Method](./java-api.html#transaction-control-methods). Default: `false` (since 3.5.12) | +| Attribute | Description | +|---------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | A unique identifier in this namespace that can be used to reference this statement. | +| `parameterType` | The fully qualified class name or alias for the parameter that will be passed into this statement. This attribute is optional because MyBatis can calculate the TypeHandler to use out of the actual parameter passed to the statement. Default is `unset`. | +| `parameterMap` | This is a deprecated approach to referencing an external `parameterMap`. Use inline parameter mappings and the `parameterType` attribute. | +| `resultType` | The fully qualified class name or alias for the expected type that will be returned from this statement. Note that in the case of collections, this should be the type that the collection contains, not the type of the collection itself. Use `resultType` OR `resultMap`, not both. | +| `resultMap` | A named reference to an external `resultMap`. Result maps are the most powerful feature of MyBatis, and with a good understanding of them, many difficult mapping cases can be solved. Use `resultMap` OR `resultType`, not both. | +| `flushCache` | Setting this to true will cause the local and 2nd level caches to be flushed whenever this statement is called. Default: `false` for select statements. | +| `useCache` | Setting this to true will cause the results of this statement to be cached in 2nd level cache. Default: `true` for select statements. | +| `timeout` | This sets the number of seconds the driver will wait for the database to return from a request, before throwing an exception. Default is `unset` (driver dependent). | +| `fetchSize` | This is a driver hint that will attempt to cause the driver to return results in batches of rows numbering in size equal to this setting. Default is `unset` (driver dependent). | +| `statementType` | Any one of `STATEMENT`, `PREPARED` or `CALLABLE`. This causes MyBatis to use `Statement`, `PreparedStatement` or `CallableStatement` respectively. Default: `PREPARED`. | +| `resultSetType` | Any one of `FORWARD_ONLY`|`SCROLL_SENSITIVE`|`SCROLL_INSENSITIVE`|`DEFAULT`(same as unset). Default is `unset` (driver dependent). | +| `databaseId` | In case there is a configured databaseIdProvider, MyBatis will load all statements with no `databaseId` attribute or with a `databaseId` that matches the current one. If case the same statement if found with and without the `databaseId` the latter will be discarded. | +| `resultOrdered` | This is only applicable for nested result select statements: If this is true, it is assumed that nested results are contained or grouped together such that when a new main result row is returned, no references to a previous result row will occur anymore. This allows nested results to be filled much more memory friendly. Default: `false`. | +| `resultSets` | This is only applicable for multiple result sets. It lists the result sets that will be returned by the statement and gives a name to each one. Names are separated by commas. | +| `affectData` | Set this to true when writing a INSERT, UPDATE or DELETE statement that returns data so that the transaction is controlled properly. Also see [Transaction Control Method](./java-api.html#transaction-control-methods). Default: `false` (since 3.5.12) | [Select Attributes] ### insert, update and delete @@ -678,7 +678,6 @@ The rest of the attributes and rules are the same as for the regular id and resu ##### 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. -This behaviour can be enabled by setting `experimentalConstructorCollectionMapping` to `true` in your configuration. Considering the following: @@ -759,6 +758,9 @@ User{username=Peter, roles=[Users, Maintainers, Approvers]} This functionality is still experimental, please report any issues you may find on the issue tracker. +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 hieracrhy to use immutable constructor mappings. + #### association ```xml diff --git a/src/test/java/org/apache/ibatis/binding/BindingTest.java b/src/test/java/org/apache/ibatis/binding/BindingTest.java index 85c23c48b1d..9ac46ac4944 100644 --- a/src/test/java/org/apache/ibatis/binding/BindingTest.java +++ b/src/test/java/org/apache/ibatis/binding/BindingTest.java @@ -17,7 +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.assertThatThrownBy; +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; @@ -80,10 +80,6 @@ static void setup() throws Exception { Configuration configuration = new Configuration(environment); configuration.setLazyLoadingEnabled(true); configuration.setUseActualParamName(false); // to test legacy style reference (#{0} #{1}) - - // # issue 101 (explicitly show the set to false to test backward compatibility for this issue) - configuration.setExperimentalConstructorCollectionMapping(false); // (false is the default) - configuration.getTypeAliasRegistry().registerAlias(Blog.class); configuration.getTypeAliasRegistry().registerAlias(Post.class); configuration.getTypeAliasRegistry().registerAlias(Author.class); @@ -403,13 +399,16 @@ void shouldExecuteBoundSelectBlogUsingConstructorWithResultMapAndProperties() { } } - @Test // issue #480 and #101 (negative case with flag disabled) - void shouldNotExecuteBoundSelectBlogUsingConstructorWithResultMapCollectionWhenFlagDisabled() { + @Test // issue #480 and #101 + void shouldExecuteBoundSelectBlogUsingConstructorWithResultMapCollection() { try (SqlSession session = sqlSessionFactory.openSession()) { BoundBlogMapper mapper = session.getMapper(BoundBlogMapper.class); - assertThatThrownBy(() -> mapper.selectBlogUsingConstructorWithResultMapCollection(1)) - .isInstanceOf(PersistenceException.class) - .hasMessageContaining("Error instantiating class org.apache.ibatis.domain.blog.Blog with invalid types"); + Blog blog = mapper.selectBlogUsingConstructorWithResultMapCollection(1); + assertEquals(1, blog.getId()); + assertEquals("Jim Business", blog.getTitle()); + assertNotNull(blog.getAuthor(), "author should not be null"); + List posts = blog.getPosts(); + assertThat(posts).isNotNull().hasSize(2); } } diff --git a/src/test/java/org/apache/ibatis/binding/ConstructorCollectionBindingTest.java b/src/test/java/org/apache/ibatis/binding/ConstructorCollectionBindingTest.java deleted file mode 100644 index b82819b072a..00000000000 --- a/src/test/java/org/apache/ibatis/binding/ConstructorCollectionBindingTest.java +++ /dev/null @@ -1,76 +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.binding; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.util.List; - -import javax.sql.DataSource; - -import org.apache.ibatis.BaseDataTest; -import org.apache.ibatis.domain.blog.Author; -import org.apache.ibatis.domain.blog.Blog; -import org.apache.ibatis.domain.blog.Post; -import org.apache.ibatis.mapping.Environment; -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; - -class ConstructorCollectionBindingTest { - private static SqlSessionFactory sqlSessionFactory; - - @BeforeAll - static void setup() throws Exception { - DataSource dataSource = BaseDataTest.createBlogDataSource(); - BaseDataTest.runScript(dataSource, BaseDataTest.BLOG_DDL); - BaseDataTest.runScript(dataSource, BaseDataTest.BLOG_DATA); - TransactionFactory transactionFactory = new JdbcTransactionFactory(); - Environment environment = new Environment("Production", transactionFactory, dataSource); - Configuration configuration = new Configuration(environment); - configuration.setLazyLoadingEnabled(true); - configuration.setUseActualParamName(false); // to test legacy style reference (#{0} #{1}) - - configuration.setExperimentalConstructorCollectionMapping(true); // # issue 101 - - configuration.getTypeAliasRegistry().registerAlias(Blog.class); - configuration.getTypeAliasRegistry().registerAlias(Post.class); - configuration.getTypeAliasRegistry().registerAlias(Author.class); - configuration.addMapper(BoundBlogMapper.class); - configuration.addMapper(BoundAuthorMapper.class); - sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); - } - - @Test // issue #480 and #101 - void shouldExecuteBoundSelectBlogUsingConstructorWithResultMapCollection() { - try (SqlSession session = sqlSessionFactory.openSession()) { - BoundBlogMapper mapper = session.getMapper(BoundBlogMapper.class); - Blog blog = mapper.selectBlogUsingConstructorWithResultMapCollection(1); - assertEquals(1, blog.getId()); - assertEquals("Jim Business", blog.getTitle()); - assertNotNull(blog.getAuthor(), "author should not be null"); - List posts = blog.getPosts(); - assertThat(posts).isNotNull().hasSize(2); - } - } -} diff --git a/src/test/java/org/apache/ibatis/immutable/ImmutableConstructorTest.java b/src/test/java/org/apache/ibatis/immutable/ImmutableConstructorTest.java index 1e9a030124f..05f42bf4282 100644 --- a/src/test/java/org/apache/ibatis/immutable/ImmutableConstructorTest.java +++ b/src/test/java/org/apache/ibatis/immutable/ImmutableConstructorTest.java @@ -57,7 +57,6 @@ void setup() throws Exception { final Environment environment = new Environment("Production", transactionFactory, dataSource); final Configuration configuration = new Configuration(environment); - configuration.setExperimentalConstructorCollectionMapping(true); // # issue 101 configuration.addMapper(ImmutableBlogMapper.class); sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); 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 index dcb136eb11d..d58b4ba49e0 100644 --- 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 @@ -22,11 +22,6 @@ - - - - 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 index 60774338d9a..e6fb9dde9d5 100644 --- 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 @@ -21,9 +21,6 @@ "https://mybatis.org/dtd/mybatis-3-config.dtd"> - - - From e163a917cf1d816c67538e0c2ff0ed4de49210c3 Mon Sep 17 00:00:00 2001 From: Willie Scholtz Date: Wed, 20 Nov 2024 12:56:03 +0100 Subject: [PATCH 05/14] #101: Keep track of pending creations that were set as property mappings - We check for the existence of these (and build them if needed) before returning the final object - Fixes `testImmutableNestedObjects()` --- .../resultset/DefaultResultSetHandler.java | 53 ++++++++++++++++++- src/site/markdown/sqlmap-xml.md | 2 +- .../CollectionInConstructorTest.java | 12 ----- .../CollectionInjectionTest.java | 10 ++++ .../immutable/HousePortfolio.java | 25 +++++++++ .../immutable/ImmutableHouseMapper.java | 1 + .../immutable/ImmutableHouseMapper.xml | 14 ++++- .../collection_injection/mybatis_config.xml | 1 + 8 files changed, 102 insertions(+), 16 deletions(-) create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/HousePortfolio.java 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 f7eed8e4159..77cf9adb0d7 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java @@ -91,6 +91,9 @@ public class DefaultResultSetHandler implements ResultSetHandler { private final ObjectFactory objectFactory; private final ReflectorFactory reflectorFactory; + // pending creations property tracker + private final Map pendingPccRelations = new HashMap<>(); + // nested resultmaps private final Map nestedResultObjects = new HashMap<>(); private final Map ancestorObjects = new HashMap<>(); @@ -387,9 +390,14 @@ private void storeObject(ResultHandler resultHandler, DefaultResultContext is always ResultHandler */) @@ -1265,6 +1273,36 @@ private boolean applyNestedPendingConstructorCreations(ResultSetWrapper rsw, Res 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, false)); + } + } + } + } + private void verifyPendingCreationPreconditions(ResultMapping parentMapping) { if (parentMapping != null) { throw new ExecutorException( @@ -1481,6 +1519,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/site/markdown/sqlmap-xml.md b/src/site/markdown/sqlmap-xml.md index 3089c5b9579..e6abf781dd9 100644 --- a/src/site/markdown/sqlmap-xml.md +++ b/src/site/markdown/sqlmap-xml.md @@ -759,7 +759,7 @@ User{username=Peter, roles=[Users, Maintainers, Approvers]} This functionality is still experimental, please report any issues you may find on the issue tracker. 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 hieracrhy to use immutable constructor mappings. +When using this functionality, it is preferable for the entire mapping hierarchy to use immutable constructor mappings. #### association 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 index 1f00f9a74a1..4a9edb3f819 100644 --- 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 @@ -165,19 +165,7 @@ void testCollectionArgWithTypeHandler() { } @Test - @Disabled void testImmutableNestedObjects() { - /* - * This resultmap contains mixed property and constructor mappings, the logic assumes the entire chain will be - * immutable when we have mixed mappings, we don't know when to create the final object, as property mappings could - * still be modified at any point in time This brings us to a design question, is this really what we want from this - * functionality, as the point was to create immutable objects in my opinion, supporting this defeats the purpose; - * for example propery mapping -> immutable collection -> immutable object -> mapped by property mapping. we cannot - * build the final object if it can still be modified; i.e, the signal to build the immutable object is lost. the - * code in this pr assumes and relies on the base object also being immutable, i.e: constructor mapping -> immutable - * collection -> immutable object -> mapped by constructor mapping. Imo, there is only one option here, it should be - * added in the documentation; as doing (and supporting this, will be extremely complex) - */ try (SqlSession sqlSession = sqlSessionFactory.openSession()) { Mapper mapper = sqlSession.getMapper(Mapper.class); Container container = mapper.getAContainer(); 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 index c6079320884..2fd006805c3 100644 --- a/src/test/java/org/apache/ibatis/submitted/collection_injection/CollectionInjectionTest.java +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/CollectionInjectionTest.java @@ -24,6 +24,7 @@ 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; @@ -117,4 +118,13 @@ private static void assertResult(StringBuilder builder) { 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..287e4403b02 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/HousePortfolio.java @@ -0,0 +1,25 @@ +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/ImmutableHouseMapper.java b/src/test/java/org/apache/ibatis/submitted/collection_injection/immutable/ImmutableHouseMapper.java index a9c85fc033b..5a97da27ae9 100644 --- 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 @@ -23,4 +23,5 @@ public interface ImmutableHouseMapper { ImmutableHouse getHouse(int it); + HousePortfolio getHousePortfolio(int id); } 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 index dc8c52a7415..f504a62fb03 100644 --- 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 @@ -63,7 +63,11 @@ - select h.* + select + + 1 as portfolioId + + , h.* , r.id as room_id , r.name as room_name @@ -92,4 +96,12 @@ where h.id = #{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 index e6fb9dde9d5..735a80163e2 100644 --- 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 @@ -34,6 +34,7 @@ + From bc53e844abe2436b2148f4dfe742111b3021c258 Mon Sep 17 00:00:00 2001 From: Willie Scholtz Date: Thu, 21 Nov 2024 11:56:45 +0100 Subject: [PATCH 06/14] #101: Add test which violates equals contract - Use identity hashmap to check relations for creations --- .../resultset/DefaultResultSetHandler.java | 3 +- .../CollectionInConstructorTest.java | 36 ++++++++++ .../collection_in_constructor/Container1.java | 67 +++++++++++++++++++ .../collection_in_constructor/Mapper.java | 2 + .../collection_in_constructor/Store9.java | 66 ++++++++++++++++++ .../collection_in_constructor/Mapper.xml | 43 +++++++++++- 6 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Container1.java create mode 100644 src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Store9.java 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 77cf9adb0d7..1bf0f5411b0 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java @@ -27,6 +27,7 @@ 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; @@ -92,7 +93,7 @@ public class DefaultResultSetHandler implements ResultSetHandler { private final ReflectorFactory reflectorFactory; // pending creations property tracker - private final Map pendingPccRelations = new HashMap<>(); + private final Map pendingPccRelations = new IdentityHashMap<>(); // nested resultmaps private final Map nestedResultObjects = new HashMap<>(); 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 index 4a9edb3f819..a1ba219b089 100644 --- 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 @@ -181,4 +181,40 @@ void testImmutableNestedObjects() { 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/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/Mapper.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Mapper.java index 54c8345ce47..0aa4db76a2e 100644 --- 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 @@ -39,4 +39,6 @@ public interface Mapper { Container getAContainer(); + List getContainers(); + } 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/resources/org/apache/ibatis/submitted/collection_in_constructor/Mapper.xml b/src/test/resources/org/apache/ibatis/submitted/collection_in_constructor/Mapper.xml index 5243e6a822e..7832b853e65 100644 --- 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 @@ -236,4 +236,45 @@ order by s.id, i.id ]]> - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + From 685bafb83293f0c8dc6198ed82ee2a1441d39c29 Mon Sep 17 00:00:00 2001 From: Iwao AVE! Date: Thu, 26 Dec 2024 18:56:25 +0900 Subject: [PATCH 07/14] =?UTF-8?q?isle=20->=20aisle=20=F0=9F=98=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{Isle.java => Aisle.java} | 12 ++--- .../CollectionInConstructorObjectFactory.java | 2 +- .../CollectionInConstructorTest.java | 44 ++++++++--------- .../collection_in_constructor/Store.java | 16 +++---- .../collection_in_constructor/Store2.java | 16 +++---- .../collection_in_constructor/Store3.java | 16 +++---- .../collection_in_constructor/Store4.java | 20 ++++---- .../collection_in_constructor/Store6.java | 16 +++---- .../collection_in_constructor/Store7.java | 16 +++---- .../collection_in_constructor/CreateDB.sql | 14 +++--- .../collection_in_constructor/Mapper.xml | 48 +++++++++---------- 11 files changed, 110 insertions(+), 110 deletions(-) rename src/test/java/org/apache/ibatis/submitted/collection_in_constructor/{Isle.java => Aisle.java} (87%) diff --git a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Isle.java b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Aisle.java similarity index 87% rename from src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Isle.java rename to src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Aisle.java index 6656893aa7c..8c23e7f8c30 100644 --- a/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Isle.java +++ b/src/test/java/org/apache/ibatis/submitted/collection_in_constructor/Aisle.java @@ -17,16 +17,16 @@ import java.util.Objects; -public class Isle { +public class Aisle { private Integer id; private String name; - public Isle() { + public Aisle() { super(); } - public Isle(Integer id, String name) { + public Aisle(Integer id, String name) { super(); this.id = id; this.name = name; @@ -58,15 +58,15 @@ public boolean equals(Object obj) { if (this == obj) { return true; } - if (!(obj instanceof Isle)) { + if (!(obj instanceof Aisle)) { return false; } - Isle other = (Isle) obj; + Aisle other = (Aisle) obj; return Objects.equals(id, other.id) && Objects.equals(name, other.name); } @Override public String toString() { - return "Isle [id=" + id + ", name=" + name + "]"; + return "Aisle [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 index 797e29b0bdd..befbd75bc46 100644 --- 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 @@ -28,7 +28,7 @@ public class CollectionInConstructorObjectFactory extends DefaultObjectFactory { @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)) + 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 index a1ba219b089..c5e916cd8ed 100644 --- 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 @@ -52,9 +52,9 @@ void testSimple() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { Mapper mapper = sqlSession.getMapper(Mapper.class); Store store = mapper.getAStore(1); - List isles = store.getIsles(); + List aisles = store.getAisles(); Assertions.assertIterableEquals( - Arrays.asList(new Isle(101, "Isle 101"), new Isle(102, "Isle 102"), new Isle(103, "Isle 103")), isles); + Arrays.asList(new Aisle(101, "Aisle 101"), new Aisle(102, "Aisle 102"), new Aisle(103, "Aisle 103")), aisles); } } @@ -64,11 +64,11 @@ void testSimpleList() { Mapper mapper = sqlSession.getMapper(Mapper.class); List stores = mapper.getStores(); Assertions.assertIterableEquals( - Arrays.asList(new Isle(101, "Isle 101"), new Isle(102, "Isle 102"), new Isle(103, "Isle 103")), - stores.get(0).getIsles()); - Assertions.assertTrue(stores.get(1).getIsles().isEmpty()); - Assertions.assertIterableEquals(Arrays.asList(new Isle(104, "Isle 104"), new Isle(105, "Isle 105")), - stores.get(2).getIsles()); + 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()); } } @@ -76,7 +76,7 @@ void testSimpleList() { void shouldEmptyListBeReturned() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { Mapper mapper = sqlSession.getMapper(Mapper.class); - Assertions.assertTrue(mapper.getAStore(2).getIsles().isEmpty()); + Assertions.assertTrue(mapper.getAStore(2).getAisles().isEmpty()); } } @@ -86,11 +86,11 @@ void testTwoLists() { Mapper mapper = sqlSession.getMapper(Mapper.class); Store2 store = mapper.getAStore2(1); List clerks = store.getClerks(); - List isles = store.getIsles(); + 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 Isle(101, "Isle 101"), new Isle(102, "Isle 102"), new Isle(103, "Isle 103")), isles); + Arrays.asList(new Aisle(101, "Aisle 101"), new Aisle(102, "Aisle 102"), new Aisle(103, "Aisle 103")), aisles); } } @@ -99,9 +99,9 @@ void testListOfStrings() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { Mapper mapper = sqlSession.getMapper(Mapper.class); Store3 store = mapper.getAStore3(1); - List isleNames = store.getIsleNames(); - Assertions.assertEquals(3, isleNames.size()); - Assertions.assertIterableEquals(Arrays.asList("Isle 101", "Isle 102", "Isle 103"), isleNames); + List aisleNames = store.getAisleNames(); + Assertions.assertEquals(3, aisleNames.size()); + Assertions.assertIterableEquals(Arrays.asList("Aisle 101", "Aisle 102", "Aisle 103"), aisleNames); } } @@ -110,9 +110,9 @@ void testObjectWithBuilder() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { Mapper mapper = sqlSession.getMapper(Mapper.class); Store4 store = mapper.getAStore4(1); - List isles = store.getIsles(); + List aisles = store.getAisles(); Assertions.assertIterableEquals( - Arrays.asList(new Isle(101, "Isle 101"), new Isle(102, "Isle 102"), new Isle(103, "Isle 103")), isles); + Arrays.asList(new Aisle(101, "Aisle 101"), new Aisle(102, "Aisle 102"), new Aisle(103, "Aisle 103")), aisles); } } @@ -136,9 +136,9 @@ void testPartiallyImmutableObject() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { Mapper mapper = sqlSession.getMapper(Mapper.class); Store6 store = mapper.getAStore6(1); - List isles = store.getIsles(); + List aisles = store.getAisles(); Assertions.assertEquals("Store 1", store.getName()); - Assertions.assertEquals(3, isles.size()); + Assertions.assertEquals(3, aisles.size()); } } @@ -147,9 +147,9 @@ void testTwoListsOfString() { try (SqlSession sqlSession = sqlSessionFactory.openSession()) { Mapper mapper = sqlSession.getMapper(Mapper.class); Store7 store = mapper.getAStore7(1); - List isleNames = store.getIsleNames(); + List aisleNames = store.getAisleNames(); List clerkNames = store.getClerkNames(); - Assertions.assertIterableEquals(Arrays.asList("Isle 101", "Isle 102", "Isle 103"), isleNames); + 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); } @@ -174,10 +174,10 @@ void testImmutableNestedObjects() { Arrays .asList( new Store(1, "Store 1", - Arrays.asList(new Isle(101, "Isle 101"), new Isle(102, "Isle 102"), - new Isle(103, "Isle 103"))), + 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 Isle(104, "Isle 104"), new Isle(105, "Isle 105")))), + new Store(3, "Store 3", Arrays.asList(new Aisle(104, "Aisle 104"), new Aisle(105, "Aisle 105")))), container.getStores()); } } 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 index 1929d4c3b67..0f8dd7a434f 100644 --- 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 @@ -22,13 +22,13 @@ public class Store { private final Integer id; private final String name; - private final List isles; + private final List aisles; - public Store(Integer id, String name, List isles) { + public Store(Integer id, String name, List aisles) { super(); this.id = id; this.name = name; - this.isles = isles; + this.aisles = aisles; } public Integer getId() { @@ -39,13 +39,13 @@ public String getName() { return name; } - public List getIsles() { - return isles; + public List getAisles() { + return aisles; } @Override public int hashCode() { - return Objects.hash(id, isles, name); + return Objects.hash(id, aisles, name); } @Override @@ -57,11 +57,11 @@ public boolean equals(Object obj) { return false; } Store other = (Store) obj; - return Objects.equals(id, other.id) && Objects.equals(isles, other.isles) && Objects.equals(name, other.name); + 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 + ", isles=" + isles + "]"; + 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 index 97306ae32b5..ca2b782136d 100644 --- 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 @@ -22,13 +22,13 @@ public class Store2 { private final Integer id; private final List clerks; - private final List isles; + private final List aisles; - public Store2(Integer id, List clerks, List isles) { + public Store2(Integer id, List clerks, List aisles) { super(); this.id = id; this.clerks = clerks; - this.isles = isles; + this.aisles = aisles; } public Integer getId() { @@ -39,13 +39,13 @@ public List getClerks() { return clerks; } - public List getIsles() { - return isles; + public List getAisles() { + return aisles; } @Override public int hashCode() { - return Objects.hash(clerks, id, isles); + return Objects.hash(clerks, id, aisles); } @Override @@ -57,11 +57,11 @@ public boolean equals(Object obj) { return false; } Store2 other = (Store2) obj; - return Objects.equals(clerks, other.clerks) && Objects.equals(id, other.id) && Objects.equals(isles, other.isles); + 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 + ", isles=" + isles + "]"; + 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 index d28eec580b4..7866f618797 100644 --- 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 @@ -21,25 +21,25 @@ public class Store3 { private final Integer id; - private final List isleNames; + private final List aisleNames; - public Store3(Integer id, List isleNames) { + public Store3(Integer id, List aisleNames) { super(); this.id = id; - this.isleNames = isleNames; + this.aisleNames = aisleNames; } public Integer getId() { return id; } - public List getIsleNames() { - return isleNames; + public List getAisleNames() { + return aisleNames; } @Override public int hashCode() { - return Objects.hash(id, isleNames); + return Objects.hash(id, aisleNames); } @Override @@ -51,11 +51,11 @@ public boolean equals(Object obj) { return false; } Store3 other = (Store3) obj; - return Objects.equals(id, other.id) && Objects.equals(isleNames, other.isleNames); + return Objects.equals(id, other.id) && Objects.equals(aisleNames, other.aisleNames); } @Override public String toString() { - return "Store3 [id=" + id + ", isleNames=" + isleNames + "]"; + 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 index 03bd331b85d..3857ff8268c 100644 --- 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 @@ -21,13 +21,13 @@ public class Store4 { private final Integer id; - private final List isles; + private final List aisles; // Using different arg order than the definition // to ensure the builder is used, see CollectionInConstructorObjectFactory.create - Store4(List isles, Integer id) { + Store4(List aisles, Integer id) { super(); - this.isles = isles; + this.aisles = aisles; this.id = id; } @@ -35,13 +35,13 @@ public Integer getId() { return id; } - public List getIsles() { - return isles; + public List getAisles() { + return aisles; } @Override public int hashCode() { - return Objects.hash(id, isles); + return Objects.hash(id, aisles); } @Override @@ -53,12 +53,12 @@ public boolean equals(Object obj) { return false; } Store4 other = (Store4) obj; - return Objects.equals(id, other.id) && Objects.equals(isles, other.isles); + return Objects.equals(id, other.id) && Objects.equals(aisles, other.aisles); } @Override public String toString() { - return "Store4 [id=" + id + ", isles=" + isles + "]"; + return "Store4 [id=" + id + ", aisles=" + aisles + "]"; } public static Store4Builder builder() { @@ -67,14 +67,14 @@ public static Store4Builder builder() { public static class Store4Builder { private Integer id; - private List isles; + private List isles; public Store4Builder id(Integer id) { this.id = id; return this; } - public Store4Builder isles(List isles) { + public Store4Builder isles(List isles) { this.isles = isles; return this; } 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 index 03768c01af4..82add491225 100644 --- 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 @@ -22,12 +22,12 @@ public class Store6 { private final Integer id; private String name; - private final List isles; + private final List aisles; - public Store6(Integer id, List isles) { + public Store6(Integer id, List aisles) { super(); this.id = id; - this.isles = isles; + this.aisles = aisles; } public Integer getId() { @@ -42,13 +42,13 @@ public String getName() { return name; } - public List getIsles() { - return isles; + public List getAisles() { + return aisles; } @Override public int hashCode() { - return Objects.hash(id, isles, name); + return Objects.hash(id, aisles, name); } @Override @@ -60,11 +60,11 @@ public boolean equals(Object obj) { return false; } Store6 other = (Store6) obj; - return Objects.equals(id, other.id) && Objects.equals(isles, other.isles) && Objects.equals(name, other.name); + 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 + ", isles=" + isles + "]"; + 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 index 1451c3e874d..6df3c459d33 100644 --- 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 @@ -21,13 +21,13 @@ public class Store7 { private final Integer id; - private final List isleNames; + private final List aisleNames; private final List clerkNames; - public Store7(Integer id, List isleNames, List clerkNames) { + public Store7(Integer id, List aisleNames, List clerkNames) { super(); this.id = id; - this.isleNames = isleNames; + this.aisleNames = aisleNames; this.clerkNames = clerkNames; } @@ -35,8 +35,8 @@ public Integer getId() { return id; } - public List getIsleNames() { - return isleNames; + public List getAisleNames() { + return aisleNames; } public List getClerkNames() { @@ -45,7 +45,7 @@ public List getClerkNames() { @Override public int hashCode() { - return Objects.hash(clerkNames, id, isleNames); + return Objects.hash(clerkNames, id, aisleNames); } @Override @@ -58,11 +58,11 @@ public boolean equals(Object obj) { } Store7 other = (Store7) obj; return Objects.equals(clerkNames, other.clerkNames) && Objects.equals(id, other.id) - && Objects.equals(isleNames, other.isleNames); + && Objects.equals(aisleNames, other.aisleNames); } @Override public String toString() { - return "Store7 [id=" + id + ", isleNames=" + isleNames + ", clerkNames=" + clerkNames + "]"; + return "Store7 [id=" + id + ", aisleNames=" + aisleNames + ", clerkNames=" + clerkNames + "]"; } } 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 index a67764472db..aff22949d4b 100644 --- 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 @@ -15,7 +15,7 @@ -- drop table store if exists; -drop table isle if exists; +drop table aisle if exists; drop table clerk if exists; create table store ( @@ -23,7 +23,7 @@ create table store ( name varchar(20) ); -create table isle ( +create table aisle ( id int, name varchar(20), store_id int @@ -40,11 +40,11 @@ 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 isle (id, name, store_id) values(101, 'Isle 101', 1); -insert into isle (id, name, store_id) values(102, 'Isle 102', 1); -insert into isle (id, name, store_id) values(103, 'Isle 103', 1); -insert into isle (id, name, store_id) values(104, 'Isle 104', 3); -insert into isle (id, name, store_id) values(105, 'Isle 105', 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); 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 index 7832b853e65..47aa2887c88 100644 --- 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 @@ -26,13 +26,13 @@ - + + type="org.apache.ibatis.submitted.collection_in_constructor.Aisle" + id="aisleRM"> @@ -47,9 +47,9 @@ @@ -57,9 +57,9 @@ @@ -69,7 +69,7 @@ - + @@ -78,10 +78,10 @@ select s.id, c.id clerk_id, c.name clerk_name, - i.id isle_id, i.name isle_name + i.id aisle_id, i.name aisle_name from store s left join clerk c on c.store_id = s.id - left join isle i on i.store_id = s.id + left join aisle i on i.store_id = s.id where s.id = #{id} order by s.id, c.id, i.id ]]> @@ -91,7 +91,7 @@ id="store3RM"> - + @@ -103,9 +103,9 @@ resultOrdered="true"> @@ -115,7 +115,7 @@ id="store4RM"> - + @@ -123,9 +123,9 @@ resultOrdered="true"> @@ -159,7 +159,7 @@ id="store6RM"> - + @@ -168,9 +168,9 @@ resultOrdered="true"> @@ -180,7 +180,7 @@ id="store7RM"> - + @@ -190,10 +190,10 @@ select s.id, c.id clerk_id, c.name clerk_name, - i.id isle_id, i.name isle_name + i.id aisle_id, i.name aisle_name from store s left join clerk c on c.store_id = s.id - left join isle i on i.store_id = s.id + left join aisle i on i.store_id = s.id where s.id = #{id} order by s.id, c.id, i.id ]]> @@ -230,9 +230,9 @@ select 1 num, s.id, s.name, - i.id isle_id, i.name isle_name + i.id aisle_id, i.name aisle_name from store s - left join isle i on i.store_id = s.id + left join aisle i on i.store_id = s.id order by s.id, i.id ]]> From ccc20712a75ba1c127e14a20771181fec5f59e21 Mon Sep 17 00:00:00 2001 From: Iwao AVE! Date: Sat, 28 Dec 2024 05:30:24 +0900 Subject: [PATCH 08/14] Cleanup by maven plugins --- .../CollectionInConstructorTest.java | 13 ++++++------- .../immutable/HousePortfolio.java | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) 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 index c5e916cd8ed..20aa7db18b2 100644 --- 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 @@ -171,13 +171,12 @@ void testImmutableNestedObjects() { 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")))), + 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()); } } 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 index 287e4403b02..409160cdcdd 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * 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; From 9ade2430db6bde6dd60d7bdd70ab04d7fcdc62e2 Mon Sep 17 00:00:00 2001 From: Iwao AVE! Date: Fri, 27 Dec 2024 19:06:09 +0900 Subject: [PATCH 09/14] Remove constructor verification process ObjectFactory will throw an appropriate error. --- .../resultset/DefaultResultSetHandler.java | 20 ++-- .../resultset/PendingConstructorCreation.java | 98 +------------------ .../immutable/ImmutableConstructorTest.java | 4 +- 3 files changed, 10 insertions(+), 112 deletions(-) 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 1bf0f5411b0..5a1dddc60e4 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java @@ -380,8 +380,7 @@ private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap r throw new ExecutorException("Expected result object to be a pending constructor creation!"); } - createAndStorePendingCreation(resultHandler, resultSet, resultContext, (PendingConstructorCreation) rowValue, - lastHandledCreation == null); + createAndStorePendingCreation(resultHandler, resultSet, resultContext, (PendingConstructorCreation) rowValue); lastHandledCreation = (PendingConstructorCreation) rowValue; } } @@ -1069,7 +1068,6 @@ 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(); - boolean verifyPendingCreationResult = true; PendingConstructorCreation lastHandledCreation = null; if (useCollectionConstructorInjection) { verifyPendingCreationPreconditions(parentMapping); @@ -1090,12 +1088,8 @@ private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap r // issue #577, #542 && #101 if (useCollectionConstructorInjection) { if (foundNewUniqueRow && lastHandledCreation != null) { - createAndStorePendingCreation(resultHandler, resultSet, resultContext, lastHandledCreation, - verifyPendingCreationResult); + createAndStorePendingCreation(resultHandler, resultSet, resultContext, lastHandledCreation); lastHandledCreation = null; - // we only need to verify the first the result for a given result set - // as we can assume the next result will look exactly the same w.r.t its mapping - verifyPendingCreationResult = false; } rowValue = getRowValue(rsw, discriminatedResultMap, rowKey, null, partialObject); @@ -1117,8 +1111,7 @@ private void handleRowValuesForNestedResultMap(ResultSetWrapper rsw, ResultMap r } if (useCollectionConstructorInjection && lastHandledCreation != null) { - createAndStorePendingCreation(resultHandler, resultSet, resultContext, lastHandledCreation, - verifyPendingCreationResult); + createAndStorePendingCreation(resultHandler, resultSet, resultContext, lastHandledCreation); } else if (rowValue != null && mappedStatement.isResultOrdered() && shouldProcessMoreRows(resultContext, rowBounds)) { storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); @@ -1298,7 +1291,7 @@ private void createPendingConstructorCreations(Object rowValue) { for (Object pendingCreation : pendingCreations) { if (pendingCreation instanceof PendingConstructorCreation) { final PendingConstructorCreation pendingConstructorCreation = (PendingConstructorCreation) pendingCreation; - targetMetaObject.add(pendingConstructorCreation.create(objectFactory, false)); + targetMetaObject.add(pendingConstructorCreation.create(objectFactory)); } } } @@ -1317,9 +1310,8 @@ private void verifyPendingCreationPreconditions(ResultMapping parentMapping) { } private void createAndStorePendingCreation(ResultHandler resultHandler, ResultSet resultSet, - DefaultResultContext resultContext, PendingConstructorCreation pendingCreation, boolean shouldVerify) - throws SQLException { - final Object result = pendingCreation.create(objectFactory, shouldVerify); + DefaultResultContext resultContext, PendingConstructorCreation pendingCreation) throws SQLException { + final Object result = pendingCreation.create(objectFactory); storeObject(resultHandler, resultContext, result, null, resultSet); nestedResultObjects.clear(); } diff --git a/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java b/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java index 1d32d391c3e..5b7f27d09e3 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java @@ -15,23 +15,16 @@ */ package org.apache.ibatis.executor.resultset; -import java.lang.reflect.Constructor; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; 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.DefaultObjectFactory; import org.apache.ibatis.reflection.factory.ObjectFactory; /** @@ -108,87 +101,6 @@ void linkCollectionValue(ResultMapping constructorMapping, Object value) { linkedCollectionsByKey.get(creationKey).add(value); } - /** - * Verifies preconditions before we can actually create the result object, this is more of a sanity check to ensure - * all the mappings are as we expect them to be. - *

- * And if anything went wrong, provide the user with more information as to what went wrong - * - * @param objectFactory - * the object factory - */ - private void verifyCanCreate(ObjectFactory objectFactory) { - // if a custom object factory was supplied, we cannot reasionably verify that creation will work - // thus, we disable verification and leave it up to the end user. - if (!DefaultObjectFactory.class.equals(objectFactory.getClass())) { - return; - } - - // before we create, we need to get the constructor to be used and verify our types match - // since we added to the collection completely unchecked - final Constructor resolvedConstructor = resolveConstructor(resultType, constructorArgTypes); - final Type[] genericParameterTypes = resolvedConstructor.getGenericParameterTypes(); - for (int i = 0; i < genericParameterTypes.length; i++) { - if (!linkedCollectionMetaInfo.containsKey(i)) { - continue; - } - - final PendingCreationMetaInfo creationMetaInfo = linkedCollectionMetaInfo.get(i); - final Class resolvedItemType = checkResolvedItemType(creationMetaInfo, genericParameterTypes[i]); - - // ensure we have an empty collection if there are linked creations for this arg - final PendingCreationKey pendingCreationKey = creationMetaInfo.getPendingCreationKey(); - if (linkedCreationsByKey.containsKey(pendingCreationKey)) { - final Object emptyCollection = constructorArgs.get(i); - if (emptyCollection == null || !objectFactory.isCollection(emptyCollection.getClass())) { - throw new ExecutorException( - "Expected empty collection for '" + resolvedItemType + "', MyBatis internal error!"); - } - } else { - final Object linkedCollection = constructorArgs.get(i); - if (!linkedCollectionsByKey.containsKey(pendingCreationKey)) { - throw new ExecutorException( - "Expected linked collection for key '" + pendingCreationKey + "', not found! MyBatis internal error!"); - } - - // comparing memory locations here (we rely on that fact) - if (linkedCollection != linkedCollectionsByKey.get(pendingCreationKey)) { - throw new ExecutorException("Expected linked collection in creation to be the same as arg for resultMap '" - + pendingCreationKey + "', not equal! MyBatis internal error!"); - } - } - } - } - - private static Constructor resolveConstructor(Class type, List> constructorArgTypes) { - try { - if (constructorArgTypes == null) { - return type.getDeclaredConstructor(); - } - - return type.getDeclaredConstructor(constructorArgTypes.toArray(new Class[0])); - } catch (Exception e) { - String argTypes = Optional.ofNullable(constructorArgTypes).orElseGet(Collections::emptyList).stream() - .map(Class::getSimpleName).collect(Collectors.joining(",")); - throw new ReflectionException( - "Error resolving constructor for " + type + " with invalid types (" + argTypes + ") . Cause: " + e, e); - } - } - - private static Class checkResolvedItemType(PendingCreationMetaInfo creationMetaInfo, Type genericParameterTypes) { - final ParameterizedType genericParameterType = (ParameterizedType) genericParameterTypes; - final Class expectedType = (Class) genericParameterType.getActualTypeArguments()[0]; - final Class resolvedItemType = creationMetaInfo.getArgumentType(); - - if (!expectedType.isAssignableFrom(resolvedItemType)) { - throw new ReflectionException( - "Expected type '" + resolvedItemType + "', while the actual type of the collection was '" + expectedType - + "', ensure your resultMap matches the type of the collection you are trying to inject"); - } - - return resolvedItemType; - } - @Override public String toString() { return "PendingConstructorCreation(" + this.hashCode() + "){" + "resultType=" + resultType + '}'; @@ -199,16 +111,10 @@ public String toString() { * * @param objectFactory * the object factory - * @param verifyCreate - * should we verify this object can be created, should only be needed once * * @return the new immutable result */ - Object create(ObjectFactory objectFactory, boolean verifyCreate) { - if (verifyCreate) { - verifyCanCreate(objectFactory); - } - + 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); @@ -228,7 +134,7 @@ Object create(ObjectFactory objectFactory, boolean verifyCreate) { final List linkedCreations = linkedCreationsByKey.get(pendingCreationKey); for (PendingConstructorCreation linkedCreation : linkedCreations) { - emptyCollection.add(linkedCreation.create(objectFactory, verifyCreate)); + emptyCollection.add(linkedCreation.create(objectFactory)); } newArguments.add(emptyCollection); diff --git a/src/test/java/org/apache/ibatis/immutable/ImmutableConstructorTest.java b/src/test/java/org/apache/ibatis/immutable/ImmutableConstructorTest.java index 05f42bf4282..709f72b014a 100644 --- a/src/test/java/org/apache/ibatis/immutable/ImmutableConstructorTest.java +++ b/src/test/java/org/apache/ibatis/immutable/ImmutableConstructorTest.java @@ -143,9 +143,9 @@ void shouldSelectBlogWithPostsButNoCommentsOrTags() { void shouldFailToSelectBlogWithMissingConstructorForPostComments() { try (SqlSession session = sqlSessionFactory.openSession()) { ImmutableBlogMapper mapper = session.getMapper(ImmutableBlogMapper.class); - assertThatThrownBy(() -> mapper.retrieveAllBlogsWithMissingConstructor()).isInstanceOf(PersistenceException.class) + assertThatThrownBy(mapper::retrieveAllBlogsWithMissingConstructor).isInstanceOf(PersistenceException.class) .hasCauseInstanceOf(ReflectionException.class).hasMessageContaining( - "Error resolving constructor for class org.apache.ibatis.domain.blog.immutable.ImmutablePost with invalid types"); + "Error instantiating class org.apache.ibatis.domain.blog.immutable.ImmutablePost with invalid types"); } } } From f3a24327253a9a5baed70e07b9a3039696914c94 Mon Sep 17 00:00:00 2001 From: Iwao AVE! Date: Tue, 31 Dec 2024 15:57:58 +0900 Subject: [PATCH 10/14] Remove redundant containsKey invocations, etc. --- .../resultset/PendingConstructorCreation.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java b/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java index 5b7f27d09e3..3c0f6b3f4b6 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/PendingConstructorCreation.java @@ -64,10 +64,9 @@ Collection initializeCollectionForResultMapping(ObjectFactory objectFact "Cannot add a collection result to non-collection based resultMapping: " + constructorMapping); } - final PendingCreationKey creationKey = new PendingCreationKey(constructorMapping); - return linkedCollectionsByKey.computeIfAbsent(creationKey, (k) -> { + 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(), creationKey)); + 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); @@ -77,7 +76,7 @@ Collection initializeCollectionForResultMapping(ObjectFactory objectFact void linkCreation(ResultMapping constructorMapping, PendingConstructorCreation pcc) { final PendingCreationKey creationKey = new PendingCreationKey(constructorMapping); final List pendingConstructorCreations = linkedCreationsByKey - .computeIfAbsent(creationKey, (k) -> new ArrayList<>()); + .computeIfAbsent(creationKey, k -> new ArrayList<>()); if (pendingConstructorCreations.contains(pcc)) { throw new ExecutorException("Cannot link inner constructor creation with same value, MyBatis internal error!"); @@ -92,13 +91,10 @@ void linkCollectionValue(ResultMapping constructorMapping, Object value) { return; } - final PendingCreationKey creationKey = new PendingCreationKey(constructorMapping); - if (!linkedCollectionsByKey.containsKey(creationKey)) { + 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!"); - } - - linkedCollectionsByKey.get(creationKey).add(value); + }).add(value); } @Override @@ -128,10 +124,10 @@ Object create(ObjectFactory objectFactory) { // time to finally build this collection final PendingCreationKey pendingCreationKey = creationMetaInfo.getPendingCreationKey(); - if (linkedCreationsByKey.containsKey(pendingCreationKey)) { + final List linkedCreations = linkedCreationsByKey.get(pendingCreationKey); + if (linkedCreations != null) { @SuppressWarnings("unchecked") final Collection emptyCollection = (Collection) existingArg; - final List linkedCreations = linkedCreationsByKey.get(pendingCreationKey); for (PendingConstructorCreation linkedCreation : linkedCreations) { emptyCollection.add(linkedCreation.create(objectFactory)); From 212cbad530add179cbaf49ca0f17f62724fb2a6b Mon Sep 17 00:00:00 2001 From: Iwao AVE! Date: Thu, 2 Jan 2025 06:35:44 +0900 Subject: [PATCH 11/14] Revert the unnecessary change (for diff) --- src/site/markdown/sqlmap-xml.md | 34 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/site/markdown/sqlmap-xml.md b/src/site/markdown/sqlmap-xml.md index e6abf781dd9..de2eba488d9 100644 --- a/src/site/markdown/sqlmap-xml.md +++ b/src/site/markdown/sqlmap-xml.md @@ -67,23 +67,23 @@ The select element has more attributes that allow you to configure the details o resultSetType="FORWARD_ONLY"> ``` -| Attribute | Description | -|---------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | A unique identifier in this namespace that can be used to reference this statement. | -| `parameterType` | The fully qualified class name or alias for the parameter that will be passed into this statement. This attribute is optional because MyBatis can calculate the TypeHandler to use out of the actual parameter passed to the statement. Default is `unset`. | -| `parameterMap` | This is a deprecated approach to referencing an external `parameterMap`. Use inline parameter mappings and the `parameterType` attribute. | -| `resultType` | The fully qualified class name or alias for the expected type that will be returned from this statement. Note that in the case of collections, this should be the type that the collection contains, not the type of the collection itself. Use `resultType` OR `resultMap`, not both. | -| `resultMap` | A named reference to an external `resultMap`. Result maps are the most powerful feature of MyBatis, and with a good understanding of them, many difficult mapping cases can be solved. Use `resultMap` OR `resultType`, not both. | -| `flushCache` | Setting this to true will cause the local and 2nd level caches to be flushed whenever this statement is called. Default: `false` for select statements. | -| `useCache` | Setting this to true will cause the results of this statement to be cached in 2nd level cache. Default: `true` for select statements. | -| `timeout` | This sets the number of seconds the driver will wait for the database to return from a request, before throwing an exception. Default is `unset` (driver dependent). | -| `fetchSize` | This is a driver hint that will attempt to cause the driver to return results in batches of rows numbering in size equal to this setting. Default is `unset` (driver dependent). | -| `statementType` | Any one of `STATEMENT`, `PREPARED` or `CALLABLE`. This causes MyBatis to use `Statement`, `PreparedStatement` or `CallableStatement` respectively. Default: `PREPARED`. | -| `resultSetType` | Any one of `FORWARD_ONLY`|`SCROLL_SENSITIVE`|`SCROLL_INSENSITIVE`|`DEFAULT`(same as unset). Default is `unset` (driver dependent). | -| `databaseId` | In case there is a configured databaseIdProvider, MyBatis will load all statements with no `databaseId` attribute or with a `databaseId` that matches the current one. If case the same statement if found with and without the `databaseId` the latter will be discarded. | -| `resultOrdered` | This is only applicable for nested result select statements: If this is true, it is assumed that nested results are contained or grouped together such that when a new main result row is returned, no references to a previous result row will occur anymore. This allows nested results to be filled much more memory friendly. Default: `false`. | -| `resultSets` | This is only applicable for multiple result sets. It lists the result sets that will be returned by the statement and gives a name to each one. Names are separated by commas. | -| `affectData` | Set this to true when writing a INSERT, UPDATE or DELETE statement that returns data so that the transaction is controlled properly. Also see [Transaction Control Method](./java-api.html#transaction-control-methods). Default: `false` (since 3.5.12) | +| Attribute | Description | +|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | A unique identifier in this namespace that can be used to reference this statement. | +| `parameterType` | The fully qualified class name or alias for the parameter that will be passed into this statement. This attribute is optional because MyBatis can calculate the TypeHandler to use out of the actual parameter passed to the statement. Default is `unset`. | +| `parameterMap` | This is a deprecated approach to referencing an external `parameterMap`. Use inline parameter mappings and the `parameterType` attribute. | +| `resultType` | The fully qualified class name or alias for the expected type that will be returned from this statement. Note that in the case of collections, this should be the type that the collection contains, not the type of the collection itself. Use `resultType` OR `resultMap`, not both. | +| `resultMap` | A named reference to an external `resultMap`. Result maps are the most powerful feature of MyBatis, and with a good understanding of them, many difficult mapping cases can be solved. Use `resultMap` OR `resultType`, not both. | +| `flushCache` | Setting this to true will cause the local and 2nd level caches to be flushed whenever this statement is called. Default: `false` for select statements. | +| `useCache` | Setting this to true will cause the results of this statement to be cached in 2nd level cache. Default: `true` for select statements. | +| `timeout` | This sets the number of seconds the driver will wait for the database to return from a request, before throwing an exception. Default is `unset` (driver dependent). | +| `fetchSize` | This is a driver hint that will attempt to cause the driver to return results in batches of rows numbering in size equal to this setting. Default is `unset` (driver dependent). | +| `statementType` | Any one of `STATEMENT`, `PREPARED` or `CALLABLE`. This causes MyBatis to use `Statement`, `PreparedStatement` or `CallableStatement` respectively. Default: `PREPARED`. | +| `resultSetType` | Any one of `FORWARD_ONLY`|`SCROLL_SENSITIVE`|`SCROLL_INSENSITIVE`|`DEFAULT`(same as unset). Default is `unset` (driver dependent). | +| `databaseId` | In case there is a configured databaseIdProvider, MyBatis will load all statements with no `databaseId` attribute or with a `databaseId` that matches the current one. If case the same statement if found with and without the `databaseId` the latter will be discarded. | +| `resultOrdered` | This is only applicable for nested result select statements: If this is true, it is assumed that nested results are contained or grouped together such that when a new main result row is returned, no references to a previous result row will occur anymore. This allows nested results to be filled much more memory friendly. Default: `false`. | +| `resultSets` | This is only applicable for multiple result sets. It lists the result sets that will be returned by the statement and gives a name to each one. Names are separated by commas. | +| `affectData` | Set this to true when writing a INSERT, UPDATE or DELETE statement that returns data so that the transaction is controlled properly. Also see [Transaction Control Method](./java-api.html#transaction-control-methods). Default: `false` (since 3.5.12) | [Select Attributes] ### insert, update and delete From 1562473bcaf52b765e94eb350477a1b3c2b53e04 Mon Sep 17 00:00:00 2001 From: Iwao AVE! Date: Thu, 2 Jan 2025 08:50:06 +0900 Subject: [PATCH 12/14] Edited en doc a little bit and copied the doc changes to the other locales MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copying new doc is now much harder. I knew this was going to happen... I shouldn't have budged 😣 https://github.com/mybatis/mybatis-3/pull/2784#issuecomment-1416833670 --- src/site/es/xdoc/sqlmap-xml.xml | 117 ++++++++++++++++++++++++++++ src/site/ja/xdoc/sqlmap-xml.xml | 117 ++++++++++++++++++++++++++++ src/site/ko/xdoc/sqlmap-xml.xml | 118 +++++++++++++++++++++++++++++ src/site/markdown/sqlmap-xml.md | 7 +- src/site/zh_CN/xdoc/sqlmap-xml.xml | 118 +++++++++++++++++++++++++++++ 5 files changed, 473 insertions(+), 4 deletions(-) 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 de2eba488d9..cf496a8fcdb 100644 --- a/src/site/markdown/sqlmap-xml.md +++ b/src/site/markdown/sqlmap-xml.md @@ -732,10 +732,11 @@ MyBatis needs to be explicitly told that the results have been ordered in such a 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 ``` -In this case, the results are by ordered correctly by default. We can imagine the output to look somthing like: +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 | |--------|------|------------|------|-------------| @@ -746,8 +747,6 @@ In this case, the results are by ordered correctly by default. We can imagine th | 5 | 3 | Peter | 3 | Maintainers | | 6 | 3 | Peter | 4 | 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. - After this query is run, we would have the following results: ``` @@ -756,7 +755,7 @@ User{username=Jack, roles=[]} User{username=Peter, roles=[Users, Maintainers, Approvers]} ``` -This functionality is still experimental, please report any issues you may find on the issue tracker. +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/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.

+ +

关联

From 9a92894bcbfd183f9b7f4206079b66ca7172b20f Mon Sep 17 00:00:00 2001 From: Iwao AVE! Date: Thu, 2 Jan 2025 09:01:50 +0900 Subject: [PATCH 13/14] License year --- src/test/java/org/apache/ibatis/binding/BindingTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/apache/ibatis/binding/BindingTest.java b/src/test/java/org/apache/ibatis/binding/BindingTest.java index 4824d2cb3a7..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. From 5916710f266b82e9b3d016011b59339f73234034 Mon Sep 17 00:00:00 2001 From: Willie Scholtz Date: Thu, 2 Jan 2025 09:28:17 +0100 Subject: [PATCH 14/14] Remove unused variable --- .../ibatis/executor/resultset/DefaultResultSetHandler.java | 2 -- 1 file changed, 2 deletions(-) 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 18b5f6049fe..81ca5429f58 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java @@ -365,7 +365,6 @@ protected void checkResultHandler() { private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { final boolean useCollectionConstructorInjection = resultMap.hasResultMapsUsingConstructorCollection(); - PendingConstructorCreation lastHandledCreation = null; DefaultResultContext resultContext = new DefaultResultContext<>(); ResultSet resultSet = rsw.getResultSet(); @@ -381,7 +380,6 @@ private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap r } createAndStorePendingCreation(resultHandler, resultSet, resultContext, (PendingConstructorCreation) rowValue); - lastHandledCreation = (PendingConstructorCreation) rowValue; } } }