DATACASS-359 - Support DTO projections.

We now support DTO projections for query methods. DTO projection selects records from Cassandra and applies projected results on the DTO. DTOs are plain Java objects that fit to the underlying entity.

@Table
class Person {

  @PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED, ordinal = 0) private String lastname;
  @PrimaryKeyColumn(type = PrimaryKeyType.CLUSTERED, ordinal = 1) private String firstname;

  private String nickname;
  private Date birthDate;

  // more columns
}

interface PersonRepository extends CrudRepository<Person, String> {
  Collection<PersonDto> findPersonDtoBy();

  <T> T findDtoByFirstnameStartsWith(String prefix, Class<T> projectionType);
}

class PersonDto {
  public String firstname, lastname;

  public PersonDto(String firstname, String lastname) {

    this.firstname = firstname;
    this.lastname = lastname;
  }
}
This commit is contained in:
Mark Paluch
2016-11-28 17:05:59 +01:00
committed by John Blum
parent 8543846dea
commit 9591e1f4fc
8 changed files with 198 additions and 29 deletions

View File

@@ -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()) {

View File

@@ -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<Select, Clause> {
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<Select, Clause> {
* @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<Select, Clause> {
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<Select, Clause> {
* 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());

View File

@@ -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<Object, Object> {
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<Object, Object> converter = new DtoInstantiatingConverter(returnedType.getReturnedType(),
mappingContext, instantiators);
return processor.processResult(source, converter);
}
}
}

View File

@@ -96,6 +96,7 @@ public class CassandraQueryMethod extends QueryMethod {
mappingContext.getPersistentEntity(domainClass));
} else {
CassandraPersistentEntity<?> returnedEntity = mappingContext.getPersistentEntity(returnedObjectType);
CassandraPersistentEntity<?> managedEntity = mappingContext.getPersistentEntity(domainClass);

View File

@@ -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<Object, Object> {
private final Class<?> targetType;
private final MappingContext<? extends PersistentEntity<?, ?>, ? 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<? extends CassandraPersistentEntity<?>, 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<?, ? extends PersistentProperty<?>> 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;
}
}

View File

@@ -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 <T> EntityMetadata<T> getEntityInformation(final Class<T> entityClass) {
return new EntityMetadata<T>() {
@Override
public Class<T> getJavaType() {
return entityClass;
}
};
@SuppressWarnings("unchecked")
private <T> CassandraEntityInformation<T, Serializable> getEntityInformation(final Class<T> entityClass) {
return new MappingCassandraEntityInformation<T, Serializable>(
(CassandraPersistentEntity) context.getPersistentEntity(entityClass), converter);
}
@Table

View File

@@ -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<Person> {
Collection<PersonProjection> findPersonProjectedBy();
Collection<PersonDto> findPersonDtoBy();
<T> T findDtoByNicknameStartsWith(String prefix, Class<T> projectionType);
@Query("select * from person where firstname = ?0 and lastname = 'White'")
List<Person> findByFirstname(String firstname);
@@ -66,4 +72,15 @@ interface PersonRepository extends CassandraRepository<Person> {
String getLastname();
}
static class PersonDto {
public String firstname, lastname;
public PersonDto(String firstname, String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
}
}

View File

@@ -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 <a href="https://jira.spring.io/browse/DATACASS-7">DATACASS-7</a>
*/
@Test
public void executesCollectionQueryWithProjectionCorrectly() {
public void executesCollectionQueryWithProjection() {
Collection<PersonProjection> collection = personRepository.findPersonProjectedBy();
assertThat(collection).hasSize(3).extracting("lastname").contains("White", "White", "White");
}
/**
* @see <a href="https://jira.spring.io/browse/DATACASS-359">DATACASS-359</a>
*/
@Test
public void executesCollectionQueryWithDtoProjection() {
Collection<PersonDto> collection = personRepository.findPersonDtoBy();
assertThat(collection).hasSize(3).extracting("lastname").contains("White", "White", "White");
}
/**
* @see <a href="https://jira.spring.io/browse/DATACASS-359">DATACASS-359</a>
*/
@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 <a href="https://jira.spring.io/browse/DATACASS-7">DATACASS-7</a>
*/