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 b5ec45aef..cdbcf9c3f 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 @@ public abstract class AbstractJdbcQuery implements RepositoryQuery { 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 f1557d056..da89bc75a 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.SelectBuilder; 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 @@ import org.springframework.data.repository.query.parser.PartTree; 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 67be6aa8b..f10fc0d0b 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.RelationalEntityMeta 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 @@ class JdbcQueryCreator extends RelationalQueryCreator { 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 5dc654dec..4199c275f 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 ea361f539..fccbe0a00 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.RelationalEntityMeta 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 class PartTreeJdbcQuery extends AbstractJdbcQuery { 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 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery { }); } - 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 365a8055c..02b5e24e2 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 class StringBasedJdbcQuery extends AbstractJdbcQuery { 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 class StringBasedJdbcQuery extends AbstractJdbcQuery { */ @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 class StringBasedJdbcQuery extends AbstractJdbcQuery { 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 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { return BeanUtils.instantiateClass(resultSetExtractorClass); } + @Nullable + RowMapper determineRowMapper(@Nullable RowMapper defaultMapper, + Converter resultProcessingConverter, boolean hasDynamicProjection) { + + RowMapper rowMapperToUse = determineRowMapper(defaultMapper); + + if ((hasDynamicProjection || rowMapperToUse == defaultMapper) && rowMapperToUse != null) { + return new ConvertingRowMapper<>(rowMapperToUse, resultProcessingConverter); + } + + return rowMapperToUse; + } + @SuppressWarnings("unchecked") @Nullable RowMapper determineRowMapper(@Nullable RowMapper defaultMapper) { 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 291d2cf50..e9ee9d36a 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 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { 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 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { } @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 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { 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 5b8cd32fe..d5d73f768 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 static org.springframework.test.context.TestExecutionListeners.MergeMode. import lombok.Data; import lombok.NoArgsConstructor; +import lombok.Value; import java.io.IOException; import java.sql.ResultSet; @@ -441,6 +442,50 @@ public class JdbcRepositoryIntegrationTests { 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 @@ public class JdbcRepositoryIntegrationTests { @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 @@ public class JdbcRepositoryIntegrationTests { Page findPageByNameContains(String name, Pageable pageable); + Page findPageProjectionByName(String name, Pageable pageable); + Slice findSliceByNameContains(String name, Pageable pageable); } @@ -528,6 +580,17 @@ public class JdbcRepositoryIntegrationTests { } } + 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 9adac223d..0ae6e18e5 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 java.util.Properties; 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.NoRepositoryBean; 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 class PartTreeJdbcQueryUnitTests { 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 d38ec8f7a..fad388e71 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 class BasicRelationalConverter implements RelationalConverter { 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 fccc0aa62..707eb824e 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.PersistentEntity; 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 @@ public interface RelationalConverter { */ @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 331829fe2..eb60da962 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