From f5f5f332fc39545e8a7b9511b53c897c2e5f9b0b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 17 May 2021 15:37:09 +0200 Subject: [PATCH 1/2] Add support for repository query method projections. JDBC repository methods now support interface and DTO projections by specifying either the projection type as return type or using generics and providing a Class parameter to query methods. --- .../repository/query/AbstractJdbcQuery.java | 68 +++++++++++-- .../query/JdbcCountQueryCreator.java | 6 +- .../repository/query/JdbcQueryCreator.java | 15 ++- .../repository/query/JdbcQueryExecution.java | 47 +++++++++ .../repository/query/PartTreeJdbcQuery.java | 74 ++++++++++---- .../query/StringBasedJdbcQuery.java | 75 +++++++++++--- .../support/JdbcQueryLookupStrategy.java | 18 ++-- .../JdbcRepositoryIntegrationTests.java | 63 ++++++++++++ .../query/PartTreeJdbcQueryUnitTests.java | 99 +++++++++++-------- .../conversion/BasicRelationalConverter.java | 5 + .../core/conversion/RelationalConverter.java | 9 ++ src/main/asciidoc/jdbc.adoc | 4 +- 12 files changed, 385 insertions(+), 98 deletions(-) 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 b5ec45aef6..cdbcf9c3f9 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 @@ -15,10 +15,15 @@ */ package org.springframework.data.jdbc.repository.query; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.List; +import org.springframework.core.convert.converter.Converter; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapperResultSetExtractor; @@ -43,23 +48,17 @@ public abstract class AbstractJdbcQuery implements RepositoryQuery { private final NamedParameterJdbcOperations operations; /** - * Creates a new {@link AbstractJdbcQuery} for the given {@link JdbcQueryMethod}, {@link NamedParameterJdbcOperations} - * and {@link RowMapper}. + * Creates a new {@link AbstractJdbcQuery} for the given {@link JdbcQueryMethod} and + * {@link NamedParameterJdbcOperations}. * * @param queryMethod must not be {@literal null}. * @param operations must not be {@literal null}. - * @param defaultRowMapper can be {@literal null} (only in case of a modifying query). */ - AbstractJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, - @Nullable RowMapper defaultRowMapper) { + AbstractJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations) { Assert.notNull(queryMethod, "Query method must not be null!"); Assert.notNull(operations, "NamedParameterJdbcOperations must not be null!"); - if (!queryMethod.isModifyingQuery()) { - Assert.notNull(defaultRowMapper, "Mapper must not be null!"); - } - this.queryMethod = queryMethod; this.operations = operations; } @@ -123,8 +122,59 @@ JdbcQueryExecution> collectionQuery(RowMapper rowMapper) { return getQueryExecution(new RowMapperResultSetExtractor<>(rowMapper)); } + /** + * Obtain the result type to read from {@link ResultProcessor}. + * + * @param resultProcessor + * @return + */ + protected Class resolveTypeToRead(ResultProcessor resultProcessor) { + + ReturnedType returnedType = resultProcessor.getReturnedType(); + + if (returnedType.getReturnedType().isAssignableFrom(returnedType.getDomainType())) { + return returnedType.getDomainType(); + } + // Slight deviation from R2DBC: Allow direct mapping into DTOs + return returnedType.isProjecting() && returnedType.getReturnedType().isInterface() ? returnedType.getDomainType() + : returnedType.getReturnedType(); + } + private JdbcQueryExecution getQueryExecution(ResultSetExtractor resultSetExtractor) { return (query, parameters) -> operations.query(query, parameters, resultSetExtractor); } + /** + * Factory to create a {@link RowMapper} for a given class. + * + * @since 2.3 + */ + public interface RowMapperFactory { + RowMapper create(Class result); + } + + /** + * Delegating {@link RowMapper} that reads a row into {@code T} and converts it afterwards into {@code Object}. + * + * @param + * @since 2.3 + */ + protected static class ConvertingRowMapper implements RowMapper { + + private final RowMapper delegate; + private final Converter converter; + + 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 converter.convert(object); + } + } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcCountQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcCountQueryCreator.java index f1557d0565..da89bc75aa 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcCountQueryCreator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcCountQueryCreator.java @@ -27,6 +27,7 @@ import org.springframework.data.relational.core.sql.Table; import org.springframework.data.relational.repository.query.RelationalEntityMetadata; import org.springframework.data.relational.repository.query.RelationalParameterAccessor; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.PartTree; /** @@ -38,8 +39,9 @@ class JdbcCountQueryCreator extends JdbcQueryCreator { JdbcCountQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect, - RelationalEntityMetadata entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery) { - super(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery); + RelationalEntityMetadata entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery, + ReturnedType returnedType) { + super(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery, returnedType); } @Override diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java index 67be6aa8b4..f10fc0d0b7 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java @@ -44,6 +44,7 @@ import org.springframework.data.relational.repository.query.RelationalParameterAccessor; import org.springframework.data.relational.repository.query.RelationalQueryCreator; import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; @@ -67,6 +68,7 @@ class JdbcQueryCreator extends RelationalQueryCreator { private final RelationalEntityMetadata entityMetadata; private final RenderContextFactory renderContextFactory; private final boolean isSliceQuery; + private final ReturnedType returnedType; /** * Creates new instance of this class with the given {@link PartTree}, {@link JdbcConverter}, {@link Dialect}, @@ -79,14 +81,17 @@ class JdbcQueryCreator extends RelationalQueryCreator { * @param entityMetadata relational entity metadata, must not be {@literal null}. * @param accessor parameter metadata provider, must not be {@literal null}. * @param isSliceQuery + * @param returnedType */ JdbcQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect, - RelationalEntityMetadata entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery) { + RelationalEntityMetadata entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery, + ReturnedType returnedType) { super(tree, accessor); Assert.notNull(converter, "JdbcConverter must not be null"); Assert.notNull(dialect, "Dialect must not be null"); Assert.notNull(entityMetadata, "Relational entity metadata must not be null"); + Assert.notNull(returnedType, "ReturnedType must not be null"); this.context = context; this.tree = tree; @@ -96,6 +101,7 @@ class JdbcQueryCreator extends RelationalQueryCreator { this.queryMapper = new QueryMapper(dialect, converter); this.renderContextFactory = new RenderContextFactory(dialect); this.isSliceQuery = isSliceQuery; + this.returnedType = returnedType; } /** @@ -241,6 +247,13 @@ private SelectBuilder.SelectJoin selectBuilder(Table table) { joinTables.add(join); } + if (returnedType.needsCustomConstruction()) { + if (!returnedType.getInputProperties() + .contains(extPath.getRequiredPersistentPropertyPath().getBaseProperty().getName())) { + continue; + } + } + Column column = getColumn(sqlContext, extPath); if (column != null) { columnExpressions.add(column); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryExecution.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryExecution.java index 5dc654dec1..4199c275ff 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryExecution.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryExecution.java @@ -15,8 +15,18 @@ */ package org.springframework.data.jdbc.repository.query; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.EntityInstantiators; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.repository.query.DtoInstantiatingConverter; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.data.util.Lazy; import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; /** * Interface specifying a query execution strategy. Implementations encapsulate information how to actually execute the @@ -37,4 +47,41 @@ interface JdbcQueryExecution { */ @Nullable T execute(String query, SqlParameterSource parameter); + + /** + * A {@link Converter} to post-process all source objects using the given {@link ResultProcessor}. + * + * @author Mark Paluch + * @since 2.3 + */ + class ResultProcessingConverter implements Converter { + + private final ResultProcessor processor; + private final Lazy> converter; + + ResultProcessingConverter(ResultProcessor processor, + MappingContext, ? extends RelationalPersistentProperty> mappingContext, + EntityInstantiators instantiators) { + this.processor = processor; + this.converter = Lazy.of(() -> new DtoInstantiatingConverter(processor.getReturnedType().getReturnedType(), + mappingContext, instantiators)); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + ReturnedType returnedType = processor.getReturnedType(); + + if (ClassUtils.isPrimitiveOrWrapper(returnedType.getReturnedType()) + || returnedType.getReturnedType().isInstance(source)) { + return source; + } + + return processor.processResult(source, converter.get()); + } + } } 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 ea361f539e..fccbe0a00c 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 @@ -15,12 +15,15 @@ */ package org.springframework.data.jdbc.repository.query; +import static org.springframework.data.jdbc.repository.query.JdbcQueryExecution.*; + import java.sql.ResultSet; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.LongSupplier; +import org.springframework.core.convert.converter.Converter; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; @@ -32,6 +35,8 @@ import org.springframework.data.relational.repository.query.RelationalParameterAccessor; import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor; import org.springframework.data.repository.query.Parameters; +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.jdbc.core.ResultSetExtractor; @@ -53,9 +58,8 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery { private final Parameters parameters; private final Dialect dialect; private final JdbcConverter converter; + private final RowMapperFactory rowMapperFactory; private final PartTree tree; - /** The execution for obtaining the bulk of the data. The execution may be decorated with further processing for handling sliced or paged queries */ - private final JdbcQueryExecution coreExecution; /** * Creates a new {@link PartTreeJdbcQuery}. @@ -69,26 +73,40 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery { */ public PartTreeJdbcQuery(RelationalMappingContext context, JdbcQueryMethod queryMethod, Dialect dialect, JdbcConverter converter, NamedParameterJdbcOperations operations, RowMapper rowMapper) { + this(context, queryMethod, dialect, converter, operations, it -> rowMapper); + } + + /** + * Creates a new {@link PartTreeJdbcQuery}. + * + * @param context must not be {@literal null}. + * @param queryMethod must not be {@literal null}. + * @param dialect must not be {@literal null}. + * @param converter must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param rowMapperFactory must not be {@literal null}. + * @since 2.3 + */ + public PartTreeJdbcQuery(RelationalMappingContext context, JdbcQueryMethod queryMethod, Dialect dialect, + JdbcConverter converter, NamedParameterJdbcOperations operations, RowMapperFactory rowMapperFactory) { - super(queryMethod, operations, rowMapper); + super(queryMethod, operations); Assert.notNull(context, "RelationalMappingContext must not be null"); Assert.notNull(queryMethod, "JdbcQueryMethod must not be null"); Assert.notNull(dialect, "Dialect must not be null"); Assert.notNull(converter, "JdbcConverter must not be null"); + Assert.notNull(rowMapperFactory, "RowMapperFactory must not be null"); this.context = context; this.parameters = queryMethod.getParameters(); this.dialect = dialect; this.converter = converter; + this.rowMapperFactory = rowMapperFactory; this.tree = new PartTree(queryMethod.getName(), queryMethod.getEntityInformation().getJavaType()); JdbcQueryCreator.validate(this.tree, this.parameters, this.converter.getMappingContext()); - ResultSetExtractor extractor = tree.isExistsProjection() ? (ResultSet::next) : null; - - this.coreExecution = queryMethod.isPageQuery() || queryMethod.isSliceQuery() ? collectionQuery(rowMapper) - : getQueryExecution(queryMethod, extractor, rowMapper); } private Sort getDynamicSort(RelationalParameterAccessor accessor) { @@ -104,30 +122,48 @@ public Object execute(Object[] values) { RelationalParametersParameterAccessor accessor = new RelationalParametersParameterAccessor(getQueryMethod(), values); - ParametrizedQuery query = createQuery(accessor); - JdbcQueryExecution execution = getDecoratedExecution(accessor); + + ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + ParametrizedQuery query = createQuery(accessor, processor.getReturnedType()); + JdbcQueryExecution execution = getQueryExecution(processor, accessor); return execution.execute(query.getQuery(), query.getParameterSource()); } - /** - * The decorated execution is the {@link #coreExecution} decorated with further processing for handling sliced or paged queries. - */ - private JdbcQueryExecution getDecoratedExecution(RelationalParametersParameterAccessor accessor) { + private JdbcQueryExecution getQueryExecution(ResultProcessor processor, + 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); + } + + JdbcQueryExecution queryExecution = getQueryMethod().isPageQuery() || getQueryMethod().isSliceQuery() + ? collectionQuery(rowMapper) + : getQueryExecution(getQueryMethod(), extractor, rowMapper); if (getQueryMethod().isSliceQuery()) { - return new SliceQueryExecution<>((JdbcQueryExecution>) this.coreExecution, accessor.getPageable()); + return new SliceQueryExecution<>((JdbcQueryExecution>) queryExecution, accessor.getPageable()); } if (getQueryMethod().isPageQuery()) { - return new PageQueryExecution<>((JdbcQueryExecution>) this.coreExecution, accessor.getPageable(), + return new PageQueryExecution<>((JdbcQueryExecution>) queryExecution, accessor.getPageable(), () -> { RelationalEntityMetadata entityMetadata = getQueryMethod().getEntityInformation(); JdbcCountQueryCreator queryCreator = new JdbcCountQueryCreator(context, tree, converter, dialect, - entityMetadata, accessor, false); + entityMetadata, accessor, false, processor.getReturnedType()); ParametrizedQuery countQuery = queryCreator.createQuery(Sort.unsorted()); Object count = singleObjectQuery((rs, i) -> rs.getLong(1)).execute(countQuery.getQuery(), @@ -137,15 +173,15 @@ private JdbcQueryExecution getDecoratedExecution(RelationalParametersParamete }); } - return this.coreExecution; + return queryExecution; } - protected ParametrizedQuery createQuery(RelationalParametersParameterAccessor accessor) { + protected ParametrizedQuery createQuery(RelationalParametersParameterAccessor accessor, ReturnedType returnedType) { RelationalEntityMetadata entityMetadata = getQueryMethod().getEntityInformation(); JdbcQueryCreator queryCreator = new JdbcQueryCreator(context, tree, converter, dialect, entityMetadata, accessor, - getQueryMethod().isSliceQuery()); + getQueryMethod().isSliceQuery(), returnedType); return queryCreator.createQuery(getDynamicSort(accessor)); } 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 365a8055c0..02b5e24e29 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 @@ -15,18 +15,24 @@ */ package org.springframework.data.jdbc.repository.query; +import static org.springframework.data.jdbc.repository.query.JdbcQueryExecution.*; + import java.lang.reflect.Constructor; import java.sql.JDBCType; 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.convert.JdbcValue; import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.repository.query.RelationalParameterAccessor; +import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor; import org.springframework.data.repository.query.Parameter; -import org.springframework.data.util.Lazy; +import org.springframework.data.repository.query.Parameters; +import org.springframework.data.repository.query.ResultProcessor; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; @@ -53,8 +59,8 @@ 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 JdbcQueryMethod queryMethod; - private final Lazy> executor; private final JdbcConverter converter; + private final RowMapperFactory rowMapperFactory; private BeanFactory beanFactory; /** @@ -67,11 +73,28 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { */ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, @Nullable RowMapper defaultRowMapper, JdbcConverter converter) { + this(queryMethod, operations, result -> (RowMapper) defaultRowMapper, converter); + } - super(queryMethod, operations, defaultRowMapper); + /** + * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} + * and {@link RowMapperFactory}. + * + * @param queryMethod must not be {@literal null}. + * @param operations must not be {@literal null}. + * @param rowMapperFactory must not be {@literal null}. + * @since 2.3 + */ + public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, + RowMapperFactory rowMapperFactory, JdbcConverter converter) { + + super(queryMethod, operations); + + Assert.notNull(rowMapperFactory, "RowMapperFactory must not be null"); this.queryMethod = queryMethod; this.converter = converter; + this.rowMapperFactory = rowMapperFactory; if (queryMethod.isSliceQuery()) { throw new UnsupportedOperationException( @@ -82,14 +105,6 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera throw new UnsupportedOperationException( "Page queries are not supported using string-based queries. Offending method: " + queryMethod); } - - executor = Lazy.of(() -> { - RowMapper rowMapper = determineRowMapper(defaultRowMapper); - return getQueryExecution( // - queryMethod, // - determineResultSetExtractor(rowMapper), // - rowMapper // - );}); } /* @@ -98,7 +113,21 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera */ @Override public Object execute(Object[] objects) { - return executor.get().execute(determineQuery(), this.bindParameters(objects)); + + RelationalParameterAccessor accessor = new RelationalParametersParameterAccessor(getQueryMethod(), objects); + ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + ResultProcessingConverter converter = new ResultProcessingConverter(processor, this.converter.getMappingContext(), + this.converter.getEntityInstantiators()); + + RowMapper rowMapper = determineRowMapper(rowMapperFactory.create(resolveTypeToRead(processor)), converter, + accessor.findDynamicProjection() != null); + + JdbcQueryExecution queryExecution = getQueryExecution(// + queryMethod, // + determineResultSetExtractor(rowMapper), // + rowMapper); + + return queryExecution.execute(determineQuery(), this.bindParameters(accessor)); } /* @@ -110,12 +139,15 @@ public JdbcQueryMethod getQueryMethod() { return queryMethod; } - private MapSqlParameterSource bindParameters(Object[] objects) { + private MapSqlParameterSource bindParameters(RelationalParameterAccessor accessor) { MapSqlParameterSource parameters = new MapSqlParameterSource(); - queryMethod.getParameters().getBindableParameters() - .forEach(p -> convertAndAddParameter(parameters, p, objects[p.getIndex()])); + Parameters bindableParameters = accessor.getBindableParameters(); + + for (Parameter bindableParameter : bindableParameters) { + convertAndAddParameter(parameters, bindableParameter, accessor.getBindableValue(bindableParameter.getIndex())); + } return parameters; } @@ -179,6 +211,19 @@ ResultSetExtractor determineResultSetExtractor(@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) { 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 291d2cf507..e9ee9d36a1 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 @@ -103,12 +103,11 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository try { if (namedQueries.hasQuery(queryMethod.getNamedQueryName()) || queryMethod.hasAnnotatedQuery()) { - RowMapper mapper = queryMethod.isModifyingQuery() ? null : createMapper(queryMethod); - StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, mapper, converter); + StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, this::createMapper, converter); query.setBeanFactory(beanfactory); return query; } else { - return new PartTreeJdbcQuery(context, queryMethod, dialect, converter, operations, createMapper(queryMethod)); + return new PartTreeJdbcQuery(context, queryMethod, dialect, converter, operations, this::createMapper); } } catch (Exception e) { throw QueryCreationException.create(queryMethod, e); @@ -116,9 +115,7 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository } @SuppressWarnings("unchecked") - private RowMapper createMapper(JdbcQueryMethod queryMethod) { - - Class returnedObjectType = queryMethod.getReturnedObjectType(); + private RowMapper createMapper(Class returnedObjectType) { RelationalPersistentEntity persistentEntity = context.getPersistentEntity(returnedObjectType); @@ -126,19 +123,18 @@ private RowMapper createMapper(JdbcQueryMethod queryMethod) { return (RowMapper) SingleColumnRowMapper.newInstance(returnedObjectType, converter.getConversionService()); } - return (RowMapper) determineDefaultMapper(queryMethod); + return (RowMapper) determineDefaultMapper(returnedObjectType); } - private RowMapper determineDefaultMapper(JdbcQueryMethod queryMethod) { + private RowMapper determineDefaultMapper(Class returnedObjectType) { - Class domainType = queryMethod.getReturnedObjectType(); - RowMapper configuredQueryMapper = queryMappingConfiguration.getRowMapper(domainType); + RowMapper configuredQueryMapper = queryMappingConfiguration.getRowMapper(returnedObjectType); if (configuredQueryMapper != null) return configuredQueryMapper; EntityRowMapper defaultEntityRowMapper = new EntityRowMapper<>( // - context.getRequiredPersistentEntity(domainType), // + context.getRequiredPersistentEntity(returnedObjectType), // converter // ); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 5b8cd32fe1..d5d73f7682 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -22,6 +22,7 @@ import lombok.Data; import lombok.NoArgsConstructor; +import lombok.Value; import java.io.IOException; import java.sql.ResultSet; @@ -441,6 +442,50 @@ public void sliceByNameShouldReturnCorrectResult() { assertThat(slice.hasNext()).isTrue(); } + @Test // #971 + public void stringQueryProjectionShouldReturnProjectedEntities() { + + repository.save(createDummyEntity()); + + List result = repository.findProjectedWithSql(DummyProjection.class); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getName()).isEqualTo("Entity Name"); + } + + @Test // #971 + public void stringQueryProjectionShouldReturnDtoProjectedEntities() { + + repository.save(createDummyEntity()); + + List result = repository.findProjectedWithSql(DtoProjection.class); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getName()).isEqualTo("Entity Name"); + } + + @Test // #971 + public void partTreeQueryProjectionShouldReturnProjectedEntities() { + + repository.save(createDummyEntity()); + + List result = repository.findProjectedByName("Entity Name"); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getName()).isEqualTo("Entity Name"); + } + + @Test // #971 + public void pageQueryProjectionShouldReturnProjectedEntities() { + + repository.save(createDummyEntity()); + + Page result = repository.findPageProjectionByName("Entity Name", PageRequest.ofSize(10)); + + assertThat(result).hasSize(1); + assertThat(result.getContent().get(0).getName()).isEqualTo("Entity Name"); + } + interface DummyEntityRepository extends CrudRepository { List findAllByNamedQuery(); @@ -450,6 +495,11 @@ interface DummyEntityRepository extends CrudRepository { @Query("SELECT * FROM DUMMY_ENTITY") List findAllWithSql(); + @Query("SELECT * FROM DUMMY_ENTITY") + List findProjectedWithSql(Class targetType); + + List findProjectedByName(String name); + @Query(value = "SELECT * FROM DUMMY_ENTITY", rowMapperClass = CustomRowMapper.class) List findAllWithCustomMapper(); @@ -472,6 +522,8 @@ interface DummyEntityRepository extends CrudRepository { Page findPageByNameContains(String name, Pageable pageable); + Page findPageProjectionByName(String name, Pageable pageable); + Slice findSliceByNameContains(String name, Pageable pageable); } @@ -528,6 +580,17 @@ public DummyEntity(String name) { } } + interface DummyProjection { + + String getName(); + } + + @Value + static class DtoProjection { + + String name; + } + static class CustomRowMapper implements RowMapper { @Override diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java index 9adac223d8..0ae6e18e5f 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java @@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; + import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcConverter; @@ -46,6 +47,7 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; @@ -67,6 +69,7 @@ public class PartTreeJdbcQueryUnitTests { JdbcMappingContext mappingContext = new JdbcMappingContext(); JdbcConverter converter = new BasicJdbcConverter(mappingContext, mock(RelationResolver.class)); + ReturnedType returnedType = mock(ReturnedType.class); @Test // DATAJDBC-318 public void shouldFailForQueryByReference() throws Exception { @@ -108,17 +111,31 @@ public void createsQueryToFindAllEntitiesByStringAttribute() throws Exception { JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstName", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); - ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { "John" })); + ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { "John" }), returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" = :first_name"); } + @Test // #971 + public void createsQueryToFindAllEntitiesByProjectionAttribute() throws Exception { + + when(returnedType.needsCustomConstruction()).thenReturn(true); + when(returnedType.getInputProperties()).thenReturn(Collections.singletonList("firstName")); + + JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstName", String.class); + PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); + ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { "John" }), returnedType); + + assertThat(query.getQuery()).isEqualTo("SELECT " + TABLE + ".\"FIRST_NAME\" AS \"FIRST_NAME\" " + JOIN_CLAUSE + + " WHERE " + TABLE + ".\"FIRST_NAME\" = :first_name"); + } + @Test // DATAJDBC-318 public void createsQueryWithIsNullCondition() throws Exception { JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstName", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); - ParametrizedQuery query = jdbcQuery.createQuery((getAccessor(queryMethod, new Object[] { null }))); + ParametrizedQuery query = jdbcQuery.createQuery((getAccessor(queryMethod, new Object[] { null })), returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" IS NULL"); } @@ -128,7 +145,7 @@ public void createsQueryWithLimitForExistsProjection() throws Exception { JdbcQueryMethod queryMethod = getQueryMethod("existsByFirstName", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); - ParametrizedQuery query = jdbcQuery.createQuery((getAccessor(queryMethod, new Object[] { "John" }))); + ParametrizedQuery query = jdbcQuery.createQuery((getAccessor(queryMethod, new Object[] { "John" })), returnedType); assertThat(query.getQuery()).isEqualTo( "SELECT " + TABLE + ".\"ID\" FROM " + TABLE + " WHERE " + TABLE + ".\"FIRST_NAME\" = :first_name LIMIT 1"); @@ -139,7 +156,8 @@ public void createsQueryToFindAllEntitiesByTwoStringAttributes() throws Exceptio JdbcQueryMethod queryMethod = getQueryMethod("findAllByLastNameAndFirstName", String.class, String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); - ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { "Doe", "John" })); + ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { "Doe", "John" }), + returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"LAST_NAME\" = :last_name AND (" + TABLE + ".\"FIRST_NAME\" = :first_name)"); @@ -150,7 +168,8 @@ public void createsQueryToFindAllEntitiesByOneOfTwoStringAttributes() throws Exc JdbcQueryMethod queryMethod = getQueryMethod("findAllByLastNameOrFirstName", String.class, String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); - ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { "Doe", "John" })); + ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { "Doe", "John" }), + returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"LAST_NAME\" = :last_name OR (" + TABLE + ".\"FIRST_NAME\" = :first_name)"); @@ -164,7 +183,7 @@ public void createsQueryToFindAllEntitiesByDateAttributeBetween() throws Excepti Date from = new Date(); Date to = new Date(); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { from, to }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()) .isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"DATE_OF_BIRTH\" BETWEEN :date_of_birth AND :date_of_birth1"); @@ -179,7 +198,7 @@ public void createsQueryToFindAllEntitiesByIntegerAttributeLessThan() throws Exc JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeLessThan", Integer.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { 30 }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"AGE\" < :age"); } @@ -190,7 +209,7 @@ public void createsQueryToFindAllEntitiesByIntegerAttributeLessThanEqual() throw JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeLessThanEqual", Integer.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { 30 }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"AGE\" <= :age"); } @@ -201,7 +220,7 @@ public void createsQueryToFindAllEntitiesByIntegerAttributeGreaterThan() throws JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeGreaterThan", Integer.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { 30 }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"AGE\" > :age"); } @@ -212,7 +231,7 @@ public void createsQueryToFindAllEntitiesByIntegerAttributeGreaterThanEqual() th JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeGreaterThanEqual", Integer.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { 30 }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"AGE\" >= :age"); } @@ -223,7 +242,7 @@ public void createsQueryToFindAllEntitiesByDateAttributeAfter() throws Exception JdbcQueryMethod queryMethod = getQueryMethod("findAllByDateOfBirthAfter", Date.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { new Date() }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"DATE_OF_BIRTH\" > :date_of_birth"); } @@ -233,7 +252,7 @@ public void createsQueryToFindAllEntitiesByDateAttributeBefore() throws Exceptio JdbcQueryMethod queryMethod = getQueryMethod("findAllByDateOfBirthBefore", Date.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { new Date() }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"DATE_OF_BIRTH\" < :date_of_birth"); } @@ -244,7 +263,7 @@ public void createsQueryToFindAllEntitiesByIntegerAttributeIsNull() throws Excep JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeIsNull"); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"AGE\" IS NULL"); } @@ -255,7 +274,7 @@ public void createsQueryToFindAllEntitiesByIntegerAttributeIsNotNull() throws Ex JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeIsNotNull"); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"AGE\" IS NOT NULL"); } @@ -266,7 +285,7 @@ public void createsQueryToFindAllEntitiesByStringAttributeLike() throws Exceptio JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameLike", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "%John%" }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name"); } @@ -277,7 +296,7 @@ public void createsQueryToFindAllEntitiesByStringAttributeNotLike() throws Excep JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameNotLike", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "%John%" }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" NOT LIKE :first_name"); } @@ -288,7 +307,7 @@ public void createsQueryToFindAllEntitiesByStringAttributeStartingWith() throws JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameStartingWith", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "Jo" }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name"); } @@ -299,7 +318,7 @@ public void appendsLikeOperatorParameterWithPercentSymbolForStartingWithQuery() JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameStartingWith", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "Jo" }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name"); assertThat(query.getParameterSource().getValue("first_name")).isEqualTo("Jo%"); @@ -311,7 +330,7 @@ public void createsQueryToFindAllEntitiesByStringAttributeEndingWith() throws Ex JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameEndingWith", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "hn" }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name"); } @@ -322,7 +341,7 @@ public void prependsLikeOperatorParameterWithPercentSymbolForEndingWithQuery() t JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameEndingWith", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "hn" }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name"); assertThat(query.getParameterSource().getValue("first_name")).isEqualTo("%hn"); @@ -334,7 +353,7 @@ public void createsQueryToFindAllEntitiesByStringAttributeContaining() throws Ex JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameContaining", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "oh" }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name"); } @@ -345,7 +364,7 @@ public void wrapsLikeOperatorParameterWithPercentSymbolsForContainingQuery() thr JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameContaining", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "oh" }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" LIKE :first_name"); assertThat(query.getParameterSource().getValue("first_name")).isEqualTo("%oh%"); @@ -357,7 +376,7 @@ public void createsQueryToFindAllEntitiesByStringAttributeNotContaining() throws JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameNotContaining", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "oh" }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" NOT LIKE :first_name"); } @@ -368,7 +387,7 @@ public void wrapsLikeOperatorParameterWithPercentSymbolsForNotContainingQuery() JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameNotContaining", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "oh" }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" NOT LIKE :first_name"); assertThat(query.getParameterSource().getValue("first_name")).isEqualTo("%oh%"); @@ -380,7 +399,7 @@ public void createsQueryToFindAllEntitiesByIntegerAttributeWithDescendingOrderin JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeOrderByLastNameDesc", Integer.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { 123 }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()) .isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"AGE\" = :age ORDER BY \"LAST_NAME\" DESC"); @@ -391,7 +410,7 @@ public void createsQueryToFindAllEntitiesByIntegerAttributeWithAscendingOrdering JdbcQueryMethod queryMethod = getQueryMethod("findAllByAgeOrderByLastNameAsc", Integer.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { 123 }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()) .isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"AGE\" = :age ORDER BY \"LAST_NAME\" ASC"); @@ -402,7 +421,7 @@ public void createsQueryToFindAllEntitiesByStringAttributeNot() throws Exception JdbcQueryMethod queryMethod = getQueryMethod("findAllByLastNameNot", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "Doe" }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"LAST_NAME\" != :last_name"); } @@ -414,7 +433,7 @@ public void createsQueryToFindAllEntitiesByIntegerAttributeIn() throws Exception PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { Collections.singleton(25) }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"AGE\" IN (:age)"); } @@ -425,7 +444,7 @@ public void createsQueryToFindAllEntitiesByIntegerAttributeNotIn() throws Except PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { Collections.singleton(25) }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"AGE\" NOT IN (:age)"); } @@ -436,7 +455,7 @@ public void createsQueryToFindAllEntitiesByBooleanAttributeTrue() throws Excepti JdbcQueryMethod queryMethod = getQueryMethod("findAllByActiveTrue"); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"ACTIVE\" = TRUE"); } @@ -447,7 +466,7 @@ public void createsQueryToFindAllEntitiesByBooleanAttributeFalse() throws Except JdbcQueryMethod queryMethod = getQueryMethod("findAllByActiveFalse"); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()).isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"ACTIVE\" = FALSE"); } @@ -458,7 +477,7 @@ public void createsQueryToFindAllEntitiesByStringAttributeIgnoringCase() throws JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameIgnoreCase", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "John" }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); assertThat(query.getQuery()) .isEqualTo(BASE_SELECT + " WHERE UPPER(" + TABLE + ".\"FIRST_NAME\") = UPPER(:first_name)"); @@ -471,7 +490,7 @@ public void throwsExceptionWhenIgnoringCaseIsImpossible() throws Exception { PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); assertThatIllegalStateException() - .isThrownBy(() -> jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { 1L }))); + .isThrownBy(() -> jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { 1L }), returnedType)); } @Test // DATAJDBC-318 @@ -481,7 +500,7 @@ public void throwsExceptionWhenConditionKeywordIsUnsupported() throws Exception PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); assertThatIllegalArgumentException() - .isThrownBy(() -> jdbcQuery.createQuery(getAccessor(queryMethod, new Object[0]))); + .isThrownBy(() -> jdbcQuery.createQuery(getAccessor(queryMethod, new Object[0]), returnedType)); } @Test // DATAJDBC-318 @@ -491,7 +510,7 @@ public void throwsExceptionWhenInvalidNumberOfParameterIsGiven() throws Exceptio PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); assertThatIllegalArgumentException() - .isThrownBy(() -> jdbcQuery.createQuery(getAccessor(queryMethod, new Object[0]))); + .isThrownBy(() -> jdbcQuery.createQuery(getAccessor(queryMethod, new Object[0]), returnedType)); } @Test // DATAJDBC-318 @@ -500,7 +519,7 @@ public void createsQueryWithLimitToFindEntitiesByStringAttribute() throws Except JdbcQueryMethod queryMethod = getQueryMethod("findTop3ByFirstName", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "John" }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); String expectedSql = BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" = :first_name LIMIT 3"; assertThat(query.getQuery()).isEqualTo(expectedSql); @@ -512,7 +531,7 @@ public void createsQueryToFindFirstEntityByStringAttribute() throws Exception { JdbcQueryMethod queryMethod = getQueryMethod("findFirstByFirstName", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "John" }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); String expectedSql = BASE_SELECT + " WHERE " + TABLE + ".\"FIRST_NAME\" = :first_name LIMIT 1"; assertThat(query.getQuery()).isEqualTo(expectedSql); @@ -525,7 +544,7 @@ public void createsQueryByEmbeddedObject() throws Exception { PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { new Address("Hello", "World") }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); String expectedSql = BASE_SELECT + " WHERE (" + TABLE + ".\"USER_STREET\" = :user_street AND " + TABLE + ".\"USER_CITY\" = :user_city)"; @@ -541,7 +560,7 @@ public void createsQueryByEmbeddedProperty() throws Exception { JdbcQueryMethod queryMethod = getQueryMethod("findByAddressStreet", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "Hello" }); - ParametrizedQuery query = jdbcQuery.createQuery(accessor); + ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); String expectedSql = BASE_SELECT + " WHERE " + TABLE + ".\"USER_STREET\" = :user_street"; @@ -554,7 +573,7 @@ public void createsQueryForCountProjection() throws Exception { JdbcQueryMethod queryMethod = getQueryMethod("countByFirstName", String.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); - ParametrizedQuery query = jdbcQuery.createQuery((getAccessor(queryMethod, new Object[] { "John" }))); + ParametrizedQuery query = jdbcQuery.createQuery((getAccessor(queryMethod, new Object[] { "John" })), returnedType); assertThat(query.getQuery()) .isEqualTo("SELECT COUNT(*) FROM " + TABLE + " WHERE " + TABLE + ".\"FIRST_NAME\" = :first_name"); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java index d38ec8f7a5..fad388e718 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java @@ -199,6 +199,11 @@ public Object writeValue(@Nullable Object value, TypeInformation type) { return conversionService.convert(value, type.getType()); } + @Override + public EntityInstantiators getEntityInstantiators() { + return this.entityInstantiators; + } + /** * Checks whether we have a custom conversion registered for the given value into an arbitrary simple JDBC type. * Returns the converted value if so. If not, we perform special enum handling or simply return the value as is. diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java index fccc0aa621..707eb824e6 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java @@ -22,6 +22,7 @@ import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PreferredConstructor.Parameter; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; @@ -92,4 +93,12 @@ T createInstance(PersistentEntity entity, */ @Nullable Object writeValue(@Nullable Object value, TypeInformation type); + + /** + * Return the underlying {@link EntityInstantiators}. + * + * @return + * @since 2.3 + */ + EntityInstantiators getEntityInstantiators(); } diff --git a/src/main/asciidoc/jdbc.adoc b/src/main/asciidoc/jdbc.adoc index 331829fe2e..eb60da962b 100644 --- a/src/main/asciidoc/jdbc.adoc +++ b/src/main/asciidoc/jdbc.adoc @@ -1,7 +1,7 @@ [[jdbc.repositories]] = JDBC Repositories -This chapter points out the specialties for repository support for JDBC. This builds on the core repository support explained in <>. +This chapter points out the specialties for repository support for JDBC.This builds on the core repository support explained in <>. You should have a sound understanding of the basic concepts explained there. [[jdbc.why]] @@ -696,6 +696,8 @@ You can specify the following return types: * `int` (updated record count) * `boolean`(whether a record was updated) +include::{spring-data-commons-docs}/repository-projections.adoc[leveloffset=+2] + [[jdbc.mybatis]] == MyBatis Integration From 3311bef1cd757680e5e25c16f25c021524fb6b08 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 17 May 2021 15:39:45 +0200 Subject: [PATCH 2/2] Polishing. Remove superfluous public keyword where not required. --- src/main/asciidoc/jdbc.adoc | 40 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/main/asciidoc/jdbc.adoc b/src/main/asciidoc/jdbc.adoc index eb60da962b..1a0356dc59 100644 --- a/src/main/asciidoc/jdbc.adoc +++ b/src/main/asciidoc/jdbc.adoc @@ -127,7 +127,7 @@ The Spring Data JDBC repositories support can be activated by an annotation thro class ApplicationConfig extends AbstractJdbcConfiguration { // <2> @Bean - public DataSource dataSource() { // <3> + DataSource dataSource() { // <3> EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); return builder.setType(EmbeddedDatabaseType.HSQL).build(); @@ -250,13 +250,11 @@ Custom converters can be registered, for types that are not supported by default [source,java] ---- @Configuration -public class DataJdbcConfiguration extends AbstractJdbcConfiguration { +class DataJdbcConfiguration extends AbstractJdbcConfiguration { @Override public JdbcCustomConversions jdbcCustomConversions() { - return new JdbcCustomConversions(Collections.singletonList(TimestampTzToDateConverter.INSTANCE)); - } @ReadingConverter @@ -303,7 +301,7 @@ The following example maps the `MyEntity` class to the `CUSTOM_TABLE_NAME` table [source,java] ---- @Table("CUSTOM_TABLE_NAME") -public class MyEntity { +class MyEntity { @Id Integer id; @@ -322,7 +320,7 @@ The following example maps the `name` property of the `MyEntity` class to the `C ==== [source,java] ---- -public class MyEntity { +class MyEntity { @Id Integer id; @@ -340,7 +338,7 @@ In the following example the corresponding table for the `MySubEntity` class has ==== [source,java] ---- -public class MyEntity { +class MyEntity { @Id Integer id; @@ -348,7 +346,7 @@ public class MyEntity { Set subEntities; } -public class MySubEntity { +class MySubEntity { String name; } ---- @@ -360,7 +358,7 @@ This additional column name may be customized with the `keyColumn` Element of th ==== [source,java] ---- -public class MyEntity { +class MyEntity { @Id Integer id; @@ -368,7 +366,7 @@ public class MyEntity { List name; } -public class MySubEntity { +class MySubEntity { String name; } ---- @@ -388,7 +386,7 @@ Opposite to this behavior `USE_EMPTY` tries to create a new instance using eithe ==== [source,java] ---- -public class MyEntity { +class MyEntity { @Id Integer id; @@ -397,7 +395,7 @@ public class MyEntity { EmbeddedEntity embeddedEntity; } -public class EmbeddedEntity { +class EmbeddedEntity { String name; } ---- @@ -414,7 +412,7 @@ Make use of the shortcuts `@Embedded.Nullable` & `@Embedded.Empty` for `@Embedde [source,java] ---- -public class MyEntity { +class MyEntity { @Id Integer id; @@ -610,7 +608,7 @@ The following example shows how to use `@Query` to declare a query method: ==== [source,java] ---- -public interface UserRepository extends CrudRepository { +interface UserRepository extends CrudRepository { @Query("select firstName, lastName from User u where u.emailAddress = :email") User findByEmailAddress(@Param("email") String email); @@ -828,7 +826,7 @@ For example, the following listener gets invoked before an aggregate gets saved: [source,java] ---- @Bean -public ApplicationListener> loggingSaves() { +ApplicationListener> loggingSaves() { return event -> { @@ -845,7 +843,7 @@ Callback methods will only get invoked for events related to the domain type and ==== [source,java] ---- -public class PersonLoadListener extends AbstractRelationalEventListener { +class PersonLoadListener extends AbstractRelationalEventListener { @Override protected void onAfterLoad(AfterLoadEvent personLoad) { @@ -937,11 +935,11 @@ If you need to tweak transaction configuration for one of the methods declared i ==== [source,java] ---- -public interface UserRepository extends CrudRepository { +interface UserRepository extends CrudRepository { @Override @Transactional(timeout = 10) - public List findAll(); + List findAll(); // Further query method declarations } @@ -959,7 +957,7 @@ The following example shows how to create such a facade: [source,java] ---- @Service -class UserManagementImpl implements UserManagement { +public class UserManagementImpl implements UserManagement { private final UserRepository userRepository; private final RoleRepository roleRepository; @@ -999,7 +997,7 @@ To let your query methods be transactional, use `@Transactional` at the reposito [source,java] ---- @Transactional(readOnly = true) -public interface UserRepository extends CrudRepository { +interface UserRepository extends CrudRepository { List findByLastname(String lastname); @@ -1035,7 +1033,7 @@ In order to activate auditing, add `@EnableJdbcAuditing` to your configuration, class Config { @Bean - public AuditorAware auditorProvider() { + AuditorAware auditorProvider() { return new AuditorAwareImpl(); } }