diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java index b41668579..3a3127be0 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/AbstractCassandraQuery.java @@ -15,6 +15,8 @@ */ package org.springframework.data.cassandra.repository.query; +import lombok.RequiredArgsConstructor; + import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -37,6 +39,7 @@ import org.springframework.data.cassandra.repository.query.CassandraQueryExecuti import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.ResultSetQuery; import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.SingleEntityExecution; import org.springframework.data.cassandra.repository.query.CassandraQueryExecution.StreamExecution; +import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; @@ -59,6 +62,7 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { private final CassandraQueryMethod queryMethod; private final CassandraOperations template; + private final EntityInstantiators instantiators; /** * Creates a new {@link AbstractCassandraQuery} from the given {@link CassandraQueryMethod} and @@ -74,6 +78,7 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { this.queryMethod = queryMethod; this.template = operations; + this.instantiators = new EntityInstantiators(); } /* (non-Javadoc) @@ -93,12 +98,12 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { CassandraParameterAccessor parameterAccessor = new ConvertingParameterAccessor(template.getConverter(), new CassandraParametersParameterAccessor(queryMethod, parameters)); - String query = createQuery(parameterAccessor); - ResultProcessor resultProcessor = queryMethod.getResultProcessor().withDynamicProjection(parameterAccessor); + String query = createQuery(parameterAccessor); + CassandraQueryExecution queryExecution = getExecution(query, parameterAccessor, - new ResultProcessingConverter(resultProcessor)); + new ResultProcessingConverter(resultProcessor, template.getConverter().getMappingContext(), instantiators)); CassandraReturnedType returnedType = new CassandraReturnedType(resultProcessor.getReturnedType(), template.getConverter().getCustomConversions()); @@ -237,16 +242,12 @@ public abstract class AbstractCassandraQuery implements RepositoryQuery { */ protected abstract String createQuery(CassandraParameterAccessor accessor); + @RequiredArgsConstructor private class CassandraReturnedType { private final ReturnedType returnedType; private final CustomConversions customConversions; - CassandraReturnedType(ReturnedType returnedType, CustomConversions customConversions) { - this.returnedType = returnedType; - this.customConversions = customConversions; - } - boolean isProjecting() { if (!returnedType.isProjecting()) { diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreator.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreator.java index b3dc0f3cd..e4df2b2ea 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreator.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreator.java @@ -23,6 +23,7 @@ import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.cassandra.mapping.CassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; @@ -53,14 +54,12 @@ import com.datastax.driver.core.querybuilder.Select; class CassandraQueryCreator extends AbstractQueryCreator { private static final Pattern PUNCTUATION_PATTERN = Pattern.compile("\\p{Punct}"); - private static final Logger LOG = LoggerFactory.getLogger(CassandraQueryCreator.class); private final CassandraMappingContext mappingContext; - private final CassandraPersistentEntity entity; - private final WhereBuilder whereBuilder = new WhereBuilder(); + private final CqlIdentifier tableName; /** * Creates a new {@link CassandraQueryCreator} from the given {@link PartTree}, {@link ConvertingParameterAccessor} @@ -72,15 +71,16 @@ class CassandraQueryCreator extends AbstractQueryCreator { * @param entityMetadata must not be {@literal null}. */ public CassandraQueryCreator(PartTree tree, CassandraParameterAccessor accessor, - CassandraMappingContext mappingContext, EntityMetadata entityMetadata) { + CassandraMappingContext mappingContext, CassandraEntityMetadata entityMetadata) { super(tree, accessor); Assert.notNull(mappingContext, "CassandraMappingContext must not be null"); - Assert.notNull(entityMetadata, "EntityMetaData must not be null"); + Assert.notNull(entityMetadata, "CassandraEntityMetadata must not be null"); this.mappingContext = mappingContext; this.entity = mappingContext.getPersistentEntity(entityMetadata.getJavaType()); + this.tableName = entityMetadata.getTableName(); } /* (non-Javadoc) @@ -131,7 +131,7 @@ class CassandraQueryCreator extends AbstractQueryCreator { whereBuilder.and(criteria); } - Select select = StatementBuilder.select(entity, whereBuilder, sort); + Select select = StatementBuilder.select(entity, tableName, whereBuilder, sort); if (LOG.isDebugEnabled()) { LOG.debug("Created query {}", select); @@ -253,9 +253,9 @@ class CassandraQueryCreator extends AbstractQueryCreator { * Build a {@link Select} statement from the given {@link WhereBuilder} and {@link Sort}. Resolves property names * for {@link Sort} using the {@link CassandraPersistentEntity}. */ - static Select select(CassandraPersistentEntity entity, WhereBuilder whereBuilder, Sort sort) { + static Select select(CassandraPersistentEntity entity, CqlIdentifier tableName, WhereBuilder whereBuilder, Sort sort) { - Select select = QueryBuilder.select().from(entity.getTableName().toCql()); + Select select = QueryBuilder.select().from(tableName.toCql()); whereBuilder.build(select.where()); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryExecution.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryExecution.java index fe6ec3c43..536450ceb 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryExecution.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryExecution.java @@ -16,18 +16,20 @@ package org.springframework.data.cassandra.repository.query; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + import java.util.function.Function; import org.springframework.core.convert.converter.Converter; import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.StreamUtils; import org.springframework.util.ClassUtils; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; - /** * Query executions for Cassandra. * @@ -151,6 +153,8 @@ interface CassandraQueryExecution { final class ResultProcessingConverter implements Converter { private final @NonNull ResultProcessor processor; + private final @NonNull CassandraMappingContext mappingContext; + private final @NonNull EntityInstantiators instantiators; /* (non-Javadoc) * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) @@ -164,7 +168,14 @@ interface CassandraQueryExecution { return source; } - return processor.processResult(source); + if (source != null && returnedType.isInstance(source)) { + return source; + } + + Converter converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), + mappingContext, instantiators); + + return processor.processResult(source, converter); } } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java index 36dc4f4a5..3d1c863fc 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/CassandraQueryMethod.java @@ -96,6 +96,7 @@ public class CassandraQueryMethod extends QueryMethod { mappingContext.getPersistentEntity(domainClass)); } else { + CassandraPersistentEntity returnedEntity = mappingContext.getPersistentEntity(returnedObjectType); CassandraPersistentEntity managedEntity = mappingContext.getPersistentEntity(domainClass); diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/DtoInstantiatingConverter.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/DtoInstantiatingConverter.java new file mode 100644 index 000000000..8ad8d4cc4 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/query/DtoInstantiatingConverter.java @@ -0,0 +1,108 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.cassandra.repository.query; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; +import org.springframework.data.cassandra.mapping.CassandraPersistentProperty; +import org.springframework.data.convert.EntityInstantiator; +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.SimplePropertyHandler; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.util.Assert; + +/** + * {@link Converter} to instantiate DTOs from fully equipped domain objects. + * + * @author Mark Paluch + */ +class DtoInstantiatingConverter implements Converter { + + private final Class targetType; + private final MappingContext, ? extends PersistentProperty> context; + private final EntityInstantiator instantiator; + + /** + * Creates a new {@link Converter} to instantiate DTOs. + * + * @param dtoType must not be {@literal null}. + * @param context must not be {@literal null}. + * @param instantiators must not be {@literal null}. + */ + DtoInstantiatingConverter(Class dtoType, + MappingContext, CassandraPersistentProperty> context, + EntityInstantiators instantiator) { + + Assert.notNull(dtoType, "DTO type must not be null!"); + Assert.notNull(context, "MappingContext must not be null!"); + Assert.notNull(instantiator, "EntityInstantiators must not be null!"); + + this.targetType = dtoType; + this.context = context; + this.instantiator = instantiator.getInstantiatorFor(context.getPersistentEntity(dtoType)); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + if (targetType.isInterface()) { + return source; + } + + final PersistentEntity sourceEntity = context.getPersistentEntity(source.getClass()); + final PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(source); + final PersistentEntity targetEntity = context.getPersistentEntity(targetType); + final PreferredConstructor> constructor = targetEntity + .getPersistenceConstructor(); + + @SuppressWarnings({ "rawtypes", "unchecked" }) + Object dto = instantiator.createInstance(targetEntity, new ParameterValueProvider() { + + @Override + public Object getParameterValue(Parameter parameter) { + return sourceAccessor.getProperty(sourceEntity.getPersistentProperty(parameter.getName())); + } + }); + + final PersistentPropertyAccessor dtoAccessor = targetEntity.getPropertyAccessor(dto); + + targetEntity.doWithProperties(new SimplePropertyHandler() { + + @Override + public void doWithPersistentProperty(PersistentProperty property) { + + if (constructor.isConstructorParameter(property)) { + return; + } + + dtoAccessor.setProperty(property, + sourceAccessor.getProperty(sourceEntity.getPersistentProperty(property.getName()))); + } + }); + + return dto; + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreatorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreatorUnitTests.java index 41ab02dde..9f070206d 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreatorUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/repository/query/CassandraQueryCreatorUnitTests.java @@ -36,12 +36,13 @@ import org.springframework.data.cassandra.convert.MappingCassandraConverter; import org.springframework.data.cassandra.domain.Person; import org.springframework.data.cassandra.mapping.BasicCassandraMappingContext; import org.springframework.data.cassandra.mapping.CassandraMappingContext; +import org.springframework.data.cassandra.mapping.CassandraPersistentEntity; import org.springframework.data.cassandra.mapping.Column; import org.springframework.data.cassandra.mapping.PrimaryKey; import org.springframework.data.cassandra.mapping.PrimaryKeyClass; import org.springframework.data.cassandra.mapping.PrimaryKeyColumn; import org.springframework.data.cassandra.mapping.Table; -import org.springframework.data.repository.core.EntityMetadata; +import org.springframework.data.cassandra.repository.support.MappingCassandraEntityInformation; import org.springframework.data.repository.query.parser.PartTree; /** @@ -350,13 +351,10 @@ public class CassandraQueryCreatorUnitTests { return creator.createQuery().toString(); } - private EntityMetadata getEntityInformation(final Class entityClass) { - return new EntityMetadata() { - @Override - public Class getJavaType() { - return entityClass; - } - }; + @SuppressWarnings("unchecked") + private CassandraEntityInformation getEntityInformation(final Class entityClass) { + return new MappingCassandraEntityInformation( + (CassandraPersistentEntity) context.getPersistentEntity(entityClass), converter); } @Table diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/PersonRepository.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/PersonRepository.java index d15748ab8..f5a286aec 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/PersonRepository.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/PersonRepository.java @@ -19,6 +19,8 @@ import java.time.LocalDate; import java.util.Collection; import java.util.List; +import lombok.Data; +import lombok.Getter; import org.springframework.data.cassandra.repository.CassandraRepository; import org.springframework.data.cassandra.repository.Query; import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Address; @@ -53,6 +55,10 @@ interface PersonRepository extends CassandraRepository { Collection findPersonProjectedBy(); + Collection findPersonDtoBy(); + + T findDtoByNicknameStartsWith(String prefix, Class projectionType); + @Query("select * from person where firstname = ?0 and lastname = 'White'") List findByFirstname(String firstname); @@ -66,4 +72,15 @@ interface PersonRepository extends CassandraRepository { String getLastname(); } + + static class PersonDto { + + public String firstname, lastname; + + public PersonDto(String firstname, String lastname) { + + this.firstname = firstname; + this.lastname = lastname; + } + } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/QueryDerivationIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/QueryDerivationIntegrationTests.java index 36b0d1ed0..3f988dc5c 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/QueryDerivationIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/repository/querymethods/derived/QueryDerivationIntegrationTests.java @@ -36,6 +36,7 @@ import org.springframework.data.cassandra.repository.config.EnableCassandraRepos import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Address; import org.springframework.data.cassandra.test.integration.repository.querymethods.declared.Person; import org.springframework.data.cassandra.test.integration.repository.querymethods.derived.PersonRepository.NumberOfChildren; +import org.springframework.data.cassandra.test.integration.repository.querymethods.derived.PersonRepository.PersonDto; import org.springframework.data.cassandra.test.integration.repository.querymethods.derived.PersonRepository.PersonProjection; import org.springframework.data.cassandra.test.integration.support.AbstractSpringDataEmbeddedCassandraIntegrationTest; import org.springframework.data.cassandra.test.integration.support.CassandraVersion; @@ -175,13 +176,45 @@ public class QueryDerivationIntegrationTests extends AbstractSpringDataEmbeddedC * @see DATACASS-7 */ @Test - public void executesCollectionQueryWithProjectionCorrectly() { + public void executesCollectionQueryWithProjection() { Collection collection = personRepository.findPersonProjectedBy(); assertThat(collection).hasSize(3).extracting("lastname").contains("White", "White", "White"); } + /** + * @see DATACASS-359 + */ + @Test + public void executesCollectionQueryWithDtoProjection() { + + Collection collection = personRepository.findPersonDtoBy(); + + assertThat(collection).hasSize(3).extracting("lastname").contains("White", "White", "White"); + } + + /** + * @see DATACASS-359 + */ + @Test + public void executesCollectionQueryWithDtoDynamicallyProjected() throws Exception { + + template.execute( + "CREATE CUSTOM INDEX IF NOT EXISTS fn_starts_with ON person (nickname) USING 'org.apache.cassandra.index.sasi.SASIIndex';"); + + // Give Cassandra some time to build the index + Thread.sleep(500); + + walter.setNickname("Heisenberg"); + personRepository.save(walter); + + PersonDto heisenberg = personRepository.findDtoByNicknameStartsWith("Heisen", PersonDto.class); + + assertThat(heisenberg.firstname).isEqualTo("Walter"); + assertThat(heisenberg.lastname).isEqualTo("White"); + } + /** * @see DATACASS-7 */