diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java index 9cc79dcb9..6418b2fbd 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraTemplate.java @@ -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 List doSelect(Query query, Class entityClass, CqlIdentifier tableName, Class returnType) { Function 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)); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java index a6f31a14b..6a7566ee7 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplate.java @@ -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 Flux doSelect(Query query, Class entityClass, CqlIdentifier tableName, Class 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 mapper = getMapper(entityClass, returnType, tableName); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java index b85b537fa..1e9055704 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/StatementFactory.java @@ -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 selectors, CqlIdentifier tableName) { + private Select createSelect(Query query, CassandraPersistentEntity entity, Filter filter, List 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 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())); } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQuery.java index 7f23d354c..2c08dbcdc 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQuery.java @@ -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) diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/QueryStatementCreator.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/QueryStatementCreator.java index 77735348b..c75ebdfb5 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/QueryStatementCreator.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/QueryStatementCreator.java @@ -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 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 doWithQuery(CassandraParameterAccessor parameterAccessor, PartTree tree, Function 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()); } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQuery.java index c85b0c986..6c1400935 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQuery.java @@ -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) diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateUnitTests.java index 5c20cafe6..702b30eaa 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraTemplateUnitTests.java @@ -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(); + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplateUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplateUnitTests.java index f872f0ab6..eaa774c8f 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplateUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/ReactiveCassandraTemplateUnitTests.java @@ -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(); + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java index 8f52c8019..327e1de7b 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/PartTreeCassandraQueryUnitTests.java @@ -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 diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQueryUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQueryUnitTests.java index c2c153334..4d67a7a58 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQueryUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/ReactivePartTreeCassandraQueryUnitTests.java @@ -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