diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java index 47344356..e936fb0f 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/EntityRowMapper.java @@ -79,8 +79,8 @@ public class EntityRowMapper implements RowMapper { RowDocument document = RowDocumentResultSetExtractor.toRowDocument(resultSet); return identifier == null // - ? converter.readAndResolve(entity.getType(), document) // - : converter.readAndResolve(entity.getType(), document, identifier); + ? converter.readAndResolve(entity.getTypeInformation(), document, Identifier.empty()) // + : converter.readAndResolve(entity.getTypeInformation(), document, identifier); } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java index d97c2a69..ae5b4aa2 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcConverter.java @@ -26,6 +26,7 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.domain.RowDocument; +import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; /** @@ -47,7 +48,21 @@ public interface JdbcConverter extends RelationalConverter { * @return The converted value wrapped in a {@link JdbcValue}. Guaranteed to be not {@literal null}. * @since 2.4 */ - JdbcValue writeJdbcValue(@Nullable Object value, Class type, SQLType sqlType); + default JdbcValue writeJdbcValue(@Nullable Object value, Class type, SQLType sqlType) { + return writeJdbcValue(value, TypeInformation.of(type), sqlType); + } + + /** + * Convert a property value into a {@link JdbcValue} that contains the converted value and information how to bind it + * to JDBC parameters. + * + * @param value a value as it is used in the object model. May be {@code null}. + * @param type {@link TypeInformation} into which the value is to be converted. Must not be {@code null}. + * @param sqlType the {@link SQLType} to be used if non is specified by a converter. + * @return The converted value wrapped in a {@link JdbcValue}. Guaranteed to be not {@literal null}. + * @since 3.2.6 + */ + JdbcValue writeJdbcValue(@Nullable Object value, TypeInformation type, SQLType sqlType); /** * Read the current row from {@link ResultSet} to an {@link RelationalPersistentEntity#getType() entity}. @@ -84,7 +99,7 @@ public interface JdbcConverter extends RelationalConverter { default T mapRow(PersistentPropertyPathExtension path, ResultSet resultSet, Identifier identifier, Object key) { try { - return (T) readAndResolve(path.getRequiredLeafEntity().getType(), + return (T) readAndResolve(path.getRequiredLeafEntity().getTypeInformation(), RowDocumentResultSetExtractor.toRowDocument(resultSet), identifier); } catch (SQLException e) { throw new RuntimeException(e); @@ -118,7 +133,23 @@ public interface JdbcConverter extends RelationalConverter { * @since 3.2 * @see #read(Class, RowDocument) */ - R readAndResolve(Class type, RowDocument source, Identifier identifier); + default R readAndResolve(Class type, RowDocument source, Identifier identifier) { + return readAndResolve(TypeInformation.of(type), source, identifier); + } + + /** + * Read a {@link RowDocument} into the requested {@link TypeInformation aggregate type} and resolve references by + * looking these up from {@link RelationResolver}. + * + * @param type target aggregate type. + * @param source source {@link RowDocument}. + * @param identifier identifier chain. + * @return the converted object. + * @param aggregate type. + * @since 3.2.6 + * @see #read(Class, RowDocument) + */ + R readAndResolve(TypeInformation type, RowDocument source, Identifier identifier); /** * The type to be used to store this property in the database. Multidimensional arrays are unwrapped to reflect a @@ -126,7 +157,7 @@ public interface JdbcConverter extends RelationalConverter { * * @return a {@link Class} that is suitable for usage with JDBC drivers. * @see org.springframework.data.jdbc.support.JdbcUtil#targetSqlTypeFor(Class) - * @since 2.0 + * @since 2.0 TODO: Introduce variant returning TypeInformation. */ Class getColumnType(RelationalPersistentProperty property); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java index 1c4f28c0..c9e50657 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MapEntityRowMapper.java @@ -64,7 +64,7 @@ class MapEntityRowMapper implements RowMapper> { @SuppressWarnings("unchecked") private T mapEntity(RowDocument document, Object key) { - return (T) converter.readAndResolve(path.getRequiredLeafEntity().getType(), document, + return (T) converter.readAndResolve(path.getRequiredLeafEntity().getTypeInformation(), document, identifier.withPart(keyColumn, key, Object.class)); } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java index ee57f76e..5b14cc07 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java @@ -231,14 +231,14 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements } @Override - public JdbcValue writeJdbcValue(@Nullable Object value, Class columnType, SQLType sqlType) { + public JdbcValue writeJdbcValue(@Nullable Object value, TypeInformation columnType, SQLType sqlType) { JdbcValue jdbcValue = tryToConvertToJdbcValue(value); if (jdbcValue != null) { return jdbcValue; } - Object convertedValue = writeValue(value, TypeInformation.of(columnType)); + Object convertedValue = writeValue(value, columnType); if (convertedValue == null || !convertedValue.getClass().isArray()) { @@ -275,7 +275,7 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements @SuppressWarnings("unchecked") @Override - public R readAndResolve(Class type, RowDocument source, Identifier identifier) { + public R readAndResolve(TypeInformation type, RowDocument source, Identifier identifier) { RelationalPersistentEntity entity = (RelationalPersistentEntity) getMappingContext() .getRequiredPersistentEntity(type); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java index 4e12fa49..aabf36f4 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java @@ -18,6 +18,7 @@ package org.springframework.data.jdbc.repository.query; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; +import java.util.function.Supplier; import java.util.stream.Stream; import org.springframework.core.convert.converter.Converter; @@ -83,9 +84,9 @@ public abstract class AbstractJdbcQuery implements RepositoryQuery { */ @Deprecated(since = "3.1", forRemoval = true) // a better name would be createReadingQueryExecution - protected JdbcQueryExecution getQueryExecution(JdbcQueryMethod queryMethod, + JdbcQueryExecution getQueryExecution(JdbcQueryMethod queryMethod, @Nullable ResultSetExtractor extractor, RowMapper rowMapper) { - return createReadingQueryExecution(extractor, rowMapper); + return createReadingQueryExecution(extractor, () -> rowMapper); } /** @@ -96,21 +97,21 @@ public abstract class AbstractJdbcQuery implements RepositoryQuery { * @param rowMapper must not be {@literal null}. * @return a JdbcQueryExecution appropriate for {@literal queryMethod}. Guaranteed to be not {@literal null}. */ - protected JdbcQueryExecution createReadingQueryExecution(@Nullable ResultSetExtractor extractor, - RowMapper rowMapper) { + JdbcQueryExecution createReadingQueryExecution(@Nullable ResultSetExtractor extractor, + Supplier> rowMapper) { if (getQueryMethod().isCollectionQuery()) { - return extractor != null ? createSingleReadingQueryExecution(extractor) : collectionQuery(rowMapper); + return extractor != null ? createSingleReadingQueryExecution(extractor) : collectionQuery(rowMapper.get()); } if (getQueryMethod().isStreamQuery()) { - return extractor != null ? createSingleReadingQueryExecution(extractor) : streamQuery(rowMapper); + return extractor != null ? createSingleReadingQueryExecution(extractor) : streamQuery(rowMapper.get()); } - return extractor != null ? createSingleReadingQueryExecution(extractor) : singleObjectQuery(rowMapper); + return extractor != null ? createSingleReadingQueryExecution(extractor) : singleObjectQuery(rowMapper.get()); } - protected JdbcQueryExecution createModifyingQueryExecutor() { + JdbcQueryExecution createModifyingQueryExecutor() { return (query, parameters) -> { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java new file mode 100755 index 00000000..27742790 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java @@ -0,0 +1,99 @@ +/* + * Copyright 2018-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.springframework.data.jdbc.repository.query; + +import java.sql.SQLType; +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.data.jdbc.core.convert.JdbcColumnTypes; +import org.springframework.data.jdbc.support.JdbcUtil; +import org.springframework.data.relational.repository.query.RelationalParameters; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.ParametersSource; +import org.springframework.data.util.Lazy; +import org.springframework.data.util.TypeInformation; + +/** + * Custom extension of {@link RelationalParameters}. + * + * @author Mark Paluch + * @since 3.2.6 + */ +public class JdbcParameters extends RelationalParameters { + + /** + * Creates a new {@link JdbcParameters} instance from the given {@link ParametersSource}. + * + * @param parametersSource must not be {@literal null}. + */ + public JdbcParameters(ParametersSource parametersSource) { + super(parametersSource, + methodParameter -> new JdbcParameter(methodParameter, parametersSource.getDomainTypeInformation())); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private JdbcParameters(List parameters) { + super((List) parameters); + } + + @Override + public JdbcParameter getParameter(int index) { + return (JdbcParameter) super.getParameter(index); + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected JdbcParameters createFrom(List parameters) { + return new JdbcParameters((List) parameters); + } + + /** + * Custom {@link Parameter} implementation. + * + * @author Mark Paluch + * @author Chirag Tailor + */ + public static class JdbcParameter extends RelationalParameter { + + private final SQLType sqlType; + private final Lazy actualSqlType; + + /** + * Creates a new {@link RelationalParameter}. + * + * @param parameter must not be {@literal null}. + */ + JdbcParameter(MethodParameter parameter, TypeInformation domainType) { + super(parameter, domainType); + + TypeInformation typeInformation = getTypeInformation(); + + sqlType = JdbcUtil.targetSqlTypeFor(JdbcColumnTypes.INSTANCE.resolvePrimitiveType(typeInformation.getType())); + + actualSqlType = Lazy.of(() -> JdbcUtil + .targetSqlTypeFor(JdbcColumnTypes.INSTANCE.resolvePrimitiveType(typeInformation.getActualType().getType()))); + } + + public SQLType getSqlType() { + return sqlType; + } + + public SQLType getActualSqlType() { + return actualSqlType.get(); + } + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java index d771eb6c..68c684f1 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryMethod.java @@ -38,6 +38,7 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -57,6 +58,7 @@ public class JdbcQueryMethod extends QueryMethod { private final Map, Optional> annotationCache; private final NamedQueries namedQueries; private @Nullable RelationalEntityMetadata metadata; + private final boolean modifyingQuery; // TODO: Remove NamedQueries and put it into JdbcQueryLookupStrategy public JdbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory, @@ -68,11 +70,12 @@ public class JdbcQueryMethod extends QueryMethod { this.method = method; this.mappingContext = mappingContext; this.annotationCache = new ConcurrentReferenceHashMap<>(); + this.modifyingQuery = AnnotationUtils.findAnnotation(method, Modifying.class) != null; } @Override protected RelationalParameters createParameters(Method method) { - return new RelationalParameters(method); + return new JdbcParameters(method); } @Override @@ -106,8 +109,8 @@ public class JdbcQueryMethod extends QueryMethod { } @Override - public RelationalParameters getParameters() { - return (RelationalParameters) super.getParameters(); + public JdbcParameters getParameters() { + return (JdbcParameters) super.getParameters(); } /** @@ -122,6 +125,17 @@ public class JdbcQueryMethod extends QueryMethod { return StringUtils.hasText(annotatedValue) ? annotatedValue : getNamedQuery(); } + String getRequiredQuery() { + + String query = getDeclaredQuery(); + + if (ObjectUtils.isEmpty(query)) { + throw new IllegalStateException(String.format("No query specified on %s", getName())); + } + + return query; + } + /** * Returns the annotated query if it exists. * @@ -208,7 +222,7 @@ public class JdbcQueryMethod extends QueryMethod { */ @Override public boolean isModifyingQuery() { - return AnnotationUtils.findAnnotation(method, Modifying.class) != null; + return modifyingQuery; } @SuppressWarnings("unchecked") @@ -246,7 +260,7 @@ public class JdbcQueryMethod extends QueryMethod { /** * Looks up the {@link Lock} annotation from the query method. - * + * * @return the {@link Optional} wrapped {@link Lock} annotation. */ Optional lookupLockAnnotation() { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java index 9c8a93c6..b29add42 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java @@ -21,7 +21,9 @@ import java.sql.ResultSet; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.function.Function; import java.util.function.LongSupplier; +import java.util.function.Supplier; import org.springframework.core.convert.converter.Converter; import org.springframework.data.domain.Pageable; @@ -29,6 +31,7 @@ import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.repository.query.RelationalEntityMetadata; @@ -39,6 +42,7 @@ import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.data.util.Lazy; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; @@ -61,7 +65,7 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery { private final Parameters parameters; private final Dialect dialect; private final JdbcConverter converter; - private final RowMapperFactory rowMapperFactory; + private final CachedRowMapperFactory cachedRowMapperFactory; private final PartTree tree; /** @@ -105,12 +109,12 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery { this.parameters = queryMethod.getParameters(); this.dialect = dialect; this.converter = converter; - this.rowMapperFactory = rowMapperFactory; - this.tree = new PartTree(queryMethod.getName(), queryMethod.getResultProcessor() - .getReturnedType().getDomainType()); + this.tree = new PartTree(queryMethod.getName(), queryMethod.getResultProcessor().getReturnedType().getDomainType()); JdbcQueryCreator.validate(this.tree, this.parameters, this.converter.getMappingContext()); + this.cachedRowMapperFactory = new CachedRowMapperFactory(tree, rowMapperFactory, converter, + queryMethod.getResultProcessor()); } private Sort getDynamicSort(RelationalParameterAccessor accessor) { @@ -134,18 +138,9 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery { RelationalParametersParameterAccessor accessor) { ResultSetExtractor extractor = tree.isExistsProjection() ? (ResultSet::next) : null; - - RowMapper rowMapper; - - if (tree.isCountProjection() || tree.isExistsProjection()) { - rowMapper = rowMapperFactory.create(resolveTypeToRead(processor)); - } else { - - Converter resultProcessingConverter = new ResultProcessingConverter(processor, - this.converter.getMappingContext(), this.converter.getEntityInstantiators()); - rowMapper = new ConvertingRowMapper<>(rowMapperFactory.create(processor.getReturnedType().getDomainType()), - resultProcessingConverter); - } + Supplier> rowMapper = parameters.hasDynamicProjection() + ? () -> cachedRowMapperFactory.getRowMapper(processor) + : cachedRowMapperFactory; JdbcQueryExecution queryExecution = getJdbcQueryExecution(extractor, rowMapper); @@ -174,7 +169,7 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery { return queryExecution; } - protected ParametrizedQuery createQuery(RelationalParametersParameterAccessor accessor, ReturnedType returnedType) { + ParametrizedQuery createQuery(RelationalParametersParameterAccessor accessor, ReturnedType returnedType) { RelationalEntityMetadata entityMetadata = getQueryMethod().getEntityInformation(); @@ -183,10 +178,11 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery { return queryCreator.createQuery(getDynamicSort(accessor)); } - private JdbcQueryExecution getJdbcQueryExecution(@Nullable ResultSetExtractor extractor, RowMapper rowMapper) { + private JdbcQueryExecution getJdbcQueryExecution(@Nullable ResultSetExtractor extractor, + Supplier> rowMapper) { if (getQueryMethod().isPageQuery() || getQueryMethod().isSliceQuery()) { - return collectionQuery(rowMapper); + return collectionQuery(rowMapper.get()); } else { if (getQueryMethod().isModifyingQuery()) { @@ -259,4 +255,45 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery { } } + + /** + * Cached implementation of {@link RowMapper} suppler providing either a cached variant of the RowMapper or creating a + * new one when using dynamic projections. + */ + class CachedRowMapperFactory implements Supplier> { + + private final Lazy> rowMapper; + private final Function> rowMapperFunction; + + public CachedRowMapperFactory(PartTree tree, RowMapperFactory rowMapperFactory, RelationalConverter converter, + ResultProcessor defaultResultProcessor) { + + this.rowMapperFunction = processor -> { + + if (tree.isCountProjection() || tree.isExistsProjection()) { + return rowMapperFactory.create(resolveTypeToRead(processor)); + } + Converter resultProcessingConverter = new ResultProcessingConverter(processor, + converter.getMappingContext(), converter.getEntityInstantiators()); + return new ConvertingRowMapper<>(rowMapperFactory.create(processor.getReturnedType().getDomainType()), + resultProcessingConverter); + }; + + this.rowMapper = Lazy.of(() -> this.rowMapperFunction.apply(defaultResultProcessor)); + } + + @Override + public RowMapper get() { + return getRowMapper(); + } + + public RowMapper getRowMapper() { + return rowMapper.get(); + } + + public RowMapper getRowMapper(ResultProcessor resultProcessor) { + return rowMapperFunction.apply(resultProcessor); + } + + } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java index dbadd46f..ef353497 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java @@ -22,10 +22,13 @@ import java.sql.SQLType; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; +import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; -import org.springframework.core.convert.converter.Converter; import org.springframework.data.jdbc.core.convert.JdbcColumnTypes; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcValue; @@ -40,6 +43,7 @@ import org.springframework.data.repository.query.QueryMethodEvaluationContextPro import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.SpelEvaluator; import org.springframework.data.repository.query.SpelQueryContext; +import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; @@ -48,6 +52,7 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ObjectUtils; /** @@ -70,8 +75,13 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { private static final String PARAMETER_NEEDS_TO_BE_NAMED = "For queries with named parameters you need to provide names for method parameters; Use @Param for query method parameters, or when on Java 8+ use the javac flag -parameters"; private final JdbcConverter converter; private final RowMapperFactory rowMapperFactory; + private final SpelEvaluator spelEvaluator; + private final boolean containsSpelExpressions; + private final String query; private BeanFactory beanFactory; - private final QueryMethodEvaluationContextProvider evaluationContextProvider; + + private final CachedRowMapperFactory cachedRowMapperFactory; + private final CachedResultSetExtractorFactory cachedResultSetExtractorFactory; /** * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} @@ -106,7 +116,6 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { this.converter = converter; this.rowMapperFactory = rowMapperFactory; - this.evaluationContextProvider = evaluationContextProvider; if (queryMethod.isSliceQuery()) { throw new UnsupportedOperationException( @@ -122,6 +131,19 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { throw new UnsupportedOperationException( "Queries with Limit are not supported using string-based queries; Offending method: " + queryMethod); } + + this.cachedRowMapperFactory = new CachedRowMapperFactory( + () -> rowMapperFactory.create(queryMethod.getResultProcessor().getReturnedType().getReturnedType())); + this.cachedResultSetExtractorFactory = new CachedResultSetExtractorFactory( + this.cachedRowMapperFactory::getRowMapper); + + SpelQueryContext.EvaluatingSpelQueryContext queryContext = SpelQueryContext + .of((counter, expression) -> String.format("__$synthetic$__%d", counter + 1), String::concat) + .withEvaluationContextProvider(evaluationContextProvider); + + this.query = queryMethod.getRequiredQuery(); + this.spelEvaluator = queryContext.parse(query, getQueryMethod().getParameters()); + this.containsSpelExpressions = !this.spelEvaluator.getQueryString().equals(queryContext); } @Override @@ -129,49 +151,38 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { RelationalParameterAccessor accessor = new RelationalParametersParameterAccessor(getQueryMethod(), objects); ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor); - ResultProcessingConverter converter = new ResultProcessingConverter(processor, this.converter.getMappingContext(), - this.converter.getEntityInstantiators()); - - JdbcQueryExecution queryExecution = createJdbcQueryExecution(accessor, processor, converter); + JdbcQueryExecution queryExecution = createJdbcQueryExecution(accessor, processor); MapSqlParameterSource parameterMap = this.bindParameters(accessor); - String query = determineQuery(); + return queryExecution.execute(processSpelExpressions(objects, parameterMap), parameterMap); + } - if (ObjectUtils.isEmpty(query)) { - throw new IllegalStateException(String.format("No query specified on %s", getQueryMethod().getName())); + private String processSpelExpressions(Object[] objects, MapSqlParameterSource parameterMap) { + + if (containsSpelExpressions) { + + spelEvaluator.evaluate(objects).forEach(parameterMap::addValue); + return spelEvaluator.getQueryString(); } - return queryExecution.execute(processSpelExpressions(objects, parameterMap, query), parameterMap); + return this.query; } private JdbcQueryExecution createJdbcQueryExecution(RelationalParameterAccessor accessor, - ResultProcessor processor, ResultProcessingConverter converter) { + ResultProcessor processor) { if (getQueryMethod().isModifyingQuery()) { return createModifyingQueryExecutor(); } else { - RowMapper rowMapper = determineRowMapper(rowMapperFactory.create(resolveTypeToRead(processor)), converter, - accessor.findDynamicProjection() != null); + Supplier> rowMapper = () -> determineRowMapper(processor, accessor.findDynamicProjection() != null); + ResultSetExtractor resultSetExtractor = determineResultSetExtractor(rowMapper); - return createReadingQueryExecution(determineResultSetExtractor(rowMapper), rowMapper); + return createReadingQueryExecution(resultSetExtractor, rowMapper); } } - private String processSpelExpressions(Object[] objects, MapSqlParameterSource parameterMap, String query) { - - SpelQueryContext.EvaluatingSpelQueryContext queryContext = SpelQueryContext - .of((counter, expression) -> String.format("__$synthetic$__%d", counter + 1), String::concat) - .withEvaluationContextProvider(evaluationContextProvider); - - SpelEvaluator spelEvaluator = queryContext.parse(query, getQueryMethod().getParameters()); - - spelEvaluator.evaluate(objects).forEach(parameterMap::addValue); - - return spelEvaluator.getQueryString(); - } - private MapSqlParameterSource bindParameters(RelationalParameterAccessor accessor) { MapSqlParameterSource parameters = new MapSqlParameterSource(); @@ -189,7 +200,7 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { String parameterName = p.getName().orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED)); - RelationalParameters.RelationalParameter parameter = getQueryMethod().getParameters().getParameter(p.getIndex()); + JdbcParameters.JdbcParameter parameter = getQueryMethod().getParameters().getParameter(p.getIndex()); TypeInformation typeInformation = parameter.getTypeInformation(); JdbcValue jdbcValue; @@ -200,8 +211,7 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { TypeInformation actualType = typeInformation.getRequiredActualType(); for (Object o : (Iterable) value) { - JdbcValue elementJdbcValue = converter.writeJdbcValue(o, actualType.getType(), - JdbcUtil.targetSqlTypeFor(JdbcColumnTypes.INSTANCE.resolvePrimitiveType(actualType.getType()))); + JdbcValue elementJdbcValue = converter.writeJdbcValue(o, actualType, parameter.getActualSqlType()); if (jdbcType == null) { jdbcType = elementJdbcValue.getJdbcType(); } @@ -211,8 +221,8 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { jdbcValue = JdbcValue.of(mapped, jdbcType); } else { - jdbcValue = converter.writeJdbcValue(value, typeInformation.getType(), - JdbcUtil.targetSqlTypeFor(JdbcColumnTypes.INSTANCE.resolvePrimitiveType(typeInformation.getType()))); + SQLType sqlType = parameter.getSqlType(); + jdbcValue = converter.writeJdbcValue(value, typeInformation, sqlType); } SQLType jdbcType = jdbcValue.getJdbcType(); @@ -224,79 +234,38 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { } } - private String determineQuery() { + RowMapper determineRowMapper(ResultProcessor resultProcessor, boolean hasDynamicProjection) { - String query = getQueryMethod().getDeclaredQuery(); - - if (ObjectUtils.isEmpty(query)) { - throw new IllegalStateException(String.format("No query specified on %s", getQueryMethod().getName())); + if (cachedRowMapperFactory.isConfiguredRowMapper()) { + return cachedRowMapperFactory.getRowMapper(); } - return query; + if (hasDynamicProjection) { + + RowMapper rowMapperToUse = rowMapperFactory.create(resultProcessor.getReturnedType().getDomainType()); + + ResultProcessingConverter converter = new ResultProcessingConverter(resultProcessor, + this.converter.getMappingContext(), this.converter.getEntityInstantiators()); + return new ConvertingRowMapper<>(rowMapperToUse, converter); + } + + return cachedRowMapperFactory.getRowMapper(); } @Nullable - @SuppressWarnings({ "rawtypes", "unchecked" }) - ResultSetExtractor determineResultSetExtractor(@Nullable RowMapper rowMapper) { + ResultSetExtractor determineResultSetExtractor(Supplier> rowMapper) { - String resultSetExtractorRef = getQueryMethod().getResultSetExtractorRef(); + if (cachedResultSetExtractorFactory.isConfiguredResultSetExtractor()) { - if (!ObjectUtils.isEmpty(resultSetExtractorRef)) { + if (cachedResultSetExtractorFactory.requiresRowMapper() && !cachedRowMapperFactory.isConfiguredRowMapper()) { + return cachedResultSetExtractorFactory.getResultSetExtractor(rowMapper); + } - Assert.notNull(beanFactory, "When a ResultSetExtractorRef is specified the BeanFactory must not be null"); - - return (ResultSetExtractor) beanFactory.getBean(resultSetExtractorRef); + // configured ResultSetExtractor defaults to configured RowMapper in case both are configured + return cachedResultSetExtractorFactory.getResultSetExtractor(); } - Class resultSetExtractorClass = getQueryMethod().getResultSetExtractorClass(); - - if (isUnconfigured(resultSetExtractorClass, ResultSetExtractor.class)) { - return null; - } - - Constructor constructor = ClassUtils - .getConstructorIfAvailable(resultSetExtractorClass, RowMapper.class); - - if (constructor != null) { - return BeanUtils.instantiateClass(constructor, rowMapper); - } - - return BeanUtils.instantiateClass(resultSetExtractorClass); - } - - @Nullable - RowMapper determineRowMapper(@Nullable RowMapper defaultMapper, - Converter resultProcessingConverter, boolean hasDynamicProjection) { - - RowMapper rowMapperToUse = determineRowMapper(defaultMapper); - - if ((hasDynamicProjection || rowMapperToUse == defaultMapper) && rowMapperToUse != null) { - return new ConvertingRowMapper<>(rowMapperToUse, resultProcessingConverter); - } - - return rowMapperToUse; - } - - @SuppressWarnings("unchecked") - @Nullable - RowMapper determineRowMapper(@Nullable RowMapper defaultMapper) { - - String rowMapperRef = getQueryMethod().getRowMapperRef(); - - if (!ObjectUtils.isEmpty(rowMapperRef)) { - - Assert.notNull(beanFactory, "When a RowMapperRef is specified the BeanFactory must not be null"); - - return (RowMapper) beanFactory.getBean(rowMapperRef); - } - - Class rowMapperClass = getQueryMethod().getRowMapperClass(); - - if (isUnconfigured(rowMapperClass, RowMapper.class)) { - return (RowMapper) defaultMapper; - } - - return (RowMapper) BeanUtils.instantiateClass(rowMapperClass); + return null; } private static boolean isUnconfigured(@Nullable Class configuredClass, Class defaultClass) { @@ -306,4 +275,130 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } + + class CachedRowMapperFactory { + + private final Lazy> cachedRowMapper; + private final boolean configuredRowMapper; + private final @Nullable Constructor constructor; + + @SuppressWarnings("unchecked") + public CachedRowMapperFactory(Supplier> defaultMapper) { + + String rowMapperRef = getQueryMethod().getRowMapperRef(); + Class rowMapperClass = getQueryMethod().getRowMapperClass(); + + if (!ObjectUtils.isEmpty(rowMapperRef) && !isUnconfigured(rowMapperClass, RowMapper.class)) { + throw new IllegalArgumentException( + "Invalid RowMapper configuration. Configure either one but not both via @Query(rowMapperRef = …, rowMapperClass = …) for query method " + + getQueryMethod()); + } + + this.configuredRowMapper = !ObjectUtils.isEmpty(rowMapperRef) || !isUnconfigured(rowMapperClass, RowMapper.class); + this.constructor = rowMapperClass != null ? findPrimaryConstructor(rowMapperClass) : null; + this.cachedRowMapper = Lazy.of(() -> { + + if (!ObjectUtils.isEmpty(rowMapperRef)) { + + Assert.notNull(beanFactory, "When a RowMapperRef is specified the BeanFactory must not be null"); + + return (RowMapper) beanFactory.getBean(rowMapperRef); + } + + if (isUnconfigured(rowMapperClass, RowMapper.class)) { + return defaultMapper.get(); + } + + return (RowMapper) BeanUtils.instantiateClass(constructor); + }); + } + + public boolean isConfiguredRowMapper() { + return configuredRowMapper; + } + + public RowMapper getRowMapper() { + return cachedRowMapper.get(); + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + class CachedResultSetExtractorFactory { + + private final Lazy> cachedResultSetExtractor; + private final boolean configuredResultSetExtractor; + private final @Nullable Constructor rowMapperConstructor; + private final @Nullable Constructor constructor; + private final Function>, ResultSetExtractor> resultSetExtractorFactory; + + public CachedResultSetExtractorFactory(Supplier> resultSetExtractor) { + + String resultSetExtractorRef = getQueryMethod().getResultSetExtractorRef(); + Class resultSetExtractorClass = getQueryMethod().getResultSetExtractorClass(); + + if (!ObjectUtils.isEmpty(resultSetExtractorRef) + && !isUnconfigured(resultSetExtractorClass, ResultSetExtractor.class)) { + throw new IllegalArgumentException( + "Invalid ResultSetExtractor configuration. Configure either one but not both via @Query(resultSetExtractorRef = …, resultSetExtractorClass = …) for query method " + + getQueryMethod()); + } + + this.configuredResultSetExtractor = !ObjectUtils.isEmpty(resultSetExtractorRef) + || !isUnconfigured(resultSetExtractorClass, ResultSetExtractor.class); + + this.rowMapperConstructor = resultSetExtractorClass != null + ? ClassUtils.getConstructorIfAvailable(resultSetExtractorClass, RowMapper.class) + : null; + this.constructor = resultSetExtractorClass != null ? findPrimaryConstructor(resultSetExtractorClass) : null; + this.resultSetExtractorFactory = rowMapper -> { + + if (!ObjectUtils.isEmpty(resultSetExtractorRef)) { + + Assert.notNull(beanFactory, "When a ResultSetExtractorRef is specified the BeanFactory must not be null"); + + return (ResultSetExtractor) beanFactory.getBean(resultSetExtractorRef); + } + + if (isUnconfigured(resultSetExtractorClass, ResultSetExtractor.class)) { + throw new UnsupportedOperationException("This should not happen"); + } + + if (rowMapperConstructor != null) { + return BeanUtils.instantiateClass(rowMapperConstructor, rowMapper.get()); + } + + return BeanUtils.instantiateClass(constructor); + }; + + this.cachedResultSetExtractor = Lazy.of(() -> resultSetExtractorFactory.apply(resultSetExtractor)); + } + + public boolean isConfiguredResultSetExtractor() { + return configuredResultSetExtractor; + } + + public ResultSetExtractor getResultSetExtractor() { + return cachedResultSetExtractor.get(); + } + + public ResultSetExtractor getResultSetExtractor(Supplier> rowMapperSupplier) { + return resultSetExtractorFactory.apply(rowMapperSupplier); + } + + public boolean requiresRowMapper() { + return rowMapperConstructor != null; + } + } + + @Nullable + static Constructor findPrimaryConstructor(Class clazz) { + try { + return clazz.getDeclaredConstructor(); + } catch (NoSuchMethodException ex) { + return BeanUtils.findPrimaryConstructor(clazz); + + } catch (LinkageError err) { + throw new BeanInstantiationException(clazz, "Unresolvable class definition", err); + } + } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java index bbfdd96c..8cccbab3 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java @@ -32,6 +32,8 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; + +import org.springframework.beans.factory.BeanFactory; import org.springframework.core.convert.converter.Converter; import org.springframework.dao.DataAccessException; import org.springframework.data.convert.ReadingConverter; @@ -59,6 +61,7 @@ import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.ReflectionUtils; /** @@ -106,7 +109,7 @@ class StringBasedJdbcQueryUnitTests { JdbcQueryMethod queryMethod = createMethod("findAll"); StringBasedJdbcQuery query = createQuery(queryMethod); - assertThat(query.determineRowMapper(defaultRowMapper)).isEqualTo(defaultRowMapper); + assertThat(query.determineRowMapper(queryMethod.getResultProcessor(), false)).isEqualTo(defaultRowMapper); } @Test // DATAJDBC-165, DATAJDBC-318 @@ -115,7 +118,7 @@ class StringBasedJdbcQueryUnitTests { JdbcQueryMethod queryMethod = createMethod("findAllWithCustomRowMapper"); StringBasedJdbcQuery query = createQuery(queryMethod); - assertThat(query.determineRowMapper(defaultRowMapper)).isInstanceOf(CustomRowMapper.class); + assertThat(query.determineRowMapper(queryMethod.getResultProcessor(), false)).isInstanceOf(CustomRowMapper.class); } @Test // DATAJDBC-290 @@ -124,12 +127,90 @@ class StringBasedJdbcQueryUnitTests { JdbcQueryMethod queryMethod = createMethod("findAllWithCustomResultSetExtractor"); StringBasedJdbcQuery query = createQuery(queryMethod); - ResultSetExtractor resultSetExtractor = query.determineResultSetExtractor(defaultRowMapper); + ResultSetExtractor resultSetExtractor1 = query.determineResultSetExtractor(() -> defaultRowMapper); + ResultSetExtractor resultSetExtractor2 = query.determineResultSetExtractor(() -> defaultRowMapper); - assertThat(resultSetExtractor) // + assertThat(resultSetExtractor1) // .isInstanceOf(CustomResultSetExtractor.class) // .matches(crse -> ((CustomResultSetExtractor) crse).rowMapper == defaultRowMapper, "RowMapper is expected to be default."); + + assertThat(resultSetExtractor1).isNotSameAs(resultSetExtractor2); + } + + @Test // GH-1721 + void cachesCustomMapperAndExtractorInstances() { + + JdbcQueryMethod queryMethod = createMethod("findAllCustomRowMapperResultSetExtractor"); + StringBasedJdbcQuery query = createQuery(queryMethod); + + ResultSetExtractor resultSetExtractor1 = query.determineResultSetExtractor(() -> { + throw new UnsupportedOperationException(); + }); + + ResultSetExtractor resultSetExtractor2 = query.determineResultSetExtractor(() -> { + throw new UnsupportedOperationException(); + }); + + assertThat(resultSetExtractor1).isSameAs(resultSetExtractor2); + assertThat(resultSetExtractor1).extracting("rowMapper").isInstanceOf(CustomRowMapper.class); + + assertThat(ReflectionTestUtils.getField(resultSetExtractor1, "rowMapper")) + .isSameAs(ReflectionTestUtils.getField(resultSetExtractor2, "rowMapper")); + } + + @Test // GH-1721 + void obtainsCustomRowMapperRef() { + + BeanFactory beanFactory = mock(BeanFactory.class); + JdbcQueryMethod queryMethod = createMethod("findAllCustomRowMapperRef"); + StringBasedJdbcQuery query = createQuery(queryMethod); + query.setBeanFactory(beanFactory); + + CustomRowMapper customRowMapper = new CustomRowMapper(); + + when(beanFactory.getBean("CustomRowMapper")).thenReturn(customRowMapper); + + RowMapper rowMapper = query.determineRowMapper(queryMethod.getResultProcessor(), false); + ResultSetExtractor resultSetExtractor = query.determineResultSetExtractor(() -> { + throw new UnsupportedOperationException(); + }); + + assertThat(rowMapper).isSameAs(customRowMapper); + assertThat(resultSetExtractor).isNull(); + } + + @Test // GH-1721 + void obtainsCustomResultSetExtractorRef() { + + BeanFactory beanFactory = mock(BeanFactory.class); + JdbcQueryMethod queryMethod = createMethod("findAllCustomResultSetExtractorRef"); + StringBasedJdbcQuery query = createQuery(queryMethod); + query.setBeanFactory(beanFactory); + + CustomResultSetExtractor cre = new CustomResultSetExtractor(); + + when(beanFactory.getBean("CustomResultSetExtractor")).thenReturn(cre); + + RowMapper rowMapper = query.determineRowMapper(queryMethod.getResultProcessor(), false); + ResultSetExtractor resultSetExtractor = query.determineResultSetExtractor(() -> { + throw new UnsupportedOperationException(); + }); + + assertThat(rowMapper).isSameAs(defaultRowMapper); + assertThat(resultSetExtractor).isSameAs(cre); + } + + @Test // GH-1721 + void failsOnRowMapperRefAndClassDeclaration() { + assertThatIllegalArgumentException().isThrownBy(() -> createQuery(createMethod("invalidMapperRefAndClass"))) + .withMessageContaining("Invalid RowMapper configuration"); + } + + @Test // GH-1721 + void failsOnResultSetExtractorRefAndClassDeclaration() { + assertThatIllegalArgumentException().isThrownBy(() -> createQuery(createMethod("invalidExtractorRefAndClass"))) + .withMessageContaining("Invalid ResultSetExtractor configuration"); } @Test // DATAJDBC-290 @@ -139,7 +220,7 @@ class StringBasedJdbcQueryUnitTests { StringBasedJdbcQuery query = createQuery(queryMethod); ResultSetExtractor resultSetExtractor = query - .determineResultSetExtractor(query.determineRowMapper(defaultRowMapper)); + .determineResultSetExtractor(() -> query.determineRowMapper(queryMethod.getResultProcessor(), false)); assertThat(resultSetExtractor) // .isInstanceOf(CustomResultSetExtractor.class) // @@ -178,8 +259,8 @@ class StringBasedJdbcQueryUnitTests { assertThatThrownBy( () -> new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider)) - .isInstanceOf(UnsupportedOperationException.class) - .hasMessageContaining("Slice queries are not supported using string-based queries"); + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("Slice queries are not supported using string-based queries"); } @Test // GH-774 @@ -189,8 +270,8 @@ class StringBasedJdbcQueryUnitTests { assertThatThrownBy( () -> new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider)) - .isInstanceOf(UnsupportedOperationException.class) - .hasMessageContaining("Page queries are not supported using string-based queries"); + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("Page queries are not supported using string-based queries"); } @Test // GH-1654 @@ -200,7 +281,7 @@ class StringBasedJdbcQueryUnitTests { assertThatThrownBy( () -> new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider)) - .isInstanceOf(UnsupportedOperationException.class); + .isInstanceOf(UnsupportedOperationException.class); } @Test // GH-1212 @@ -329,6 +410,24 @@ class StringBasedJdbcQueryUnitTests { @Query(value = "some sql statement", resultSetExtractorClass = CustomResultSetExtractor.class) Stream findAllWithStreamReturnTypeAndResultSetExtractor(); + @Query(value = "some sql statement", rowMapperClass = CustomRowMapper.class, + resultSetExtractorClass = CustomResultSetExtractor.class) + Stream findAllCustomRowMapperResultSetExtractor(); + + @Query(value = "some sql statement", rowMapperRef = "CustomRowMapper") + Stream findAllCustomRowMapperRef(); + + @Query(value = "some sql statement", resultSetExtractorRef = "CustomResultSetExtractor") + Stream findAllCustomResultSetExtractorRef(); + + @Query(value = "some sql statement", rowMapperRef = "CustomResultSetExtractor", + rowMapperClass = CustomRowMapper.class) + Stream invalidMapperRefAndClass(); + + @Query(value = "some sql statement", resultSetExtractorRef = "CustomResultSetExtractor", + resultSetExtractorClass = CustomResultSetExtractor.class) + Stream invalidExtractorRefAndClass(); + List noAnnotation(); @Query(value = "some sql statement") diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java index c239ef17..9b8ae2cb 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java @@ -72,14 +72,15 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** - * {@link RelationalConverter} that uses a {@link MappingContext} to apply sophisticated mapping of domain objects from - * {@link RowDocument}. + * {@link org.springframework.data.relational.core.conversion.RelationalConverter} that uses a + * {@link org.springframework.data.mapping.context.MappingContext} to apply sophisticated mapping of domain objects from + * {@link org.springframework.data.relational.domain.RowDocument}. * * @author Mark Paluch * @author Jens Schauder * @author Chirag Tailor * @author Vincent Galloy - * @see MappingContext + * @see org.springframework.data.mapping.context.MappingContext * @see SimpleTypeHolder * @see CustomConversions * @since 3.2 @@ -677,8 +678,25 @@ public class MappingRelationalConverter extends AbstractRelationalConverter impl if (getConversions().isSimpleType(value.getClass())) { - if (TypeInformation.OBJECT != type && getConversionService().canConvert(value.getClass(), type.getType())) { - value = getConversionService().convert(value, type.getType()); + Optional> customWriteTarget = getConversions().getCustomWriteTarget(type.getType()); + if (customWriteTarget.isPresent()) { + return getConversionService().convert(value, customWriteTarget.get()); + } + + if (TypeInformation.OBJECT != type) { + + if (type.getType().isAssignableFrom(value.getClass())) { + + if (value.getClass().isEnum()) { + return getPotentiallyConvertedSimpleWrite(value); + } + + return value; + } else { + if (getConversionService().canConvert(value.getClass(), type.getType())) { + value = getConversionService().convert(value, type.getType()); + } + } } return getPotentiallyConvertedSimpleWrite(value); @@ -700,7 +718,9 @@ public class MappingRelationalConverter extends AbstractRelationalConverter impl return writeValue(id, type); } - return getConversionService().convert(value, type.getType()); + return + + getConversionService().convert(value, type.getType()); } private Object writeArray(Object value, TypeInformation type) { @@ -1183,8 +1203,7 @@ public class MappingRelationalConverter extends AbstractRelationalConverter impl * @param delegate must not be {@literal null}. */ public ConverterAwareSpELExpressionParameterValueProvider(ConversionContext context, - SpELExpressionEvaluator evaluator, ConversionService conversionService, - ParameterValueProvider delegate) { + SpELExpressionEvaluator evaluator, ConversionService conversionService, ParameterValueProvider delegate) { super(evaluator, conversionService, delegate); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java index 1082e2d9..3808b2ba 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java @@ -23,6 +23,7 @@ import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.util.Lazy; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ConcurrentLruCache; /** * Represents a path within an aggregate starting from the aggregate root. @@ -43,6 +44,8 @@ class DefaultAggregatePath implements AggregatePath { private final Lazy columnInfo = Lazy.of(() -> ColumnInfo.of(this)); + private final ConcurrentLruCache nestedCache; + @SuppressWarnings("unchecked") DefaultAggregatePath(RelationalMappingContext context, PersistentPropertyPath path) { @@ -53,6 +56,7 @@ class DefaultAggregatePath implements AggregatePath { this.context = context; this.path = (PersistentPropertyPath) path; this.rootType = path.getBaseProperty().getOwner(); + this.nestedCache = new ConcurrentLruCache<>(32, this::doGetAggegatePath); } DefaultAggregatePath(RelationalMappingContext context, RelationalPersistentEntity rootType) { @@ -63,6 +67,7 @@ class DefaultAggregatePath implements AggregatePath { this.context = context; this.rootType = rootType; this.path = null; + this.nestedCache = new ConcurrentLruCache<>(32, this::doGetAggegatePath); } /** @@ -89,6 +94,10 @@ class DefaultAggregatePath implements AggregatePath { @Override public AggregatePath append(RelationalPersistentProperty property) { + return nestedCache.get(property); + } + + private AggregatePath doGetAggegatePath(RelationalPersistentProperty property) { PersistentPropertyPath newPath = isRoot() // ? context.getPersistentPropertyPath(property.getName(), rootType.getTypeInformation()) // diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java index e67b5aa9..7bf01c48 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DerivedSqlIdentifier.java @@ -36,12 +36,15 @@ class DerivedSqlIdentifier implements SqlIdentifier { private final String name; private final boolean quoted; + private final String toString; + private volatile @Nullable CachedSqlName sqlName; DerivedSqlIdentifier(String name, boolean quoted) { Assert.hasText(name, "A database object must have at least on name part."); this.name = name; this.quoted = quoted; + this.toString = quoted ? toSql(IdentifierProcessing.ANSI) : this.name; } @Override @@ -60,13 +63,19 @@ class DerivedSqlIdentifier implements SqlIdentifier { @Override public String toSql(IdentifierProcessing processing) { - String normalized = processing.standardizeLetterCase(name); + CachedSqlName sqlName = this.sqlName; + if (sqlName == null || sqlName.processing != processing) { - return quoted ? processing.quote(normalized) : normalized; + String normalized = processing.standardizeLetterCase(name); + this.sqlName = sqlName = new CachedSqlName(processing, quoted ? processing.quote(normalized) : normalized); + return sqlName.sqlName(); + } + + return sqlName.sqlName(); } @Override - @Deprecated(since="3.1", forRemoval = true) + @Deprecated(since = "3.1", forRemoval = true) public String getReference(IdentifierProcessing processing) { return this.name; } @@ -92,6 +101,9 @@ class DerivedSqlIdentifier implements SqlIdentifier { @Override public String toString() { - return quoted ? toSql(IdentifierProcessing.ANSI) : this.name; + return toString; + } + + record CachedSqlName(IdentifierProcessing processing, String sqlName) { } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSqlIdentifier.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSqlIdentifier.java index 71fb1a4a..c2f5916f 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSqlIdentifier.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/DefaultSqlIdentifier.java @@ -34,6 +34,8 @@ class DefaultSqlIdentifier implements SqlIdentifier { private final String name; private final boolean quoted; + private final String toString; + private volatile @Nullable CachedSqlName sqlName; DefaultSqlIdentifier(String name, boolean quoted) { @@ -41,6 +43,7 @@ class DefaultSqlIdentifier implements SqlIdentifier { this.name = name; this.quoted = quoted; + this.toString = quoted ? toSql(IdentifierProcessing.ANSI) : this.name; } @Override @@ -58,11 +61,19 @@ class DefaultSqlIdentifier implements SqlIdentifier { @Override public String toSql(IdentifierProcessing processing) { - return quoted ? processing.quote(name) : name; + + CachedSqlName sqlName = this.sqlName; + if (sqlName == null || sqlName.processing != processing) { + + this.sqlName = sqlName = new CachedSqlName(processing, quoted ? processing.quote(name) : name); + return sqlName.sqlName(); + } + + return sqlName.sqlName(); } @Override - @Deprecated(since="3.1", forRemoval = true) + @Deprecated(since = "3.1", forRemoval = true) public String getReference(IdentifierProcessing processing) { return name; } @@ -88,6 +99,9 @@ class DefaultSqlIdentifier implements SqlIdentifier { @Override public String toString() { - return quoted ? toSql(IdentifierProcessing.ANSI) : this.name; + return toString; + } + + record CachedSqlName(IdentifierProcessing processing, String sqlName) { } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java index 0ff927b3..c1a789d8 100755 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java @@ -17,6 +17,7 @@ package org.springframework.data.relational.repository.query; import java.lang.reflect.Method; import java.util.List; +import java.util.function.Function; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; @@ -41,7 +42,12 @@ public class RelationalParameters extends Parameters parameters) { + protected RelationalParameters(ParametersSource parametersSource, + Function parameterFactory) { + super(parametersSource, parameterFactory); + } + + protected RelationalParameters(List parameters) { super(parameters); } @@ -63,16 +69,16 @@ public class RelationalParameters extends Parameters typeInformation; /** * Creates a new {@link RelationalParameter}. * * @param parameter must not be {@literal null}. */ - RelationalParameter(MethodParameter parameter) { + protected RelationalParameter(MethodParameter parameter) { super(parameter); - this.parameter = parameter; + this.typeInformation = TypeInformation.fromMethodParameter(parameter); } public ResolvableType getResolvableType() { @@ -80,7 +86,7 @@ public class RelationalParameters extends Parameters getTypeInformation() { - return TypeInformation.fromMethodParameter(parameter); + return typeInformation; } } } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/BasicRelationalConverterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/BasicRelationalConverterUnitTests.java index 90910b0d..e29b6696 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/BasicRelationalConverterUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/BasicRelationalConverterUnitTests.java @@ -133,7 +133,7 @@ class BasicRelationalConverterUnitTests { @Test // DATAJDBC-516 void shouldConsiderWriteConverter() { - Object result = converter.writeValue(new MyValue("hello-world"), TypeInformation.of(MyValue.class)); + Object result = converter.writeValue(new MyValue("hello-world"), TypeInformation.of(String.class)); assertThat(result).isEqualTo("hello-world"); }