From 2bb0c5785031cad257a545fbb2c4e9630216b719 Mon Sep 17 00:00:00 2001 From: mipo256 Date: Fri, 21 Feb 2025 09:29:27 +0300 Subject: [PATCH 1/2] Polishing: JdbcQueryLookupStrategy Signed-off-by: mipo256 --- .../support/JdbcQueryLookupStrategy.java | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java index fee40edb19..bb828733cc 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java @@ -303,18 +303,13 @@ public static QueryLookupStrategy create(@Nullable Key key, ApplicationEventPubl LOG.debug(String.format("Using the queryLookupStrategy %s", keyToUse)); - switch (keyToUse) { - case CREATE: - return createQueryLookupStrategy; - case USE_DECLARED_QUERY: - return declaredQueryLookupStrategy; - case CREATE_IF_NOT_FOUND: - return new CreateIfNotFoundQueryLookupStrategy(publisher, callbacks, context, converter, dialect, - queryMappingConfiguration, operations, createQueryLookupStrategy, declaredQueryLookupStrategy, - delegate); - default: - throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s", key)); - } + return switch (keyToUse) { + case CREATE -> createQueryLookupStrategy; + case USE_DECLARED_QUERY -> declaredQueryLookupStrategy; + case CREATE_IF_NOT_FOUND -> new CreateIfNotFoundQueryLookupStrategy( + publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, + createQueryLookupStrategy, declaredQueryLookupStrategy, delegate); + }; } JdbcConverter getConverter() { From 8032c0ffa0ba29c8d6105362afe9c01ea9338bf2 Mon Sep 17 00:00:00 2001 From: mipo256 Date: Fri, 21 Feb 2025 16:13:44 +0300 Subject: [PATCH 2/2] rowmappers extraction Signed-off-by: mipo256 --- .../query/AbstractDelegatingRowMapper.java | 54 ++++++++ .../repository/query/AbstractJdbcQuery.java | 53 ++------ .../query/CallbackCapableRowMapper.java | 59 +++++++++ .../repository/query/ConvertingRowMapper.java | 43 ++++++ .../query/DefaultRowMapperFactory.java | 90 +++++++++++++ .../repository/query/PartTreeJdbcQuery.java | 7 +- .../repository/query/RowMapperFactory.java | 58 +++++++++ .../query/StringBasedJdbcQuery.java | 20 +-- .../BeanFactoryAwareRowMapperFactory.java | 76 +++++++++++ .../support/JdbcQueryLookupStrategy.java | 122 +++--------------- .../query/StringBasedJdbcQueryUnitTests.java | 7 +- 11 files changed, 424 insertions(+), 165 deletions(-) create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractDelegatingRowMapper.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/CallbackCapableRowMapper.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/ConvertingRowMapper.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/DefaultRowMapperFactory.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/RowMapperFactory.java create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/BeanFactoryAwareRowMapperFactory.java diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractDelegatingRowMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractDelegatingRowMapper.java new file mode 100644 index 0000000000..371fa77360 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractDelegatingRowMapper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2020-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. + * 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.ResultSet; +import java.sql.SQLException; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Abstract {@link RowMapper} that delegates the actual mapping logic to a {@link AbstractDelegatingRowMapper#delegate delegate} + * + * @author Mikhail Polivakha + */ +public abstract class AbstractDelegatingRowMapper implements RowMapper { + + private final RowMapper delegate; + + protected AbstractDelegatingRowMapper(RowMapper delegate) { + Assert.notNull(delegate, "Delegating RowMapper cannot be null"); + + this.delegate = delegate; + } + + @Override + public T mapRow(ResultSet rs, int rowNum) throws SQLException { + T intermediate = delegate.mapRow(rs, rowNum); + return postProcessMapping(intermediate); + } + + /** + * The post-processing callback for implementations. + * + * @return the mapped entity after applying post-processing logic + */ + protected T postProcessMapping(@Nullable T object) { + return object; + } +} 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 100e7e44f4..ca236e5284 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 @@ -153,60 +153,25 @@ private JdbcQueryExecution createSingleReadingQueryExecution(ResultSetExt * Factory to create a {@link RowMapper} for a given class. * * @since 2.3 + * @deprecated Use {@link org.springframework.data.jdbc.repository.query.RowMapperFactory} instead */ - public interface RowMapperFactory { - - /** - * Create a {@link RowMapper} based on the expected return type passed in as an argument. - * - * @param result must not be {@code null}. - * @return a {@code RowMapper} producing instances of {@code result}. - */ - RowMapper create(Class result); - - /** - * Obtain a {@code RowMapper} from some other source, typically a {@link org.springframework.beans.factory.BeanFactory}. - * - * @param reference must not be {@code null}. - * @since 3.4 - */ - default RowMapper getRowMapper(String reference) { - throw new UnsupportedOperationException("getRowMapper is not supported"); - } - - /** - * Obtain a {@code ResultSetExtractor} from some other source, typically a {@link org.springframework.beans.factory.BeanFactory}. - * - * @param reference must not be {@code null}. - * @since 3.4 - */ - default ResultSetExtractor getResultSetExtractor(String reference) { - throw new UnsupportedOperationException("getResultSetExtractor is not supported"); - } - } + @Deprecated(forRemoval = true, since = "3.4.4") + public interface RowMapperFactory extends org.springframework.data.jdbc.repository.query.RowMapperFactory { } /** * Delegating {@link RowMapper} that reads a row into {@code T} and converts it afterwards into {@code Object}. * * @param * @since 2.3 + * @deprecated use {@link org.springframework.data.jdbc.repository.query.ConvertingRowMapper} instead */ - protected static class ConvertingRowMapper implements RowMapper { - - private final RowMapper delegate; - private final Converter converter; + @Deprecated(forRemoval = true, since = "3.4.4") + protected static class ConvertingRowMapper extends + org.springframework.data.jdbc.repository.query.ConvertingRowMapper { + @SuppressWarnings("unchecked") public ConvertingRowMapper(RowMapper delegate, Converter converter) { - this.delegate = delegate; - this.converter = converter; - } - - @Override - public Object mapRow(ResultSet rs, int rowNum) throws SQLException { - - T object = delegate.mapRow(rs, rowNum); - - return object == null ? null : converter.convert(object); + super((RowMapper) delegate, converter); } } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/CallbackCapableRowMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/CallbackCapableRowMapper.java new file mode 100644 index 0000000000..69165653db --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/CallbackCapableRowMapper.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020-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. + * 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.ResultSet; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.mapping.callback.EntityCallbacks; +import org.springframework.data.relational.core.mapping.event.AfterConvertCallback; +import org.springframework.data.relational.core.mapping.event.AfterConvertEvent; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.lang.Nullable; + +/** + * Delegating {@link RowMapper} implementation that applies post-processing logic + * after the {@link RowMapper#mapRow(ResultSet, int)}. In particular, it emits the + * {@link AfterConvertEvent} event and invokes the {@link AfterConvertCallback} callbacks. + * + * @author Mark Paluch + * @author Mikhail Polivakha + */ +public class CallbackCapableRowMapper extends AbstractDelegatingRowMapper { + + private final ApplicationEventPublisher publisher; + private final @Nullable EntityCallbacks callbacks; + + public CallbackCapableRowMapper(RowMapper delegate, ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks) { + super(delegate); + this.publisher = publisher; + this.callbacks = callbacks; + } + + @Override + public T postProcessMapping(@Nullable T object) { + if (object != null) { + + publisher.publishEvent(new AfterConvertEvent<>(object)); + + if (callbacks != null) { + return callbacks.callback(AfterConvertCallback.class, object); + } + + } + return object; + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/ConvertingRowMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/ConvertingRowMapper.java new file mode 100644 index 0000000000..2f3c6e6da8 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/ConvertingRowMapper.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020-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. + * 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 org.springframework.core.convert.converter.Converter; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.lang.Nullable; + +/** + * Delegating {@link RowMapper} that reads a row into {@code T} and converts it afterwards into {@code Object}. + * + * @author Mark Paluch + * @author Mikhail Polivakha + * + * @since 2.3 + */ +public class ConvertingRowMapper extends AbstractDelegatingRowMapper { + + private final Converter converter; + + public ConvertingRowMapper(RowMapper delegate, Converter converter) { + super(delegate); + this.converter = converter; + } + + @Override + public Object postProcessMapping(@Nullable Object object) { + return object != null ? converter.convert(object) : null; + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/DefaultRowMapperFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/DefaultRowMapperFactory.java new file mode 100644 index 0000000000..06a7cc183f --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/DefaultRowMapperFactory.java @@ -0,0 +1,90 @@ +/* + * Copyright 2020-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. + * 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 org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.jdbc.core.convert.EntityRowMapper; +import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.repository.QueryMappingConfiguration; +import org.springframework.data.mapping.callback.EntityCallbacks; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.SingleColumnRowMapper; + +/** + * Default implementation of {@link RowMapperFactory}. Honors the custom mappings defined + * in {@link QueryMappingConfiguration}. + *

+ * This implementation is not capable of loading the {@link RowMapper} or {@link ResultSetExtractor} + * by reference via corresponding methods from {@link RowMapperFactory}. + * + * @implNote Public APIs of this class are thread-safe. + * @author Mikhail Polivakha + */ +public class DefaultRowMapperFactory implements RowMapperFactory { + + private final RelationalMappingContext context; + private final JdbcConverter converter; + private final QueryMappingConfiguration queryMappingConfiguration; + private final EntityCallbacks entityCallbacks; + private final ApplicationEventPublisher publisher; + + public DefaultRowMapperFactory( + RelationalMappingContext context, + JdbcConverter converter, + QueryMappingConfiguration queryMappingConfiguration, + EntityCallbacks entityCallbacks, + ApplicationEventPublisher publisher + ) { + this.context = context; + this.converter = converter; + this.queryMappingConfiguration = queryMappingConfiguration; + this.entityCallbacks = entityCallbacks; + this.publisher = publisher; + } + + @Override + @SuppressWarnings("unchecked") + public RowMapper create(Class returnedObjectType) { + + RelationalPersistentEntity persistentEntity = context.getPersistentEntity(returnedObjectType); + + if (persistentEntity == null) { + return (RowMapper) SingleColumnRowMapper.newInstance(returnedObjectType, + converter.getConversionService()); + } + + return (RowMapper) determineDefaultMapper(returnedObjectType); + } + + private RowMapper determineDefaultMapper(Class returnedObjectType) { + + RowMapper configuredQueryMapper = queryMappingConfiguration.getRowMapper(returnedObjectType); + + if (configuredQueryMapper != null) { + return configuredQueryMapper; + } + + EntityRowMapper defaultEntityRowMapper = new EntityRowMapper<>( // + context.getRequiredPersistentEntity(returnedObjectType), // + converter // + ); + + return new CallbackCapableRowMapper<>(defaultEntityRowMapper, publisher, entityCallbacks); + } +} 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 a40056e72d..1d542a74fe 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 @@ -24,7 +24,6 @@ import java.util.function.Function; import java.util.function.LongSupplier; import java.util.function.Supplier; -import java.util.stream.Stream; import org.springframework.core.convert.converter.Converter; import org.springframework.data.domain.Pageable; @@ -98,7 +97,7 @@ public PartTreeJdbcQuery(RelationalMappingContext context, JdbcQueryMethod query * @since 2.3 */ public PartTreeJdbcQuery(RelationalMappingContext context, JdbcQueryMethod queryMethod, Dialect dialect, - JdbcConverter converter, NamedParameterJdbcOperations operations, RowMapperFactory rowMapperFactory) { + JdbcConverter converter, NamedParameterJdbcOperations operations, org.springframework.data.jdbc.repository.query.RowMapperFactory rowMapperFactory) { super(queryMethod, operations); @@ -292,7 +291,7 @@ class CachedRowMapperFactory implements Supplier> { private final Lazy> rowMapper; private final Function> rowMapperFunction; - public CachedRowMapperFactory(PartTree tree, RowMapperFactory rowMapperFactory, RelationalConverter converter, + public CachedRowMapperFactory(PartTree tree, org.springframework.data.jdbc.repository.query.RowMapperFactory rowMapperFactory, RelationalConverter converter, ResultProcessor defaultResultProcessor) { this.rowMapperFunction = processor -> { @@ -302,7 +301,7 @@ public CachedRowMapperFactory(PartTree tree, RowMapperFactory rowMapperFactory, } Converter resultProcessingConverter = new ResultProcessingConverter(processor, converter.getMappingContext(), converter.getEntityInstantiators()); - return new ConvertingRowMapper<>(rowMapperFactory.create(processor.getReturnedType().getDomainType()), + return new org.springframework.data.jdbc.repository.query.ConvertingRowMapper(rowMapperFactory.create(processor.getReturnedType().getDomainType()), resultProcessingConverter); }; diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/RowMapperFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/RowMapperFactory.java new file mode 100644 index 0000000000..9a93d5634f --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/RowMapperFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020-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. + * 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 org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowMapper; + +/** + * Factory to create a {@link RowMapper} for a given class. + * + * @author Jens Schauder + * @author Mikhail Polivakha + * + * @since 2.3 + */ +public interface RowMapperFactory { + + /** + * Obtain a {@link RowMapper} based on the expected return type passed in as an argument. + * + * @param result must not be {@code null}. + * @return a {@code RowMapper} producing instances of {@code result}. + */ + RowMapper create(Class result); + + /** + * Obtain a {@link RowMapper} from some other source, typically a {@link org.springframework.beans.factory.BeanFactory}. + * + * @param reference must not be {@code null}. + * @since 3.4 + */ + default RowMapper getRowMapper(String reference) { + throw new UnsupportedOperationException("getRowMapper by reference is not supported"); + } + + /** + * Obtain a {@code ResultSetExtractor} from some other source, typically a {@link org.springframework.beans.factory.BeanFactory}. + * + * @param reference must not be {@code null}. + * @since 3.4 + */ + default ResultSetExtractor getResultSetExtractor(String reference) { + throw new UnsupportedOperationException("getResultSetExtractor by reference is not supported"); + } +} 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 7c4ff0d78c..e8dde20285 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 @@ -83,7 +83,7 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { private final static String LOCKING_IS_NOT_SUPPORTED = "Currently, @Lock is supported only on derived queries. In other words, for queries created with @Query, the locking condition specified with @Lock does nothing"; private static final Log LOG = LogFactory.getLog(StringBasedJdbcQuery.class); private final JdbcConverter converter; - private final RowMapperFactory rowMapperFactory; + private final org.springframework.data.jdbc.repository.query.RowMapperFactory rowMapperFactory; private final ValueExpressionQueryRewriter.ParsedQuery parsedQuery; private final String query; @@ -110,7 +110,7 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera /** * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} - * and {@link RowMapperFactory}. + * and {@link org.springframework.data.jdbc.repository.query.RowMapperFactory}. * * @param queryMethod must not be {@literal null}. * @param operations must not be {@literal null}. @@ -122,7 +122,7 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera */ @Deprecated(since = "3.4") public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, - RowMapperFactory rowMapperFactory, JdbcConverter converter, + org.springframework.data.jdbc.repository.query.RowMapperFactory rowMapperFactory, JdbcConverter converter, QueryMethodEvaluationContextProvider evaluationContextProvider) { this(queryMethod.getRequiredQuery(), queryMethod, operations, rowMapperFactory, converter, evaluationContextProvider); @@ -130,7 +130,7 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera /** * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} - * and {@link RowMapperFactory}. + * and {@link org.springframework.data.jdbc.repository.query.RowMapperFactory}. * * @param queryMethod must not be {@literal null}. * @param operations must not be {@literal null}. @@ -140,13 +140,13 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera * @since 3.4 */ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, - RowMapperFactory rowMapperFactory, JdbcConverter converter, ValueExpressionDelegate delegate) { + org.springframework.data.jdbc.repository.query.RowMapperFactory rowMapperFactory, JdbcConverter converter, ValueExpressionDelegate delegate) { this(queryMethod.getRequiredQuery(), queryMethod, operations, rowMapperFactory, converter, delegate); } /** * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} - * and {@link RowMapperFactory}. + * and {@link org.springframework.data.jdbc.repository.query.RowMapperFactory}. * * @param query must not be {@literal null} or empty. * @param queryMethod must not be {@literal null}. @@ -157,7 +157,7 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera * @since 3.4 */ public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, - RowMapperFactory rowMapperFactory, JdbcConverter converter, ValueExpressionDelegate delegate) { + org.springframework.data.jdbc.repository.query.RowMapperFactory rowMapperFactory, JdbcConverter converter, ValueExpressionDelegate delegate) { super(queryMethod, operations); Assert.hasText(query, "Query must not be null or empty"); Assert.notNull(rowMapperFactory, "RowMapperFactory must not be null"); @@ -199,7 +199,7 @@ public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedPara /** * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} - * and {@link RowMapperFactory}. + * and {@link org.springframework.data.jdbc.repository.query.RowMapperFactory}. * * @param query must not be {@literal null} or empty. * @param queryMethod must not be {@literal null}. @@ -212,7 +212,7 @@ public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedPara */ @Deprecated(since = "3.4") public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, - RowMapperFactory rowMapperFactory, JdbcConverter converter, + org.springframework.data.jdbc.repository.query.RowMapperFactory rowMapperFactory, JdbcConverter converter, QueryMethodEvaluationContextProvider evaluationContextProvider) { this(query, queryMethod, operations, rowMapperFactory, converter, new CachingValueExpressionDelegate( new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), rootObject -> evaluationContextProvider @@ -386,7 +386,7 @@ RowMapper determineRowMapper(ResultProcessor resultProcessor, boolean ha ResultProcessingConverter converter = new ResultProcessingConverter(resultProcessor, this.converter.getMappingContext(), this.converter.getEntityInstantiators()); - return new ConvertingRowMapper<>(rowMapperToUse, converter); + return new org.springframework.data.jdbc.repository.query.ConvertingRowMapper(rowMapperToUse, converter); } return cachedRowMapperFactory.getRowMapper(); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/BeanFactoryAwareRowMapperFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/BeanFactoryAwareRowMapperFactory.java new file mode 100644 index 0000000000..92c2b76a0c --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/BeanFactoryAwareRowMapperFactory.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020-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. + * 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.support; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.repository.QueryMappingConfiguration; +import org.springframework.data.jdbc.repository.query.DefaultRowMapperFactory; +import org.springframework.data.jdbc.repository.query.RowMapperFactory; +import org.springframework.data.mapping.callback.EntityCallbacks; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.lang.Nullable; + +/** + * This {@link RowMapperFactory} implementation extends the {@link DefaultRowMapperFactory} + * by adding the capabilities to load {@link RowMapper} or {@link ResultSetExtractor} beans by + * their names in {@link BeanFactory}. + * + * @author Mark Paluch + * @author Jens Schauder + * @author Mikhail Polivakha + */ +@SuppressWarnings("unchecked") +public class BeanFactoryAwareRowMapperFactory extends DefaultRowMapperFactory { + + private final @Nullable BeanFactory beanFactory; + + public BeanFactoryAwareRowMapperFactory( + RelationalMappingContext context, + JdbcConverter converter, + QueryMappingConfiguration queryMappingConfiguration, + EntityCallbacks entityCallbacks, + ApplicationEventPublisher publisher, + @Nullable BeanFactory beanFactory + ) { + super(context, converter, queryMappingConfiguration, entityCallbacks, publisher); + + this.beanFactory = beanFactory; + } + + @Override + public RowMapper getRowMapper(String reference) { + if (beanFactory == null) { + throw new IllegalStateException( + "Cannot resolve RowMapper bean reference '" + reference + "'; BeanFactory is not configured."); + } + + return beanFactory.getBean(reference, RowMapper.class); + } + + @Override + public ResultSetExtractor getResultSetExtractor(String reference) { + if (beanFactory == null) { + throw new IllegalStateException( + "Cannot resolve ResultSetExtractor bean reference '" + reference + "'; BeanFactory is not configured."); + } + + return beanFactory.getBean(reference, ResultSetExtractor.class); + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java index bb828733cc..e477431797 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java @@ -16,36 +16,28 @@ package org.springframework.data.jdbc.repository.support; import java.lang.reflect.Method; -import java.sql.ResultSet; -import java.sql.SQLException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanFactory; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.jdbc.core.convert.EntityRowMapper; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; -import org.springframework.data.jdbc.repository.query.AbstractJdbcQuery; +import org.springframework.data.jdbc.repository.query.DefaultRowMapperFactory; import org.springframework.data.jdbc.repository.query.JdbcQueryMethod; import org.springframework.data.jdbc.repository.query.PartTreeJdbcQuery; +import org.springframework.data.jdbc.repository.query.RowMapperFactory; import org.springframework.data.jdbc.repository.query.StringBasedJdbcQuery; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.mapping.RelationalMappingContext; -import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; -import org.springframework.data.relational.core.mapping.event.AfterConvertCallback; -import org.springframework.data.relational.core.mapping.event.AfterConvertEvent; import org.springframework.data.relational.repository.support.RelationalQueryLookupStrategy; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.jdbc.core.ResultSetExtractor; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.core.SingleColumnRowMapper; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -62,6 +54,7 @@ * @author Hebert Coelho * @author Diego Krupitza * @author Christopher Klein + * @author Mikhail Polivakha */ abstract class JdbcQueryLookupStrategy extends RelationalQueryLookupStrategy { @@ -109,6 +102,8 @@ public RelationalMappingContext getMappingContext() { */ static class CreateQueryLookupStrategy extends JdbcQueryLookupStrategy { + private final RowMapperFactory rowMapperFactory; + CreateQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks, RelationalMappingContext context, JdbcConverter converter, Dialect dialect, QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, @@ -116,6 +111,8 @@ static class CreateQueryLookupStrategy extends JdbcQueryLookupStrategy { super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, delegate); + + this.rowMapperFactory = new DefaultRowMapperFactory(getMappingContext(), getConverter(), getQueryMappingConfiguration(), getCallbacks(), getPublisher()); } @Override @@ -124,8 +121,7 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository JdbcQueryMethod queryMethod = getJdbcQueryMethod(method, repositoryMetadata, projectionFactory, namedQueries); - return new PartTreeJdbcQuery(getMappingContext(), queryMethod, getDialect(), getConverter(), getOperations(), - this::createMapper); + return new PartTreeJdbcQuery(getMappingContext(), queryMethod, getDialect(), getConverter(), getOperations(), rowMapperFactory); } } @@ -138,7 +134,7 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository */ static class DeclaredQueryLookupStrategy extends JdbcQueryLookupStrategy { - private final AbstractJdbcQuery.RowMapperFactory rowMapperFactory; + private final RowMapperFactory rowMapperFactory; DeclaredQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks, RelationalMappingContext context, JdbcConverter converter, Dialect dialect, @@ -147,7 +143,7 @@ static class DeclaredQueryLookupStrategy extends JdbcQueryLookupStrategy { super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, delegate); - this.rowMapperFactory = new BeanFactoryRowMapperFactory(beanfactory); + this.rowMapperFactory = new BeanFactoryAwareRowMapperFactory(context, converter, queryMappingConfiguration, callbacks, publisher, beanfactory); } @Override @@ -172,44 +168,6 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository throw new IllegalStateException( String.format("Did neither find a NamedQuery nor an annotated query for method %s", method)); } - - @SuppressWarnings("unchecked") - private class BeanFactoryRowMapperFactory implements AbstractJdbcQuery.RowMapperFactory { - - private final @Nullable BeanFactory beanFactory; - - BeanFactoryRowMapperFactory(@Nullable BeanFactory beanFactory) { - this.beanFactory = beanFactory; - } - - @Override - public RowMapper create(Class result) { - return createMapper(result); - } - - @Override - public RowMapper getRowMapper(String reference) { - - if (beanFactory == null) { - throw new IllegalStateException( - "Cannot resolve RowMapper bean reference '" + reference + "'; BeanFactory is not configured."); - } - - return beanFactory.getBean(reference, RowMapper.class); - } - - @Override - public ResultSetExtractor getResultSetExtractor(String reference) { - - if (beanFactory == null) { - throw new IllegalStateException( - "Cannot resolve ResultSetExtractor bean reference '" + reference + "'; BeanFactory is not configured."); - } - - return beanFactory.getBean(reference, ResultSetExtractor.class); - } - } - } /** @@ -320,57 +278,15 @@ NamedParameterJdbcOperations getOperations() { return operations; } - @SuppressWarnings("unchecked") - RowMapper createMapper(Class returnedObjectType) { - - RelationalPersistentEntity persistentEntity = getMappingContext().getPersistentEntity(returnedObjectType); - - if (persistentEntity == null) { - return (RowMapper) SingleColumnRowMapper.newInstance(returnedObjectType, - converter.getConversionService()); - } - - return (RowMapper) determineDefaultMapper(returnedObjectType); - } - - private RowMapper determineDefaultMapper(Class returnedObjectType) { - - RowMapper configuredQueryMapper = queryMappingConfiguration.getRowMapper(returnedObjectType); - - if (configuredQueryMapper != null) - return configuredQueryMapper; - - EntityRowMapper defaultEntityRowMapper = new EntityRowMapper<>( // - getMappingContext().getRequiredPersistentEntity(returnedObjectType), // - converter // - ); - - return new PostProcessingRowMapper<>(defaultEntityRowMapper); - } - - class PostProcessingRowMapper implements RowMapper { - - private final RowMapper delegate; + QueryMappingConfiguration getQueryMappingConfiguration() { + return queryMappingConfiguration; + } - PostProcessingRowMapper(RowMapper delegate) { - this.delegate = delegate; - } - - @Override - public T mapRow(ResultSet rs, int rowNum) throws SQLException { - - T entity = delegate.mapRow(rs, rowNum); - - if (entity != null) { - - publisher.publishEvent(new AfterConvertEvent<>(entity)); + EntityCallbacks getCallbacks() { + return callbacks; + } - if (callbacks != null) { - return callbacks.callback(AfterConvertCallback.class, entity); - } - } - - return entity; - } - } + ApplicationEventPublisher getPublisher() { + return publisher; + } } 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 c0d9cd5bf2..bd964e78a2 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 @@ -633,7 +633,7 @@ public Object getRootObject() { } } - private class StubRowMapperFactory implements AbstractJdbcQuery.RowMapperFactory { + private class StubRowMapperFactory implements RowMapperFactory { private final String preparedReference; private final Object value; @@ -643,7 +643,6 @@ private class StubRowMapperFactory implements AbstractJdbcQuery.RowMapperFactory this.value = value; } - @Override public RowMapper create(Class result) { return defaultRowMapper; } @@ -654,7 +653,7 @@ public RowMapper getRowMapper(String reference) { if (preparedReference.equals(reference)) { return (RowMapper) value; } - return AbstractJdbcQuery.RowMapperFactory.super.getRowMapper(reference); + return RowMapperFactory.super.getRowMapper(reference); } @Override @@ -663,7 +662,7 @@ public ResultSetExtractor getResultSetExtractor(String reference) { if (preparedReference.equals(reference)) { return (ResultSetExtractor) value; } - return AbstractJdbcQuery.RowMapperFactory.super.getResultSetExtractor(reference); + return RowMapperFactory.super.getResultSetExtractor(reference); } } }