Skip to content

Add support for repository query method projections #980

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -123,8 +122,59 @@ <T> JdbcQueryExecution<List<T>> collectionQuery(RowMapper<T> 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 <T> JdbcQueryExecution<T> getQueryExecution(ResultSetExtractor<T> 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<Object> create(Class<?> result);
}

/**
* Delegating {@link RowMapper} that reads a row into {@code T} and converts it afterwards into {@code Object}.
*
* @param <T>
* @since 2.3
*/
protected static class ConvertingRowMapper<T> implements RowMapper<Object> {

private final RowMapper<T> delegate;
private final Converter<Object, Object> converter;

public ConvertingRowMapper(RowMapper<T> delegate, Converter<Object, Object> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -67,6 +68,7 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
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},
Expand All @@ -79,14 +81,17 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
* @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;
Expand All @@ -96,6 +101,7 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
this.queryMapper = new QueryMapper(dialect, converter);
this.renderContextFactory = new RenderContextFactory(dialect);
this.isSliceQuery = isSliceQuery;
this.returnedType = returnedType;
}

/**
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,4 +47,41 @@ interface JdbcQueryExecution<T> {
*/
@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<Object, Object> {

private final ResultProcessor processor;
private final Lazy<Converter<Object, Object>> converter;

ResultProcessingConverter(ResultProcessor processor,
MappingContext<? extends RelationalPersistentEntity<?>, ? 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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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}.
Expand All @@ -69,26 +73,40 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery {
*/
public PartTreeJdbcQuery(RelationalMappingContext context, JdbcQueryMethod queryMethod, Dialect dialect,
JdbcConverter converter, NamedParameterJdbcOperations operations, RowMapper<Object> 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<Boolean> extractor = tree.isExistsProjection() ? (ResultSet::next) : null;

this.coreExecution = queryMethod.isPageQuery() || queryMethod.isSliceQuery() ? collectionQuery(rowMapper)
: getQueryExecution(queryMethod, extractor, rowMapper);
}

private Sort getDynamicSort(RelationalParameterAccessor accessor) {
Expand All @@ -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<Boolean> extractor = tree.isExistsProjection() ? (ResultSet::next) : null;

RowMapper<Object> rowMapper;

if (tree.isCountProjection() || tree.isExistsProjection()) {
rowMapper = rowMapperFactory.create(resolveTypeToRead(processor));
} else {

Converter<Object, Object> 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<Collection<Object>>) this.coreExecution, accessor.getPageable());
return new SliceQueryExecution<>((JdbcQueryExecution<Collection<Object>>) queryExecution, accessor.getPageable());
}

if (getQueryMethod().isPageQuery()) {

return new PageQueryExecution<>((JdbcQueryExecution<Collection<Object>>) this.coreExecution, accessor.getPageable(),
return new PageQueryExecution<>((JdbcQueryExecution<Collection<Object>>) 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(),
Expand All @@ -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));
}

Expand Down
Loading