diff --git a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/core/CosmosOperations.java b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/core/CosmosOperations.java index ca3092abf1b1a..fe9732f49f7d9 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/core/CosmosOperations.java +++ b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/core/CosmosOperations.java @@ -5,6 +5,7 @@ import com.azure.cosmos.models.CosmosContainerProperties; import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.SqlQuerySpec; import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; import com.azure.spring.data.cosmos.core.query.CosmosQuery; import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; @@ -274,4 +275,15 @@ public interface CosmosOperations { * @return MappingCosmosConverter */ MappingCosmosConverter getConverter(); + + /** + * Run the query. + * + * @param the type parameter + * @param querySpec the query spec + * @param domainType the domain type + * @param returnType the return type + * @return the Iterable + */ + Iterable runQuery(SqlQuerySpec querySpec, Class domainType, Class returnType); } diff --git a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/core/CosmosTemplate.java b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/core/CosmosTemplate.java index 0223389dc7578..10895eac62caf 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/core/CosmosTemplate.java +++ b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/core/CosmosTemplate.java @@ -720,6 +720,13 @@ public MappingCosmosConverter getConverter() { return this.mappingCosmosConverter; } + @Override + public Iterable runQuery(SqlQuerySpec querySpec, Class domainType, Class returnType) { + return getJsonNodeFluxFromQuerySpec(domainType.getSimpleName(), querySpec, returnType) + .collectList() + .block(); + } + private JsonNode prepareToPersistAndConvertToItemProperties(Object object) { if (cosmosAuditingHandler != null) { cosmosAuditingHandler.markAudited(object); @@ -766,13 +773,35 @@ private Flux findItemsAsFlux(@NonNull CosmosQuery query, .publishOn(Schedulers.parallel()) .flatMap(cosmosItemFeedResponse -> { CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, - cosmosItemFeedResponse.getCosmosDiagnostics(), cosmosItemFeedResponse); + cosmosItemFeedResponse.getCosmosDiagnostics(), + cosmosItemFeedResponse); return Flux.fromIterable(cosmosItemFeedResponse.getResults()); }) .onErrorResume(throwable -> CosmosExceptionUtils.exceptionHandler("Failed to find items", throwable)); } + private Flux getJsonNodeFluxFromQuerySpec( + @NonNull String containerName, SqlQuerySpec sqlQuerySpec, Class classType) { + final CosmosQueryRequestOptions cosmosQueryRequestOptions = new CosmosQueryRequestOptions(); + cosmosQueryRequestOptions.setQueryMetricsEnabled(this.queryMetricsEnabled); + + return cosmosAsyncClient + .getDatabase(this.databaseName) + .getContainer(containerName) + .queryItems(sqlQuerySpec, cosmosQueryRequestOptions, classType) + .byPage() + .publishOn(Schedulers.parallel()) + .flatMap(cosmosItemFeedResponse -> { + CosmosUtils.fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemFeedResponse.getCosmosDiagnostics(), + cosmosItemFeedResponse); + return Flux.fromIterable(cosmosItemFeedResponse.getResults()); + }) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to find items", throwable)); + } + private List findItems(@NonNull CosmosQuery query, @NonNull String containerName) { return findItemsAsFlux(query, containerName) diff --git a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosOperations.java b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosOperations.java index 8024a890974e4..584966fdd08fe 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosOperations.java +++ b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosOperations.java @@ -5,6 +5,7 @@ import com.azure.cosmos.models.CosmosContainerResponse; import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.SqlQuerySpec; import com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter; import com.azure.spring.data.cosmos.core.query.CosmosQuery; import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; @@ -242,4 +243,15 @@ public interface ReactiveCosmosOperations { * @return MappingCosmosConverter */ MappingCosmosConverter getConverter(); + + /** + * Run the query. + * + * @param the type parameter + * @param querySpec the query spec + * @param domainType the domain type + * @param returnType the return type + * @return the flux + */ + Flux runQuery(SqlQuerySpec querySpec, Class domainType, Class returnType); } diff --git a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplate.java b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplate.java index 110155b5cb11f..feb23a344ceb8 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplate.java +++ b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/core/ReactiveCosmosTemplate.java @@ -578,6 +578,26 @@ public MappingCosmosConverter getConverter() { return mappingCosmosConverter; } + @Override + public Flux runQuery(SqlQuerySpec querySpec, Class domainType, Class returnType) { + String containerName = domainType.getSimpleName(); + CosmosQueryRequestOptions options = new CosmosQueryRequestOptions(); + return cosmosAsyncClient.getDatabase(this.databaseName) + .getContainer(containerName) + .queryItems(querySpec, options, returnType) + .byPage() + .publishOn(Schedulers.parallel()) + .flatMap(cosmosItemFeedResponse -> { + CosmosUtils + .fillAndProcessResponseDiagnostics(this.responseDiagnosticsProcessor, + cosmosItemFeedResponse.getCosmosDiagnostics(), + cosmosItemFeedResponse); + return Flux.fromIterable(cosmosItemFeedResponse.getResults()); + }) + .onErrorResume(throwable -> + CosmosExceptionUtils.exceptionHandler("Failed to find items", throwable)); + } + private Mono getCountValue(CosmosQuery query, String containerName) { final SqlQuerySpec querySpec = new CountQueryGenerator().generateCosmos(query); final CosmosQueryRequestOptions options = new CosmosQueryRequestOptions(); diff --git a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/Query.java b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/Query.java new file mode 100644 index 0000000000000..fb181ae431406 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/Query.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository; +import org.springframework.data.annotation.QueryAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to declare finder queries directly on repository methods. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@QueryAnnotation +public @interface Query { + /** + * value of the query + * + * @return the value + */ + String value() default ""; +} diff --git a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/query/AbstractCosmosQuery.java b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/query/AbstractCosmosQuery.java index 98a99c4a8d11b..792a985313225 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/query/AbstractCosmosQuery.java +++ b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/query/AbstractCosmosQuery.java @@ -14,7 +14,7 @@ public abstract class AbstractCosmosQuery implements RepositoryQuery { private final CosmosQueryMethod method; - private final CosmosOperations operations; + protected final CosmosOperations operations; /** * Initialization @@ -33,6 +33,7 @@ public AbstractCosmosQuery(CosmosQueryMethod method, CosmosOperations operations * @param parameters must not be {@literal null}. * @return execution result. Can be {@literal null}. */ + @Override public Object execute(Object[] parameters) { final CosmosParameterAccessor accessor = new CosmosParameterParameterAccessor(method, parameters); final CosmosQuery query = createQuery(accessor); diff --git a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/query/AbstractReactiveCosmosQuery.java b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/query/AbstractReactiveCosmosQuery.java index 6986db4ce5f61..c718f8f847049 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/query/AbstractReactiveCosmosQuery.java +++ b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/query/AbstractReactiveCosmosQuery.java @@ -15,7 +15,7 @@ public abstract class AbstractReactiveCosmosQuery implements RepositoryQuery { private final ReactiveCosmosQueryMethod method; - private final ReactiveCosmosOperations operations; + protected final ReactiveCosmosOperations operations; /** * Initialization @@ -35,6 +35,7 @@ public AbstractReactiveCosmosQuery(ReactiveCosmosQueryMethod method, * @param parameters must not be {@literal null}. * @return execution result. Can be {@literal null}. */ + @Override public Object execute(Object[] parameters) { final ReactiveCosmosParameterAccessor accessor = new ReactiveCosmosParameterParameterAccessor(method, parameters); diff --git a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosQueryMethod.java b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosQueryMethod.java index ebce667e0520a..2e1ed6ab31939 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosQueryMethod.java +++ b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/query/CosmosQueryMethod.java @@ -2,13 +2,18 @@ // Licensed under the MIT License. package com.azure.spring.data.cosmos.repository.query; +import com.azure.spring.data.cosmos.repository.Query; import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.EntityMetadata; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; import java.lang.reflect.Method; +import java.util.Optional; /** * Inherit QueryMethod class to generate a method that is designated to execute a finder query. @@ -16,6 +21,7 @@ public class CosmosQueryMethod extends QueryMethod { private CosmosEntityMetadata metadata; + private final String annotatedQueryValue; /** * Creates a new {@link CosmosQueryMethod} from the given parameters. Looks up the correct query to use @@ -27,6 +33,7 @@ public class CosmosQueryMethod extends QueryMethod { */ public CosmosQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { super(method, metadata, factory); + this.annotatedQueryValue = findAnnotatedQuery(method).orElse(null); } @Override @@ -39,4 +46,32 @@ public EntityMetadata getEntityInformation() { this.metadata = new SimpleCosmosEntityMetadata(domainType, entityInformation); return this.metadata; } + + /** + * Returns whether the method has an annotated query. + * + * @return if the query method has an annotated query + */ + public boolean hasAnnotatedQuery() { + return annotatedQueryValue != null; + } + + /** + * Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation + * found + * nor the attribute was specified. + * + * @return the query string or null + */ + @Nullable + public String getQueryAnnotation() { + return annotatedQueryValue; + } + + private Optional findAnnotatedQuery(Method method) { + return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Query.class)) + .map(Query::value) + .filter(StringUtils::hasText); + } + } diff --git a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosQueryMethod.java b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosQueryMethod.java index 2102e988b7955..5b2755257164d 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosQueryMethod.java +++ b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/query/ReactiveCosmosQueryMethod.java @@ -2,15 +2,20 @@ // Licensed under the MIT License. package com.azure.spring.data.cosmos.repository.query; +import com.azure.spring.data.cosmos.repository.Query; import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.EntityMetadata; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.lang.reflect.Method; +import java.util.Optional; /** * Inherit from QueryMethod class to execute a finder query. @@ -19,6 +24,7 @@ public class ReactiveCosmosQueryMethod extends QueryMethod { private ReactiveCosmosEntityMetadata metadata; private final Method method; + private final String annotatedQueryValue; /** * Creates a new {@link QueryMethod} from the given parameters. Looks up the correct query to use for following @@ -31,6 +37,7 @@ public class ReactiveCosmosQueryMethod extends QueryMethod { public ReactiveCosmosQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { super(method, metadata, factory); this.method = method; + this.annotatedQueryValue = findAnnotatedQuery(method).orElse(null); } @Override @@ -56,4 +63,28 @@ public Class getReactiveWrapper() { private static boolean isReactiveWrapperClass(Class clazz) { return clazz.equals(Flux.class) || clazz.equals(Mono.class); } + + /** + * Returns whether the method has an annotated query. + * @return if the query method has an annotated query + */ + public boolean hasAnnotatedQuery() { + return annotatedQueryValue != null; + } + + /** + * Gets the annotated query or returns null + * @return the annotated query String or null + */ + @Nullable + public String getQueryAnnotation() { + return annotatedQueryValue; + } + + private Optional findAnnotatedQuery(Method method) { + return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Query.class)) + .map(Query::value) + .filter(StringUtils::hasText); + } + } diff --git a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactory.java b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactory.java index ac495642ec7d9..ec70b15aad30b 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactory.java +++ b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/support/CosmosRepositoryFactory.java @@ -74,7 +74,12 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, Assert.notNull(queryMethod, "queryMethod must not be null!"); Assert.notNull(dbOperations, "dbOperations must not be null!"); - return new PartTreeCosmosQuery(queryMethod, dbOperations); + + if (queryMethod.hasAnnotatedQuery()) { + return new StringBasedCosmosQuery(queryMethod, dbOperations); + } else { + return new PartTreeCosmosQuery(queryMethod, dbOperations); + } } } diff --git a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/support/ReactiveCosmosRepositoryFactory.java b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/support/ReactiveCosmosRepositoryFactory.java index a949964eb4630..ad04d04b21fb1 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/support/ReactiveCosmosRepositoryFactory.java +++ b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/support/ReactiveCosmosRepositoryFactory.java @@ -77,8 +77,11 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, Assert.notNull(queryMethod, "queryMethod must not be null!"); Assert.notNull(cosmosOperations, "dbOperations must not be null!"); - return new PartTreeReactiveCosmosQuery(queryMethod, cosmosOperations); - + if (queryMethod.hasAnnotatedQuery()) { + return new StringBasedReactiveCosmosQuery(queryMethod, cosmosOperations); + } else { + return new PartTreeReactiveCosmosQuery(queryMethod, cosmosOperations); + } } } diff --git a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/support/StringBasedCosmosQuery.java b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/support/StringBasedCosmosQuery.java new file mode 100644 index 0000000000000..e9825d5dd7798 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/support/StringBasedCosmosQuery.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.support; + +import com.azure.cosmos.models.SqlParameter; +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.spring.data.cosmos.core.CosmosOperations; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.repository.query.AbstractCosmosQuery; +import com.azure.spring.data.cosmos.repository.query.CosmosParameterAccessor; +import com.azure.spring.data.cosmos.repository.query.CosmosParameterParameterAccessor; +import com.azure.spring.data.cosmos.repository.query.CosmosQueryMethod; +import org.springframework.data.repository.query.ResultProcessor; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter.toCosmosDbValue; + +/** + * Cosmos query class to handle the annotated queries. This overrides the execution and runs the query directly + */ +public class StringBasedCosmosQuery extends AbstractCosmosQuery { + private final String query; + + /** + * Constructor + * @param queryMethod the CosmosQueryMethod + * @param dbOperations the CosmosOperations + */ + public StringBasedCosmosQuery(CosmosQueryMethod queryMethod, CosmosOperations dbOperations) { + super(queryMethod, dbOperations); + this.query = queryMethod.getQueryAnnotation(); + } + + @Override + protected CosmosQuery createQuery(CosmosParameterAccessor accessor) { + return null; + } + + @Override + public Object execute(final Object[] parameters) { + final CosmosParameterAccessor accessor = new CosmosParameterParameterAccessor(getQueryMethod(), parameters); + final ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + + List sqlParameters = getQueryMethod().getParameters().stream() + .map(p -> new SqlParameter("@" + p.getName().orElse(""), + toCosmosDbValue(parameters[p.getIndex()]))) + .collect(Collectors.toList()); + + SqlQuerySpec querySpec = new SqlQuerySpec(query, sqlParameters); + return this.operations.runQuery(querySpec, processor.getReturnedType().getDomainType(), + processor.getReturnedType().getReturnedType()); + } + + @Override + protected boolean isDeleteQuery() { + return false; + } + + @Override + protected boolean isExistsQuery() { + return false; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/support/StringBasedReactiveCosmosQuery.java b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/support/StringBasedReactiveCosmosQuery.java new file mode 100644 index 0000000000000..f2ec40b97dba5 --- /dev/null +++ b/sdk/cosmos/azure-spring-data-cosmos-core/src/main/java/com/azure/spring/data/cosmos/repository/support/StringBasedReactiveCosmosQuery.java @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.data.cosmos.repository.support; + +import com.azure.cosmos.models.SqlParameter; +import com.azure.cosmos.models.SqlQuerySpec; +import com.azure.spring.data.cosmos.core.ReactiveCosmosOperations; +import com.azure.spring.data.cosmos.core.query.CosmosQuery; +import com.azure.spring.data.cosmos.repository.query.AbstractReactiveCosmosQuery; +import com.azure.spring.data.cosmos.repository.query.ReactiveCosmosParameterAccessor; +import com.azure.spring.data.cosmos.repository.query.ReactiveCosmosParameterParameterAccessor; +import com.azure.spring.data.cosmos.repository.query.ReactiveCosmosQueryMethod; +import org.springframework.data.repository.query.ResultProcessor; +import reactor.core.publisher.Flux; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.azure.spring.data.cosmos.core.convert.MappingCosmosConverter.toCosmosDbValue; + +/** + * Cosmos query class to handle the annotated queries. This overrides the execution and runs the query directly + */ +public class StringBasedReactiveCosmosQuery extends AbstractReactiveCosmosQuery { + private final String query; + + /** + * Constructor + * @param queryMethod the query method + * @param dbOperations the reactive cosmos operations + */ + public StringBasedReactiveCosmosQuery(ReactiveCosmosQueryMethod queryMethod, + ReactiveCosmosOperations dbOperations) { + super(queryMethod, dbOperations); + this.query = queryMethod.getQueryAnnotation(); + } + + @Override + protected CosmosQuery createQuery(ReactiveCosmosParameterAccessor accessor) { + return null; + } + + @Override + public Object execute(final Object[] parameters) { + final ReactiveCosmosParameterAccessor accessor = new ReactiveCosmosParameterParameterAccessor(getQueryMethod(), + parameters); + final ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor); + + List sqlParameters = getQueryMethod().getParameters().stream() + .map(p -> new SqlParameter("@" + p.getName().orElse(""), + toCosmosDbValue(parameters[p.getIndex()]))) + .collect(Collectors.toList()); + + SqlQuerySpec querySpec = new SqlQuerySpec(query, sqlParameters); + Flux flux = this.operations.runQuery(querySpec, processor.getReturnedType().getDomainType(), + processor.getReturnedType().getReturnedType()); + return flux; + } + + @Override + protected boolean isDeleteQuery() { + return false; + } + + @Override + protected boolean isExistsQuery() { + return false; + } +} diff --git a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Contact.java b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Contact.java index 1fd2466910110..5b8bc513dfd36 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Contact.java +++ b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/domain/Contact.java @@ -14,6 +14,10 @@ public class Contact { private String title; + private int intValue; + + private boolean isActive; + public Contact() { } @@ -22,6 +26,13 @@ public Contact(String logicId, String title) { this.title = title; } + public Contact(final String logicId, final String title, final int intValue, boolean isActive) { + this.logicId = logicId; + this.title = title; + this.intValue = intValue; + this.isActive = isActive; + } + public String getLogicId() { return logicId; } @@ -38,6 +49,42 @@ public void setTitle(String title) { this.title = title; } + /** + * Getter for property 'status'. + * + * @return Value for property 'status'. + */ + public boolean isActive() { + return isActive; + } + + /** + * Setter for property 'status'. + * + * @param active Value to set for property 'status'. + */ + public void setActive(final boolean active) { + this.isActive = active; + } + + /** + * Getter for property 'value'. + * + * @return Value for property 'value'. + */ + public int getIntValue() { + return intValue; + } + + /** + * Setter for property 'value'. + * + * @param intValue Value to set for property 'value'. + */ + public void setIntValue(final int intValue) { + this.intValue = intValue; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -59,12 +106,18 @@ public int hashCode() { @Override public String toString() { return "Contact{" - + "logicId='" - + logicId - + '\'' - + ", title='" - + title - + '\'' - + '}'; + + "logicId='" + + logicId + + '\'' + + ", title='" + + title + + '\'' + + ", value='" + + intValue + + '\'' + + ", status='" + + isActive + + '\'' + + '}'; } } diff --git a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ContactRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ContactRepositoryIT.java index e7fa52777d084..450010c2417d3 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ContactRepositoryIT.java +++ b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ContactRepositoryIT.java @@ -9,6 +9,7 @@ import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; import com.azure.spring.data.cosmos.repository.repository.ContactRepository; import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.assertj.core.util.Lists; import org.junit.After; import org.junit.AfterClass; @@ -22,6 +23,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -33,7 +35,11 @@ @ContextConfiguration(classes = TestRepositoryConfig.class) public class ContactRepositoryIT { - private static final Contact TEST_CONTACT = new Contact("testId", "faketitle"); + private static final Contact TEST_CONTACT1 = new Contact("testId", "faketitle", 25, true); + private static final Contact TEST_CONTACT2 = new Contact("testId2", "faketitle2", 32, false); + private static final Contact TEST_CONTACT3 = new Contact("testId3", "faketitle3", 25, false); + private static final Contact TEST_CONTACT4 = new Contact("testId4", "faketitle4", 43, true); + private static final Contact TEST_CONTACT5 = new Contact("testId5", "faketitle3", 43, true); private static final CosmosEntityInformation entityInformation = new CosmosEntityInformation<>(Contact.class); @@ -47,13 +53,22 @@ public class ContactRepositoryIT { @Autowired private CosmosTemplate template; + @AfterClass + public static void afterClassCleanup() { + staticTemplate.deleteContainer(entityInformation.getContainerName()); + } + @Before public void setUp() { if (!isSetupDone) { staticTemplate = template; template.createContainerIfNotExists(entityInformation); } - repository.save(TEST_CONTACT); + repository.save(TEST_CONTACT1); + repository.save(TEST_CONTACT2); + repository.save(TEST_CONTACT3); + repository.save(TEST_CONTACT4); + repository.save(TEST_CONTACT5); isSetupDone = true; } @@ -62,23 +77,18 @@ public void cleanup() { repository.deleteAll(); } - @AfterClass - public static void afterClassCleanup() { - staticTemplate.deleteContainer(entityInformation.getContainerName()); - } - @Test public void testFindAll() { final List result = TestUtils.toList(repository.findAll()); - assertThat(result.size()).isEqualTo(1); - assertThat(result.get(0).getLogicId()).isEqualTo(TEST_CONTACT.getLogicId()); - assertThat(result.get(0).getTitle()).isEqualTo(TEST_CONTACT.getTitle()); + assertThat(result.size()).isEqualTo(5); + Assert.assertEquals(Arrays.asList(TEST_CONTACT1, TEST_CONTACT2, TEST_CONTACT3, TEST_CONTACT4, + TEST_CONTACT5), result); - final Contact contact = repository.findById(TEST_CONTACT.getLogicId()).get(); + final Contact contact = repository.findById(TEST_CONTACT1.getLogicId()).get(); - assertThat(contact.getLogicId()).isEqualTo(TEST_CONTACT.getLogicId()); - assertThat(contact.getTitle()).isEqualTo(TEST_CONTACT.getTitle()); + assertThat(contact.getLogicId()).isEqualTo(TEST_CONTACT1.getLogicId()); + assertThat(contact.getTitle()).isEqualTo(TEST_CONTACT1.getTitle()); } @Test @@ -86,21 +96,21 @@ public void testCountAndDeleteByID() { final Contact contact2 = new Contact("newid", "newtitle"); repository.save(contact2); final List all = TestUtils.toList(repository.findAll()); - assertThat(all.size()).isEqualTo(2); + assertThat(all.size()).isEqualTo(6); long count = repository.count(); - assertThat(count).isEqualTo(2); + assertThat(count).isEqualTo(6); repository.deleteById(contact2.getLogicId()); final List result = TestUtils.toList(repository.findAll()); - assertThat(result.size()).isEqualTo(1); - assertThat(result.get(0).getLogicId()).isEqualTo(TEST_CONTACT.getLogicId()); - assertThat(result.get(0).getTitle()).isEqualTo(TEST_CONTACT.getTitle()); + assertThat(result.size()).isEqualTo(5); + assertThat(result.get(0).getLogicId()).isEqualTo(TEST_CONTACT1.getLogicId()); + assertThat(result.get(0).getTitle()).isEqualTo(TEST_CONTACT1.getTitle()); count = repository.count(); - assertThat(count).isEqualTo(1); + assertThat(count).isEqualTo(5); } @Test @@ -108,20 +118,22 @@ public void testCountAndDeleteEntity() { final Contact contact2 = new Contact("newid", "newtitle"); repository.save(contact2); final List all = TestUtils.toList(repository.findAll()); - assertThat(all.size()).isEqualTo(2); + assertThat(all.size()).isEqualTo(6); repository.delete(contact2); final List result = TestUtils.toList(repository.findAll()); - assertThat(result.size()).isEqualTo(1); - assertThat(result.get(0).getLogicId()).isEqualTo(TEST_CONTACT.getLogicId()); - assertThat(result.get(0).getTitle()).isEqualTo(TEST_CONTACT.getTitle()); + assertThat(result.size()).isEqualTo(5); + Assert.assertEquals(Arrays.asList(TEST_CONTACT1, TEST_CONTACT2, TEST_CONTACT3, TEST_CONTACT4, + TEST_CONTACT5), result); + assertThat(result.get(0).getLogicId()).isEqualTo(TEST_CONTACT1.getLogicId()); + assertThat(result.get(0).getTitle()).isEqualTo(TEST_CONTACT1.getTitle()); } @Test public void testUpdateEntity() { - final Contact updatedContact = new Contact(TEST_CONTACT.getLogicId(), "updated"); + final Contact updatedContact = new Contact(TEST_CONTACT1.getLogicId(), "updated"); final Contact savedContact = repository.save(updatedContact); @@ -129,7 +141,7 @@ public void testUpdateEntity() { assertThat(savedContact.getLogicId()).isEqualTo(updatedContact.getLogicId()); assertThat(savedContact.getTitle()).isEqualTo(updatedContact.getTitle()); - final Contact contact = repository.findById(TEST_CONTACT.getLogicId()).get(); + final Contact contact = repository.findById(TEST_CONTACT1.getLogicId()).get(); assertThat(contact.getLogicId()).isEqualTo(updatedContact.getLogicId()); assertThat(contact.getTitle()).isEqualTo(updatedContact.getTitle()); @@ -168,11 +180,11 @@ public void testBatchOperations() { @Test public void testCustomQuery() { - final List result = TestUtils.toList(repository.findByTitle(TEST_CONTACT.getTitle())); + final List result = TestUtils.toList(repository.findByTitle(TEST_CONTACT1.getTitle())); assertThat(result.size()).isEqualTo(1); - assertThat(result.get(0).getLogicId()).isEqualTo(TEST_CONTACT.getLogicId()); - assertThat(result.get(0).getTitle()).isEqualTo(TEST_CONTACT.getTitle()); + assertThat(result.get(0).getLogicId()).isEqualTo(TEST_CONTACT1.getLogicId()); + assertThat(result.get(0).getTitle()).isEqualTo(TEST_CONTACT1.getTitle()); } @@ -188,10 +200,10 @@ public void testNullIdContact() { @Test public void testFindById() { - final Optional optional = repository.findById(TEST_CONTACT.getLogicId()); + final Optional optional = repository.findById(TEST_CONTACT1.getLogicId()); Assert.assertTrue(optional.isPresent()); - Assert.assertEquals(TEST_CONTACT, optional.get()); + Assert.assertEquals(TEST_CONTACT1, optional.get()); Assert.assertFalse(repository.findById("").isPresent()); } @@ -204,36 +216,51 @@ public void testFindByIdNotFound() { @Test public void testShouldFindSingleEntity() { - final Contact contact = repository.findOneByTitle(TEST_CONTACT.getTitle()); + final Contact contact = repository.findOneByTitle(TEST_CONTACT1.getTitle()); - Assert.assertEquals(TEST_CONTACT, contact); + Assert.assertEquals(TEST_CONTACT1, contact); } @Test public void testShouldFindSingleOptionalEntity() { - final Optional contact = repository.findOptionallyByTitle(TEST_CONTACT.getTitle()); + final Optional contact = repository.findOptionallyByTitle(TEST_CONTACT1.getTitle()); Assert.assertTrue(contact.isPresent()); - Assert.assertEquals(TEST_CONTACT, contact.get()); + Assert.assertEquals(TEST_CONTACT1, contact.get()); Assert.assertFalse(repository.findOptionallyByTitle("not here").isPresent()); } @Test(expected = CosmosAccessException.class) public void testShouldFailIfMultipleResultsReturned() { - repository.save(new Contact("testId2", TEST_CONTACT.getTitle())); + repository.save(new Contact("testId2", TEST_CONTACT1.getTitle())); - repository.findOneByTitle(TEST_CONTACT.getTitle()); + repository.findOneByTitle(TEST_CONTACT1.getTitle()); } @Test public void testShouldAllowListAndIterableResponses() { - final List contactList = TestUtils.toList(repository.findByTitle(TEST_CONTACT.getTitle())); - Assert.assertEquals(TEST_CONTACT, contactList.get(0)); + final List contactList = TestUtils.toList(repository.findByTitle(TEST_CONTACT1.getTitle())); + Assert.assertEquals(TEST_CONTACT1, contactList.get(0)); Assert.assertEquals(1, contactList.size()); - final Iterator contactIterator = repository.findByLogicId(TEST_CONTACT.getLogicId()).iterator(); + final Iterator contactIterator = repository.findByLogicId(TEST_CONTACT1.getLogicId()).iterator(); Assert.assertTrue(contactIterator.hasNext()); - Assert.assertEquals(TEST_CONTACT, contactIterator.next()); + Assert.assertEquals(TEST_CONTACT1, contactIterator.next()); Assert.assertFalse(contactIterator.hasNext()); } + + @Test + public void testAnnotatedQueries() { + List valueContacts = repository.getContactsByTitleAndValue(43, TEST_CONTACT5.getTitle()); + Assert.assertEquals(1, valueContacts.size()); + Assert.assertEquals(TEST_CONTACT5, valueContacts.get(0)); + + List contactsWithOffset = repository.getContactsWithOffsetLimit(1, 2); + Assert.assertEquals(2, contactsWithOffset.size()); + Assert.assertEquals(TEST_CONTACT2, contactsWithOffset.get(0)); + Assert.assertEquals(TEST_CONTACT3, contactsWithOffset.get(1)); + + List groupByContacts = repository.selectGroupBy(); + Assert.assertEquals(3, groupByContacts.size()); + } } diff --git a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveCourseRepositoryIT.java b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveCourseRepositoryIT.java index c4f0694998afe..d7049921ca8ae 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveCourseRepositoryIT.java +++ b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/integration/ReactiveCourseRepositoryIT.java @@ -9,6 +9,7 @@ import com.azure.spring.data.cosmos.repository.TestRepositoryConfig; import com.azure.spring.data.cosmos.repository.repository.ReactiveCourseRepository; import com.azure.spring.data.cosmos.repository.support.CosmosEntityInformation; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -299,4 +300,14 @@ public void testFindByNameOrDepartmentAllIgnoreCase() { COURSE_NAME_1.toLowerCase(), DEPARTMENT_NAME_3.toLowerCase()); StepVerifier.create(findResult).expectNext(COURSE_1).verifyComplete(); } + + @Test + public void testAnnotatedQueries() { + Flux courseFlux = repository.getCoursesWithNameDepartment(COURSE_NAME_1, DEPARTMENT_NAME_3); + StepVerifier.create(courseFlux).expectNext(COURSE_1).verifyComplete(); + + Flux courseGroupBy = repository.getCoursesGroupByDepartment(); + StepVerifier.create(courseGroupBy).expectComplete(); + StepVerifier.create(courseGroupBy).expectNextCount(1); + } } diff --git a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ContactRepository.java b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ContactRepository.java index 449c0d5952028..58fed4c43623f 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ContactRepository.java +++ b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ContactRepository.java @@ -4,8 +4,12 @@ import com.azure.spring.data.cosmos.domain.Contact; import com.azure.spring.data.cosmos.repository.CosmosRepository; +import com.azure.spring.data.cosmos.repository.Query; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository @@ -17,4 +21,17 @@ public interface ContactRepository extends CosmosRepository { Contact findOneByTitle(String title); Optional findOptionallyByTitle(String title); + + @Query(value = "select * from c where c.title = @title and c.intValue = @value") + List getContactsByTitleAndValue(@Param("value") int value, @Param("title") String name); + + @Query(value = "select * from c offset @offset limit @limit") + List getContactsWithOffsetLimit(@Param("offset") int offset, @Param("limit") int limit); + + @Query(value = "select * from c where c.status= true") + List findActiveContacts(); + + @Query(value = "SELECT count(c.id) as id_count, c.intValue FROM c group by c.intValue") + List selectGroupBy(); + } diff --git a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveCourseRepository.java b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveCourseRepository.java index d43708223fdc5..d3d1545d4de83 100644 --- a/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveCourseRepository.java +++ b/sdk/cosmos/azure-spring-data-cosmos-test/src/test/java/com/azure/spring/data/cosmos/repository/repository/ReactiveCourseRepository.java @@ -3,7 +3,10 @@ package com.azure.spring.data.cosmos.repository.repository; import com.azure.spring.data.cosmos.domain.Course; +import com.azure.spring.data.cosmos.repository.Query; import com.azure.spring.data.cosmos.repository.ReactiveCosmosRepository; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.data.repository.query.Param; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -43,4 +46,10 @@ public interface ReactiveCourseRepository extends ReactiveCosmosRepository findOneByName(String name); + @Query(value = "select * from c where c.name = @name and c.department = @department") + Flux getCoursesWithNameDepartment(@Param("name") String name, @Param("department") String department); + + @Query(value = "select count(c.id) as num_ids, c.department from c group by c.department") + Flux getCoursesGroupByDepartment(); + }