DATACASS-313 - Limit queried fields for projections.

We now limit the selected fields when using projections in repository query methods and by using query operations. Queries that return either a closed interface projection or DTO projection are inspected for the select fields and only these fields are included in the SELECT list.
This commit is contained in:
Mark Paluch
2019-01-28 16:13:15 +01:00
parent 9e4c603d17
commit 1be26f9612
10 changed files with 164 additions and 56 deletions

View File

@@ -49,6 +49,7 @@ import org.springframework.data.cassandra.core.mapping.event.AfterLoadEvent;
import org.springframework.data.cassandra.core.mapping.event.AfterSaveEvent;
import org.springframework.data.cassandra.core.mapping.event.BeforeDeleteEvent;
import org.springframework.data.cassandra.core.mapping.event.BeforeSaveEvent;
import org.springframework.data.cassandra.core.query.Columns;
import org.springframework.data.cassandra.core.query.Query;
import org.springframework.data.domain.Slice;
import org.springframework.data.mapping.context.MappingContext;
@@ -322,8 +323,14 @@ public class CassandraTemplate implements CassandraOperations, ApplicationEventP
<T> List<T> doSelect(Query query, Class<?> entityClass, CqlIdentifier tableName, Class<T> returnType) {
Function<Row, T> mapper = getMapper(entityClass, returnType, tableName);
CassandraPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(entityClass);
RegularStatement select = getStatementFactory().select(query, getRequiredPersistentEntity(entityClass), tableName);
Columns columns = getStatementFactory().computeColumnsForProjection(query.getColumns(), persistentEntity,
returnType);
Query queryToUse = query.columns(columns);
RegularStatement select = getStatementFactory().select(queryToUse, persistentEntity, tableName);
return getCqlOperations().query(select, (row, rowNum) -> mapper.apply(row));
}

View File

