From ce356f827fa59a48784ed5659aa59c1c8afe6651 Mon Sep 17 00:00:00 2001 From: Subhashni Balakrishnan Date: Fri, 23 Sep 2016 15:39:59 -0700 Subject: [PATCH] DATACOUCH-246 - Projections support for query derivation. Used the result processor projecting information to just fetch the selected fields and again to post process results to projection object/interfaces. Added test for projection using DTO. Original pull request: #122. --- .../core/CouchbaseTemplateTests.java | 6 +- .../core/CouchbaseTemplateViewListener.java | 2 +- .../data/couchbase/core/TypeKeyTests.java | 2 +- src/main/asciidoc/repository.adoc | 89 ++++++++++++++++++- .../query/AbstractN1qlBasedQuery.java | 32 +++++-- .../query/PartTreeN1qlBasedQuery.java | 19 ++-- .../query/StringN1qlBasedQuery.java | 4 +- .../repository/query/support/N1qlUtils.java | 47 ++++++++-- .../data/couchbase/core/Beer.java | 20 ++++- .../data/couchbase/core/BeerDTO.java | 34 +++++++ .../data/couchbase/core/BeerProjection.java | 10 +++ .../query/PartTreeN1qBasedQueryTest.java | 80 ++++++++++++++++- 12 files changed, 310 insertions(+), 35 deletions(-) create mode 100644 src/test/java/org/springframework/data/couchbase/core/BeerDTO.java create mode 100644 src/test/java/org/springframework/data/couchbase/core/BeerProjection.java diff --git a/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateTests.java b/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateTests.java index df532d6b..d75c22e7 100644 --- a/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateTests.java +++ b/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateTests.java @@ -95,7 +95,7 @@ public class CouchbaseTemplateTests { String id = "beers:awesome-stout"; String name = "The Awesome Stout"; boolean active = false; - Beer beer = new Beer(id).setName(name).setActive(active); + Beer beer = new Beer(id, name, active, ""); template.save(beer); RawJsonDocument resultDoc = client.get(id, RawJsonDocument.class); @@ -163,7 +163,7 @@ public class CouchbaseTemplateTests { @Test public void removeDocument() { String id = "beers:to-delete-stout"; - Beer beer = new Beer(id); + Beer beer = new Beer(id, "", false, ""); template.save(beer); Object result = client.get(id); @@ -208,7 +208,7 @@ public class CouchbaseTemplateTests { String id = "beers:findme-stout"; String name = "The Findme Stout"; boolean active = true; - Beer beer = new Beer(id).setName(name).setActive(active); + Beer beer = new Beer(id, name, active, ""); template.save(beer); Beer found = template.findById(id, Beer.class); diff --git a/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateViewListener.java b/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateViewListener.java index 45a523ae..2530c4cb 100644 --- a/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateViewListener.java +++ b/src/integration/java/org/springframework/data/couchbase/core/CouchbaseTemplateViewListener.java @@ -45,7 +45,7 @@ public class CouchbaseTemplateViewListener extends DependencyInjectionTestExecut CouchbaseTemplate template = new CouchbaseTemplate(clusterInfo, client); for (int i = 0; i < 100; i++) { - Beer b = new Beer("testbeer-" + i).setName("MyBeer " + i).setActive(true); + Beer b = new Beer("testbeer-" + i, "MyBeer" + i, true, ""); template.save(b); } } diff --git a/src/integration/java/org/springframework/data/couchbase/core/TypeKeyTests.java b/src/integration/java/org/springframework/data/couchbase/core/TypeKeyTests.java index d6ffdcda..ff64fdb5 100644 --- a/src/integration/java/org/springframework/data/couchbase/core/TypeKeyTests.java +++ b/src/integration/java/org/springframework/data/couchbase/core/TypeKeyTests.java @@ -59,7 +59,7 @@ public class TypeKeyTests { String id = "beers:awesome-stout"; String name = "The Awesome Stout"; boolean active = false; - Beer beer = new Beer(id).setName(name).setActive(active); + Beer beer = new Beer(id, name, active, ""); template.save(beer); RawJsonDocument resultDoc = client.get(id, RawJsonDocument.class); diff --git a/src/main/asciidoc/repository.adoc b/src/main/asciidoc/repository.adoc index f210d934..c98c9d78 100644 --- a/src/main/asciidoc/repository.adoc +++ b/src/main/asciidoc/repository.adoc @@ -656,4 +656,91 @@ By storing 3 items using a `MyRepository` instance and calling `count()` then `c ---- 100 -3 ----- \ No newline at end of file +---- + +=== DTO Projections +Spring Data Repositories usually return the domain model when using query methods. However, sometimes, you may need to alter the view of that model for various reasons. In this section, you will learn how to define projections to serve up simplified and reduced views of resources. + +Look at the following domain model: + +[source,java] +---- +@Entity +public class Person { + + @Id @GeneratedValue + private Long id; + private String firstName, lastName; + + @OneToOne + private Address address; + … +} + +@Entity +public class Address { + + @Id @GeneratedValue + private Long id; + private String street, state, country; + + … +} +---- + +This `Person` has several attributes: + +* `id` is the primary key +* `firstName` and `lastName` are data attributes +* `address` is a link to another domain object + +Now assume we create a corresponding repository as follows: + +[source,java] +---- +interface PersonRepository extends CrudRepository { + + Person findPersonByFirstName(String firstName); +} +---- + +Spring Data will return the domain object including all of its attributes. There are two options just to retrieve the `address` attribute. One option is to define a repository for `Address` objects like this: + +[source,java] +---- +interface AddressRepository extends CrudRepository {} +---- + +In this situation, using `PersonRepository` will still return the whole `Person` object. Using `AddressRepository` will return just the `Address`. + +However, what if you do not want to expose `address` details at all? You can offer the consumer of your repository service an alternative by defining one or more projections. + +.Simple Projection +==== +[source,java] +---- +interface NoAddresses { <1> + + String getFirstName(); <2> + + String getLastName(); <3> +} +---- +This projection has the following details: + +<1> A plain Java interface making it declarative. +<2> Export the `firstName`. +<3> Export the `lastName`. +==== + +The `NoAddresses` projection only has getters for `firstName` and `lastName` meaning that it will not serve up any address information. The query method definition returns in this case `NoAdresses` instead of `Person`. + +[source,java] +---- +interface PersonRepository extends CrudRepository { + + NoAddresses findByFirstName(String firstName); +} +---- + +Projections declare a contract between the underlying type and the method signatures related to the exposed properties. Hence it is required to name getter methods according to the property name of the underlying type. If the underlying property is named `firstName`, then the getter method must be named `getFirstName` otherwise Spring Data is not able to look up the source property. \ No newline at end of file diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java index 42749bed..6eabb9c6 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/AbstractN1qlBasedQuery.java @@ -20,7 +20,6 @@ import java.util.Collection; import java.util.List; import java.util.Map; -import com.couchbase.client.core.lang.Tuple2; import com.couchbase.client.java.document.json.JsonArray; import com.couchbase.client.java.document.json.JsonObject; import com.couchbase.client.java.document.json.JsonValue; @@ -40,12 +39,17 @@ import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.StreamUtils; import org.springframework.util.Assert; /** * Abstract base for all Couchbase {@link RepositoryQuery}. It is in charge of inspecting the parameters * and choosing the correct {@link N1qlQuery} implementation to use. + * + * @author Simon Baslé + * @author Subhashni Balakrishnan */ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { @@ -53,6 +57,7 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { protected final CouchbaseQueryMethod queryMethod; private final CouchbaseOperations couchbaseOperations; + private Class returnedType; protected AbstractN1qlBasedQuery(CouchbaseQueryMethod queryMethod, CouchbaseOperations couchbaseOperations) { this.queryMethod = queryMethod; @@ -61,6 +66,7 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { /** * The statement for a count() query. This must aggregate using count with the alias {@link CountFragment#COUNT_ALIAS}. + * * @see CountFragment */ protected abstract Statement getCount(ParameterAccessor accessor, Object[] runtimeParameters); @@ -71,14 +77,23 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { */ protected abstract boolean useGeneratedCountQuery(); - protected abstract Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters); + protected abstract Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters, ReturnedType returnedType); protected abstract JsonValue getPlaceholderValues(ParameterAccessor accessor); @Override public Object execute(Object[] parameters) { ParametersParameterAccessor accessor = new ParametersParameterAccessor(queryMethod.getParameters(), parameters); - Statement statement = getStatement(accessor, parameters); + + ResultProcessor processor = this.queryMethod.getResultProcessor().withDynamicProjection(accessor); + ReturnedType returnedType = processor.getReturnedType(); + this.returnedType = returnedType.getReturnedType(); + + if (this.returnedType.isInterface()) { + this.returnedType = returnedType.getDomainType(); + } + + Statement statement = getStatement(accessor, parameters, returnedType); JsonValue queryPlaceholderValues = getPlaceholderValues(accessor); //prepare the final query @@ -89,9 +104,8 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { Statement countStatement = getCount(accessor, parameters); N1qlQuery countQuery = buildQuery(countStatement, queryPlaceholderValues, getCouchbaseOperations().getDefaultConsistency().n1qlConsistency()); - - return executeDependingOnType(query, countQuery, queryMethod, accessor.getPageable(), - queryMethod.isPageQuery(), queryMethod.isSliceQuery(), queryMethod.isModifyingQuery()); + return processor.processResult(executeDependingOnType(query, countQuery, queryMethod, accessor.getPageable(), + queryMethod.isPageQuery(), queryMethod.isSliceQuery(), queryMethod.isModifyingQuery())); } protected static N1qlQuery buildQuery(Statement statement, JsonValue queryPlaceholderValues, ScanConsistency scanConsistency) { @@ -144,7 +158,7 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { protected List executeCollection(N1qlQuery query) { logIfNecessary(query); - List result = couchbaseOperations.findByN1QL(query, queryMethod.getEntityInformation().getJavaType()); + List result = couchbaseOperations.findByN1QL(query, this.returnedType); return result; } @@ -169,14 +183,14 @@ public abstract class AbstractN1qlBasedQuery implements RepositoryQuery { } logIfNecessary(query); - List result = couchbaseOperations.findByN1QL(query, queryMethod.getEntityInformation().getJavaType()); + List result = couchbaseOperations.findByN1QL(query, this.returnedType); return new PageImpl(result, pageable, total); } protected Object executeSliced(N1qlQuery query, N1qlQuery countQuery, Pageable pageable) { Assert.notNull(pageable); logIfNecessary(query); - List result = couchbaseOperations.findByN1QL(query, queryMethod.getEntityInformation().getJavaType()); + List result = couchbaseOperations.findByN1QL(query, this.returnedType); int pageSize = pageable.getPageSize(); boolean hasNext = result.size() > pageSize; diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java index 08ab5b16..025be3b3 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/PartTreeN1qlBasedQuery.java @@ -19,12 +19,7 @@ package org.springframework.data.couchbase.repository.query; import static com.couchbase.client.java.query.Select.select; import static com.couchbase.client.java.query.dsl.Expression.i; import static com.couchbase.client.java.query.dsl.functions.AggregateFunctions.count; -import static com.couchbase.client.java.query.dsl.functions.MetaFunctions.meta; - -import com.couchbase.client.core.lang.Tuple; -import com.couchbase.client.core.lang.Tuple2; import com.couchbase.client.java.document.json.JsonArray; -import com.couchbase.client.java.document.json.JsonObject; import com.couchbase.client.java.document.json.JsonValue; import com.couchbase.client.java.query.Statement; import com.couchbase.client.java.query.dsl.Expression; @@ -36,9 +31,18 @@ import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.repository.query.support.N1qlUtils; import org.springframework.data.domain.Pageable; import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.util.Assert; +/** + * A {@link RepositoryQuery} for Couchbase, based on query derivation + * + * @author Simon Baslé + * @author Subhashni Balakrishnan + */ + public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery { private final PartTree partTree; @@ -63,16 +67,15 @@ public class PartTreeN1qlBasedQuery extends AbstractN1qlBasedQuery { return queryCreator.createQuery(); } @Override - protected Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters) { + protected Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters, ReturnedType returnedType) { String bucketName = getCouchbaseOperations().getCouchbaseBucket().name(); Expression bucket = N1qlUtils.escapedBucket(bucketName); - FromPath select; if (partTree.isCountProjection()) { select = select(count("*")); } else { - select = N1qlUtils.createSelectClauseForEntity(bucketName); + select = N1qlUtils.createSelectClauseForEntity(bucketName, returnedType, this.getCouchbaseOperations().getConverter()); } WherePath selectFrom = select.from(bucket); diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java index 6cbdde31..cf02fdc0 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/StringN1qlBasedQuery.java @@ -34,6 +34,7 @@ import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.common.TemplateParserContext; @@ -53,6 +54,7 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; * along with a variable for {@link #SPEL_FILTER WHERE clause filtering} of the correct entity. * * @author Simon Baslé + * @author Subhashni Balakrishnan */ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery { @@ -264,7 +266,7 @@ public class StringN1qlBasedQuery extends AbstractN1qlBasedQuery { } @Override - public Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters) { + public Statement getStatement(ParameterAccessor accessor, Object[] runtimeParameters, ReturnedType returnedType) { String parsedStatement = parseSpel(this.originalStatement, false, runtimeParameters); return N1qlQuery.simple(parsedStatement).statement(); } diff --git a/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlUtils.java b/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlUtils.java index ca6eb9e2..ea18ceda 100644 --- a/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlUtils.java +++ b/src/main/java/org/springframework/data/couchbase/repository/query/support/N1qlUtils.java @@ -33,10 +33,10 @@ import com.couchbase.client.java.query.dsl.functions.TypeFunctions; import com.couchbase.client.java.query.dsl.path.FromPath; import com.couchbase.client.java.query.dsl.path.WherePath; import com.couchbase.client.java.repository.annotation.Field; - import org.springframework.core.convert.converter.Converter; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; +import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation; import org.springframework.data.couchbase.repository.query.CountFragment; @@ -44,10 +44,14 @@ import org.springframework.data.domain.Sort; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.context.PersistentPropertyPath; import org.springframework.data.repository.core.EntityMetadata; +import org.springframework.data.repository.query.ReturnedType; /** * Utility class to deal with constructing well formed N1QL queries around Spring Data entities, so that * the framework can use N1QL to find such entities (eg. restrict the bucket search to a particular type). + * + * @author Simon Baslé + * @author Subhashni Balakrishnan */ public class N1qlUtils { @@ -70,6 +74,41 @@ public class N1qlUtils { return i(bucketName); } + /** + * Produce a {@link Statement} that corresponds to the SELECT clause for looking for Spring Data entities + * stored in Couchbase. Notably it will select the content of the document AND its id and cas and use custom + * construction of query if required. + * + * @param bucketName the bucket that stores the entity documents (will be escaped). + * @param returnedType Returned type projection information from result processor. + * @param converter couchbase converter + * @return the needed SELECT clause of the statement. + */ + public static FromPath createSelectClauseForEntity(String bucketName, ReturnedType returnedType, CouchbaseConverter converter) { + Expression bucket = escapedBucket(bucketName); + Expression metaId = path(meta(bucket), "id").as(CouchbaseOperations.SELECT_ID); + Expression metaCas = path(meta(bucket), "cas").as(CouchbaseOperations.SELECT_CAS); + List expList = new ArrayList(); + expList.add(metaId); + expList.add(metaCas); + + if (returnedType != null && returnedType.needsCustomConstruction()) { + List properties = returnedType.getInputProperties(); + CouchbasePersistentEntity entity = converter.getMappingContext().getPersistentEntity(returnedType.getDomainType()); + + for (String property : properties) { + expList.add(path(bucket, i(entity.getPersistentProperty(property).getFieldName()))); + } + } else { + expList.add(path(bucket, "*")); + } + + Expression[] propertiesExp = new Expression[expList.size()]; + propertiesExp = expList.toArray(propertiesExp); + + return select(propertiesExp); + } + /** * Produce a {@link Statement} that corresponds to the SELECT clause for looking for Spring Data entities * stored in Couchbase. Notably it will select the content of the document AND its id and cas. @@ -78,11 +117,7 @@ public class N1qlUtils { * @return the needed SELECT clause of the statement. */ public static FromPath createSelectClauseForEntity(String bucketName) { - Expression bucket = escapedBucket(bucketName); - Expression metaId = path(meta(bucket), "id").as(CouchbaseOperations.SELECT_ID); - Expression metaCas = path(meta(bucket), "cas").as(CouchbaseOperations.SELECT_CAS); - - return select(metaId, metaCas, path(bucket, "*")); + return createSelectClauseForEntity(bucketName, null, null); } /** diff --git a/src/test/java/org/springframework/data/couchbase/core/Beer.java b/src/test/java/org/springframework/data/couchbase/core/Beer.java index 59064f46..d203441a 100644 --- a/src/test/java/org/springframework/data/couchbase/core/Beer.java +++ b/src/test/java/org/springframework/data/couchbase/core/Beer.java @@ -24,6 +24,7 @@ import com.couchbase.client.java.repository.annotation.Field; * Test class for persisting and loading from {@link CouchbaseTemplate}. * * @author Michael Nitschinger + * @author Subhashni Balakrishnan */ public class Beer { @@ -35,13 +36,19 @@ public class Beer { @Field("is_active") private boolean active = true; - public Beer(String id) { + @Field("desc") + private String description; + + public Beer(String id, String name, Boolean active, String description) { this.id = id; + this.name = name; + this.active = active; + this.description = description; } @Override public String toString() { - return "Beer [id=" + id + ", name=" + name + ", active=" + active + "]"; + return "Beer [id=" + id + ", name=" + name + ", active=" + active + ", description=" + description + "]"; } public Beer setName(String name) { @@ -62,6 +69,15 @@ public class Beer { return active; } + public Beer setDescription(String description) { + this.description = description; + return this; + } + + public String getDescription() { + return description; + } + public String getId() { return id; } diff --git a/src/test/java/org/springframework/data/couchbase/core/BeerDTO.java b/src/test/java/org/springframework/data/couchbase/core/BeerDTO.java new file mode 100644 index 00000000..3abfcb67 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/core/BeerDTO.java @@ -0,0 +1,34 @@ +package org.springframework.data.couchbase.core; + +import com.couchbase.client.java.repository.annotation.Field; + +/** + * Test DTO for projecting from {@link CouchbaseTemplate}. + * + * @author Subhashni Balakrishnan + */ +public class BeerDTO{ + private String name; + + @Field("desc") + private String description; + + public BeerDTO(String name, String description) { + this.name = name; + this.description = description; + } + + public BeerDTO setName(String name) { + this.name = name; + return this; + } + + public String getName() { return name; } + + public BeerDTO setDescription(String description) { + this.description = description; + return this; + } + + public String getDescription() { return description; } +} diff --git a/src/test/java/org/springframework/data/couchbase/core/BeerProjection.java b/src/test/java/org/springframework/data/couchbase/core/BeerProjection.java new file mode 100644 index 00000000..a241282f --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/core/BeerProjection.java @@ -0,0 +1,10 @@ +package org.springframework.data.couchbase.core; + +/** + * Test interface for projecting data from {@link CouchbaseTemplate}. + * + * @author Subhashni Balakrishnan + */ +public interface BeerProjection { + String getDescription(); +} diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/PartTreeN1qBasedQueryTest.java b/src/test/java/org/springframework/data/couchbase/repository/query/PartTreeN1qBasedQueryTest.java index 01d39760..82931dd6 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/PartTreeN1qBasedQueryTest.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/PartTreeN1qBasedQueryTest.java @@ -6,10 +6,16 @@ import static org.mockito.Mockito.*; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.Collection; +import java.util.List; import org.junit.Test; +import org.springframework.data.couchbase.core.BeerDTO; +import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; +import org.springframework.data.mapping.*; import org.springframework.core.convert.converter.Converter; import org.springframework.data.couchbase.core.Beer; +import org.springframework.data.couchbase.core.BeerProjection; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.core.convert.CouchbaseConverter; import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty; @@ -18,10 +24,10 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; -import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.PersistentPropertyPath; import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.core.EntityMetadata; import org.springframework.data.repository.core.RepositoryMetadata; @@ -30,6 +36,7 @@ import org.springframework.data.repository.query.ParameterAccessor; import com.couchbase.client.java.CouchbaseBucket; import com.couchbase.client.java.query.Statement; +import org.springframework.data.repository.query.ResultProcessor; public class PartTreeN1qBasedQueryTest { @@ -117,12 +124,79 @@ public class PartTreeN1qBasedQueryTest { } + @Test + public void testProjectionInterface() throws Exception { + + CouchbaseOperations couchbaseOperations = mock(CouchbaseOperations.class); + CouchbaseBucket couchbaseBucket = mock(CouchbaseBucket.class); + CouchbaseConverter couchbaseConverter = mock(CouchbaseConverter.class); + EntityMetadata entityInformation = mock(EntityMetadata.class); + ParameterAccessor accessor = mock(ParameterAccessor.class); + + ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); + factory.createProjection(BeerProjection.class); + MappingContext mappingContext = new CouchbaseMappingContext(); + RepositoryMetadata metadata = new DefaultRepositoryMetadata(TestRepository.class); + Method method = TestRepository.class.getMethod("findAllProjectedBy"); + CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, factory, mappingContext); + ResultProcessor processor = queryMethod.getResultProcessor().withDynamicProjection(accessor); + + + when(entityInformation.getJavaType()).thenReturn(Beer.class); + when(couchbaseOperations.getCouchbaseBucket()).thenReturn(couchbaseBucket); + when(couchbaseBucket.name()).thenReturn("B"); + when(couchbaseOperations.getConverter()).thenReturn(couchbaseConverter); + when(couchbaseOperations.getConverter().getMappingContext()).thenReturn(mappingContext); + when(couchbaseConverter.getTypeKey()).thenReturn("_class"); + when(couchbaseConverter.convertForWriteIfNeeded(eq("value"))).thenReturn("value"); + + PartTreeN1qlBasedQuery query = new PartTreeN1qlBasedQuery(queryMethod, couchbaseOperations); + Statement statement = query.getStatement(accessor, null, processor.getReturnedType()); + + assertEquals("SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS, `B`.`desc` FROM `B` WHERE " + + "`_class` = \"org.springframework.data.couchbase.core.Beer\"", statement.toString()); + + } + + @Test + public void testProjectionDTO() throws Exception { + CouchbaseOperations couchbaseOperations = mock(CouchbaseOperations.class); + CouchbaseBucket couchbaseBucket = mock(CouchbaseBucket.class); + CouchbaseConverter couchbaseConverter = mock(CouchbaseConverter.class); + EntityMetadata entityInformation = mock(EntityMetadata.class); + ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); + ParameterAccessor accessor = mock(ParameterAccessor.class); + + RepositoryMetadata metadata = new DefaultRepositoryMetadata(TestRepository.class); + Method method = TestRepository.class.getMethod("findAllDtoedBy"); + MappingContext mappingContext = new CouchbaseMappingContext(); + CouchbaseQueryMethod queryMethod = new CouchbaseQueryMethod(method, metadata, factory, mappingContext); + ResultProcessor processor = queryMethod.getResultProcessor().withDynamicProjection(accessor); + + when(entityInformation.getJavaType()).thenReturn(Beer.class); + when(couchbaseOperations.getCouchbaseBucket()).thenReturn(couchbaseBucket); + when(couchbaseBucket.name()).thenReturn("B"); + when(couchbaseOperations.getConverter()).thenReturn(couchbaseConverter); + when(couchbaseOperations.getConverter().getMappingContext()).thenReturn(mappingContext); + when(couchbaseConverter.getTypeKey()).thenReturn("_class"); + + PartTreeN1qlBasedQuery query = new PartTreeN1qlBasedQuery(queryMethod, couchbaseOperations); + Statement statement = query.getStatement(accessor, null, processor.getReturnedType()); + + assertEquals("SELECT META(`B`).id AS _ID, META(`B`).cas AS _CAS, `B`.`name`, `B`.`desc` FROM `B` " + + "WHERE `_class` = \"org.springframework.data.couchbase.core.Beer\"", statement.toString()); + + } + public static interface TestRepository extends CrudRepository { Page findByNameOrderByName(String name, Pageable pageRequest); Page findByName(String name, Pageable pageRequest); - } + Collection findAllProjectedBy(); -} + List findAllDtoedBy(); + + } +} \ No newline at end of file