@@ -53,6 +53,7 @@ import org.springframework.data.cassandra.core.mapping.event.AfterLoadEvent;
import org.springframework.data.cassandra.core.mapping.event.AfterSaveEvent;
import org.springframework.data.cassandra.core.mapping.event.BeforeDeleteEvent;
import org.springframework.data.cassandra.core.mapping.event.BeforeSaveEvent;
import org.springframework.data.cassandra.core.query.Columns;
import org.springframework.data.cassandra.core.query.Query;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
@@ -296,7 +297,14 @@ public class ReactiveCassandraTemplate implements ReactiveCassandraOperations, A
<T> Flux<T> doSelect(Query query, Class<?> entityClass, CqlIdentifier tableName, Class<T> returnType) {
RegularStatement select = getStatementFactory().select(query, getRequiredPersistentEntity(entityClass), tableName);
CassandraPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(entityClass);
Columns columns = getStatementFactory().computeColumnsForProjection(query.getColumns(), persistentEntity,
returnType);
Query queryToUse = query.columns(columns);
RegularStatement select = getStatementFactory().select(queryToUse, persistentEntity, tableName);
Function<Row, T> mapper = getMapper(entityClass, returnType, tableName);

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.cassandra.core;
import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -27,6 +28,7 @@ import org.springframework.data.cassandra.core.cql.CqlIdentifier;
import org.springframework.data.cassandra.core.cql.QueryOptionsUtil;
import org.springframework.data.cassandra.core.cql.WriteOptions;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
import org.springframework.data.cassandra.core.query.Columns;
import org.springframework.data.cassandra.core.query.Columns.ColumnSelector;
import org.springframework.data.cassandra.core.query.Columns.FunctionCall;
import org.springframework.data.cassandra.core.query.Columns.Selector;
@@ -46,7 +48,13 @@ import org.springframework.data.cassandra.core.query.Update.SetAtKeyOp;
import org.springframework.data.cassandra.core.query.Update.SetOp;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.ProjectionInformation;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import com.datastax.driver.core.RegularStatement;
import com.datastax.driver.core.Statement;
@@ -75,6 +83,8 @@ public class StatementFactory {
private final UpdateMapper updateMapper;
private final ProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
/**
* Create {@link StatementFactory} given {@link UpdateMapper}.
*
@@ -190,8 +200,8 @@ public class StatementFactory {
return createSelect(query, entity, filter, selectors, tableName);
}
private Select createSelect(Query query, CassandraPersistentEntity<?> entity, Filter filter,
List<Selector> selectors, CqlIdentifier tableName) {
private Select createSelect(Query query, CassandraPersistentEntity<?> entity, Filter filter, List<Selector> selectors,
CqlIdentifier tableName) {
Sort sort = Optional.of(query.getSort()).map(querySort -> getQueryMapper().getMappedSort(querySort, entity))
.orElse(Sort.unsorted());
@@ -223,8 +233,8 @@ public class StatementFactory {
Selection selection = QueryBuilder.select();
selectors.forEach(selector ->
selector.getAlias().map(CqlIdentifier::toCql).ifPresent(getSelection(selection, selector)::as));
selectors.forEach(
selector -> selector.getAlias().map(CqlIdentifier::toCql).ifPresent(getSelection(selection, selector)::as));
select = selection.from(from.toCql());
}
@@ -465,6 +475,43 @@ public class StatementFactory {
return delete;
}
/**
* Compute the {@link Columns} to include type if the {@code returnType} is a {@literal DTO projection} or a
* {@literal closed interface projection}.
*
* @param columns must not be {@literal null}.
* @param persistentEntity must not be {@literal null}.
* @param returnType must not be {@literal null}.
* @return {@link Columns} with columns to be included.
* @since 2.2
*/
Columns computeColumnsForProjection(Columns columns, PersistentEntity<?, ?> persistentEntity, Class<?> returnType) {
if (!columns.isEmpty() || ClassUtils.isAssignable(persistentEntity.getType(), returnType)) {
return columns;
}
Columns projectedColumns = Columns.empty();
if (returnType.isInterface()) {
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnType);
if (projectionInformation.isClosed()) {
for (PropertyDescriptor inputProperty : projectionInformation.getInputProperties()) {
projectedColumns = projectedColumns.include(inputProperty.getName());
}
}
} else {
for (PersistentProperty<?> property : persistentEntity) {
projectedColumns = projectedColumns.include(property.getName());
}
}
return projectedColumns;
}
private static Delete delete(List<String> columnNames, CqlIdentifier from, Filter filter) {
Delete select;
@@ -538,7 +585,7 @@ public class StatementFactory {
return QueryBuilder.containsKey(columnName, predicate.getValue());
}
throw new IllegalArgumentException(String.format("Criteria %s %s %s not supported",
columnName, predicate.getOperator(), predicate.getValue()));
throw new IllegalArgumentException(
String.format("Criteria %s %s %s not supported", columnName, predicate.getOperator(), predicate.getValue()));
}
}

View File

@@ -104,7 +104,8 @@ public class PartTreeCassandraQuery extends AbstractCassandraQuery {
return getQueryStatementCreator().exists(getStatementFactory(), getTree(), parameterAccessor);
}
return getQueryStatementCreator().select(getStatementFactory(), getTree(), parameterAccessor);
return getQueryStatementCreator().select(getStatementFactory(), getTree(), parameterAccessor,
getQueryMethod().getResultProcessor());
}
/* (non-Javadoc)

View File

@@ -15,24 +15,26 @@
*/
package org.springframework.data.cassandra.repository.query;
import lombok.RequiredArgsConstructor;
import java.util.Optional;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.cassandra.core.StatementFactory;
import org.springframework.data.cassandra.core.cql.QueryOptions;
import org.springframework.data.cassandra.core.cql.QueryOptionsUtil;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty;
import org.springframework.data.cassandra.core.query.Columns;
import org.springframework.data.cassandra.core.query.Query;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.repository.query.QueryCreationException;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.repository.query.ReturnedType;
import org.springframework.data.repository.query.parser.PartTree;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.datastax.driver.core.RegularStatement;
import com.datastax.driver.core.SimpleStatement;
import com.datastax.driver.core.Statement;
@@ -65,11 +67,19 @@ class QueryStatementCreator {
* @param parameterAccessor must not be {@literal null}.
* @return the {@literal SELECT} {@link Statement}.
*/
Statement select(StatementFactory statementFactory, PartTree tree,
CassandraParameterAccessor parameterAccessor) {
Statement select(StatementFactory statementFactory, PartTree tree, CassandraParameterAccessor parameterAccessor,
ResultProcessor processor) {
Function<Query, Statement> function = query -> {
ReturnedType returnedType = processor.withDynamicProjection(parameterAccessor).getReturnedType();
if (returnedType.needsCustomConstruction()) {
Columns columns = Columns.from(returnedType.getInputProperties().toArray(new String[0]));
query = query.columns(columns);
}
RegularStatement statement = statementFactory.select(query, requirePersistentEntity());
if (LOG.isDebugEnabled()) {
@@ -144,8 +154,7 @@ class QueryStatementCreator {
<T> T doWithQuery(CassandraParameterAccessor parameterAccessor, PartTree tree,
Function<Query, ? extends T> function) {
CassandraQueryCreator queryCreator =
new CassandraQueryCreator(tree, parameterAccessor, this.mappingContext);
CassandraQueryCreator queryCreator = new CassandraQueryCreator(tree, parameterAccessor, this.mappingContext);
Query query = queryCreator.createQuery();
@@ -164,9 +173,8 @@ class QueryStatementCreator {
if (queryOptions.isPresent()) {
query = Optional.ofNullable(parameterAccessor.getQueryOptions()).map(query::queryOptions).orElse(query);
} else if (this.queryMethod.hasConsistencyLevel()) {
query = query.queryOptions(QueryOptions.builder()
.consistencyLevel(this.queryMethod.getRequiredAnnotatedConsistencyLevel())
.build());
query = query.queryOptions(
QueryOptions.builder().consistencyLevel(this.queryMethod.getRequiredAnnotatedConsistencyLevel()).build());
}
return function.apply(query);
@@ -178,8 +186,7 @@ class QueryStatementCreator {
private boolean allowsFiltering() {
return this.queryMethod.getQueryAnnotation()
.map(org.springframework.data.cassandra.repository.Query::allowFiltering)
.orElse(false);
.map(org.springframework.data.cassandra.repository.Query::allowFiltering).orElse(false);
}
/**
@@ -201,8 +208,7 @@ class QueryStatementCreator {
if (queryOptions.isPresent()) {
queryToUse = Optional.ofNullable(parameterAccessor.getQueryOptions())
.map(it -> QueryOptionsUtil.addQueryOptions(boundQuery, it))
.orElse(boundQuery);
.map(it -> QueryOptionsUtil.addQueryOptions(boundQuery, it)).orElse(boundQuery);
} else if (this.queryMethod.hasConsistencyLevel()) {
queryToUse.setConsistencyLevel(this.queryMethod.getRequiredAnnotatedConsistencyLevel());
}

View File

@@ -104,7 +104,8 @@ public class ReactivePartTreeCassandraQuery extends AbstractReactiveCassandraQue
return getQueryStatementCreator().exists(getStatementFactory(), getTree(), parameterAccessor);
}
return getQueryStatementCreator().select(getStatementFactory(), getTree(), parameterAccessor);
return getQueryStatementCreator().select(getStatementFactory(), getTree(), parameterAccessor,
getQueryMethod().getResultProcessor());
}
/* (non-Javadoc)

View File

@@ -16,9 +16,7 @@
package org.springframework.data.cassandra.core;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.Collections;
@@ -152,6 +150,24 @@ public class CassandraTemplateUnitTests {
assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT * FROM users WHERE id='myid';");
}
@Test // DATACASS-313
public void selectProjectedOneShouldReturnMappedResults() {
when(resultSet.iterator()).thenReturn(Collections.singleton(row).iterator());
when(columnDefinitions.contains(anyString())).thenReturn(true);
when(columnDefinitions.getType(anyInt())).thenReturn(DataType.ascii());
when(columnDefinitions.getIndexOf("firstname")).thenReturn(0);
when(row.getObject(0)).thenReturn("Walter");
UserProjection user = template.query(User.class).as(UserProjection.class).oneValue();
assertThat(user.getFirstname()).isEqualTo("Walter");
verify(session).execute(statementCaptor.capture());
assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT firstname FROM users LIMIT 2;");
}
@Test // DATACASS-292
public void existsShouldReturnExistingElement() {
@@ -394,4 +410,8 @@ public class CassandraTemplateUnitTests {
verify(session).execute(Mockito.any(Batch.class));
}
interface UserProjection {
String getFirstname();
}
}

View File

@@ -15,20 +15,16 @@
*/
package org.springframework.data.cassandra.core;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Collections;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -36,7 +32,6 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.cassandra.ReactiveResultSet;
import org.springframework.data.cassandra.ReactiveSession;
import org.springframework.data.cassandra.core.query.Query;
@@ -131,6 +126,28 @@ public class ReactiveCassandraTemplateUnitTests {
assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT * FROM users WHERE id='myid';");
}
@Test // DATACASS-313
public void selectProjectedOneShouldReturnMappedResults() {
when(reactiveResultSet.rows()).thenReturn(Flux.just(row));
when(columnDefinitions.contains(anyString())).thenReturn(true);
when(columnDefinitions.getType(anyInt())).thenReturn(DataType.ascii());
when(columnDefinitions.getIndexOf("firstname")).thenReturn(0);
when(row.getObject(0)).thenReturn("Walter");
template.query(User.class).as(UserProjection.class).first() //
.as(StepVerifier::create) //
.assertNext(actual -> {
assertThat(actual.getFirstname()).isEqualTo("Walter");
}).verifyComplete();
verify(session).execute(statementCaptor.capture());
assertThat(statementCaptor.getValue().toString()).isEqualTo("SELECT firstname FROM users LIMIT 1;");
}
@Test // DATACASS-335
public void existsShouldReturnExistingElement() {
@@ -266,4 +283,8 @@ public class ReactiveCassandraTemplateUnitTests {
verify(session).execute(statementCaptor.capture());
assertThat(statementCaptor.getValue().toString()).isEqualTo("TRUNCATE users;");
}
interface UserProjection {
String getFirstname();
}
}

View File

@@ -15,8 +15,8 @@
*/
package org.springframework.data.cassandra.repository.query;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import java.util.Arrays;
@@ -30,7 +30,6 @@ import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.cassandra.core.CassandraOperations;
import org.springframework.data.cassandra.core.convert.CassandraConverter;
import org.springframework.data.cassandra.core.convert.MappingCassandraConverter;
@@ -118,20 +117,20 @@ public class PartTreeCassandraQueryUnitTests {
assertThat(query).isEqualTo("SELECT * FROM person WHERE firstname='foo' AND lastname='bar';");
}
@Test // DATACASS-7
@Test // DATACASS-7, DATACASS-313
public void usesDynamicProjection() {
String query = deriveQueryFromMethod("findDynamicallyProjectedBy", PersonProjection.class);
assertThat(query).isEqualTo("SELECT * FROM person;");
assertThat(query).isEqualTo("SELECT lastname,firstname FROM person;");
}
@Test // DATACASS-479
@Test // DATACASS-479, DATACASS-313
public void usesProjectionQueryHiddenField() {
String query = deriveQueryFromMethod("findPersonProjectedByNickname", "foo");
assertThat(query).isEqualTo("SELECT * FROM person WHERE nickname='foo';");
assertThat(query).isEqualTo("SELECT lastname,firstname FROM person WHERE nickname='foo';");
}
@Test // DATACASS-357

View File

@@ -15,14 +15,15 @@
*/
package org.springframework.data.cassandra.repository.query;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import java.lang.reflect.Method;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import rx.Single;
import java.lang.reflect.Method;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Rule;
@@ -31,7 +32,6 @@ import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
import org.springframework.data.cassandra.core.convert.MappingCassandraConverter;
import org.springframework.data.cassandra.core.cql.QueryOptions;
@@ -49,8 +49,6 @@ import org.springframework.util.ClassUtils;
import com.datastax.driver.core.ConsistencyLevel;
import com.datastax.driver.core.Statement;
import rx.Single;
/**
* Unit tests for {@link ReactivePartTreeCassandraQuery}.
*
@@ -107,12 +105,12 @@ public class ReactivePartTreeCassandraQueryUnitTests {
assertThat(query).isEqualTo("SELECT * FROM person WHERE firstname='foo' ALLOW FILTERING;");
}
@Test // DATACASS-335
@Test // DATACASS-335, DATACASS-313
public void usesDynamicProjection() {
String query = deriveQueryFromMethod("findDynamicallyProjectedBy", PersonProjection.class);
assertThat(query).isEqualTo("SELECT * FROM person;");
assertThat(query).isEqualTo("SELECT lastname,firstname FROM person;");
}
@Test // DATACASS-146