Browse Source

DATAMONGO-1929 - Add fluent mapReduce template API.

Original pull request: #551.
pull/535/merge
Christoph Strobl 8 years ago committed by Mark Paluch
parent
commit
e424573f0d
  1. 199
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperation.java
  2. 184
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java
  3. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FluentMongoOperations.java
  4. 62
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
  5. 3
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFluentMongoOperations.java
  6. 199
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperation.java
  7. 186
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java
  8. 12
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
  9. 141
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupportUnitTests.java
  10. 141
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java

199
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperation.java

@ -0,0 +1,199 @@ @@ -0,0 +1,199 @@
/*
* Copyright 2018 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.mongodb.core;
import java.util.List;
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.Query;
/**
* {@link ExecutableMapReduceOperation} allows creation and execution of MongoDB mapReduce operations in a fluent API
* style. The starting {@literal domainType} is used for mapping an optional {@link Query} provided via {@code matching}
* into the MongoDB specific representation. By default, the originating {@literal domainType} is also used for mapping
* back the results from the {@link org.bson.Document}. However, it is possible to define an different
* {@literal returnType} via {@code as} to mapping the result.<br />
* The collection to operate on is by default derived from the initial {@literal domainType} and can be defined there
* via {@link org.springframework.data.mongodb.core.mapping.Document}. Using {@code inCollection} allows to override the
* collection name for the execution.
*
* <pre>
* <code>
* mapReduce(Human.class)
* .map("function() { emit(this.id, this.firstname) }")
* .reduce("function(id, name) { return sum(id, name); }")
* .inCollection("star-wars")
* .as(Jedi.class)
* .matching(query(where("lastname").is("skywalker")))
* .all();
* </code>
* </pre>
*
* @author Christoph Strobl
* @since 2.1
*/
public interface ExecutableMapReduceOperation {
/**
* Start creating a mapReduce operation for the given {@literal domainType}.
*
* @param domainType must not be {@literal null}.
* @return new instance of {@link ExecutableFind}.
* @throws IllegalArgumentException if domainType is {@literal null}.
*/
<T> MapReduceWithMapFunction<T> mapReduce(Class<T> domainType);
/**
* Trigger mapReduce execution by calling one of the terminating methods.
*
* @author Christoph Strobl
* @since 2.1
*/
interface TerminatingMapReduce<T> {
/**
* Get the mapReduce results.
*
* @return never {@literal null}.
*/
List<T> all();
}
/**
* Provide the Javascript {@code function()} used to map matching documents.
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithMapFunction<T> {
/**
* Set the Javascript map {@code function()}.
*
* @param mapFunction must not be {@literal null} nor empty.
* @return new instance of {@link MapReduceWithReduceFunction}.
* @throws IllegalArgumentException if {@literal mapFunction} is {@literal null} or empty.
*/
MapReduceWithReduceFunction<T> map(String mapFunction);
}
/**
* Provide the Javascript {@code function()} used to reduce matching documents.
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithReduceFunction<T> {
/**
* Set the Javascript map {@code function()}.
*
* @param reduceFunction must not be {@literal null} nor empty.
* @return new instance of {@link ExecutableMapReduce}.
* @throws IllegalArgumentException if {@literal reduceFunction} is {@literal null} or empty.
*/
ExecutableMapReduce<T> reduce(String reduceFunction);
}
/**
* Collection override (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithCollection<T> extends MapReduceWithQuery<T> {
/**
* Explicitly set the name of the collection to perform the mapReduce operation on. <br />
* Skip this step to use the default collection derived from the domain type.
*
* @param collection must not be {@literal null} nor {@literal empty}.
* @return new instance of {@link MapReduceWithProjection}.
* @throws IllegalArgumentException if collection is {@literal null}.
*/
MapReduceWithProjection<T> inCollection(String collection);
}
/**
* Input document filter query (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithQuery<T> extends TerminatingMapReduce<T> {
/**
* Set the filter query to be used.
*
* @param query must not be {@literal null}.
* @return new instance of {@link TerminatingMapReduce}.
* @throws IllegalArgumentException if query is {@literal null}.
*/
TerminatingMapReduce<T> matching(Query query);
}
/**
* Result type override (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithProjection<T> extends MapReduceWithQuery<T> {
/**
* Define the target type fields should be mapped to. <br />
* Skip this step if you are anyway only interested in the original domain type.
*
* @param resultType must not be {@literal null}.
* @param <R> result type.
* @return new instance of {@link TerminatingMapReduce}.
* @throws IllegalArgumentException if resultType is {@literal null}.
*/
<R> MapReduceWithQuery<R> as(Class<R> resultType);
}
/**
* Additional mapReduce options (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithOptions<T> {
/**
* Set additional options to apply to the mapReduce operation.
*
* @param options must not be {@literal null}.
* @return new instance of {@link ExecutableMapReduce}.
* @throws IllegalArgumentException if options is {@literal null}.
*/
ExecutableMapReduce<T> with(MapReduceOptions options);
}
/**
* {@link ExecutableMapReduce} provides methods for constructing mapReduce operations in a fluent way.
*
* @author Christoph Strobl
* @since 2.1
*/
interface ExecutableMapReduce<T> extends MapReduceWithMapFunction<T>, MapReduceWithReduceFunction<T>,
MapReduceWithCollection<T>, MapReduceWithProjection<T>, MapReduceWithOptions<T> {
}
}

184
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java

@ -0,0 +1,184 @@ @@ -0,0 +1,184 @@
/*
* Copyright 2018 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.mongodb.core;
import java.util.List;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Implementation of {@link ExecutableMapReduceOperation}.
*
* @author Christoph Strobl
* @since 2.1
*/
class ExecutableMapReduceOperationSupport implements ExecutableMapReduceOperation {
private static final Query ALL_QUERY = new Query();
private final MongoTemplate template;
/**
* Create new {@link ExecutableMapReduceOperationSupport}.
*
* @param template must not be {@literal null}.
* @throws IllegalArgumentException if template is {@literal null}.
*/
ExecutableMapReduceOperationSupport(MongoTemplate template) {
Assert.notNull(template, "Template must not be null!");
this.template = template;
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation#mapReduce(java.lang.Class)
*/
@Override
public <T> ExecutableMapReduceSupport<T> mapReduce(Class<T> domainType) {
Assert.notNull(domainType, "DomainType must not be null!");
return new ExecutableMapReduceSupport(template, domainType, domainType, null, ALL_QUERY, null, null, null);
}
/**
* @author Christoph Strobl
* @since 2.1
*/
static class ExecutableMapReduceSupport<T>
implements ExecutableMapReduce<T>, MapReduceWithOptions<T>, MapReduceWithCollection<T>,
MapReduceWithProjection<T>, MapReduceWithQuery<T>, MapReduceWithReduceFunction<T>, MapReduceWithMapFunction<T> {
private final MongoTemplate template;
private final Class<?> domainType;
private final Class<T> returnType;
private final @Nullable String collection;
private final Query query;
private final @Nullable String mapFunction;
private final @Nullable String reduceFunction;
private final @Nullable MapReduceOptions options;
ExecutableMapReduceSupport(MongoTemplate template, Class<?> domainType, Class<T> returnType, String collection,
Query query, @Nullable String mapFunction, @Nullable String reduceFunction, MapReduceOptions options) {
this.template = template;
this.domainType = domainType;
this.returnType = returnType;
this.collection = collection;
this.query = query;
this.mapFunction = mapFunction;
this.reduceFunction = reduceFunction;
this.options = options;
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.TerminatingMapReduce#all()
*/
@Override
public List<T> all() {
return template.mapReduce(query, domainType, getCollectionName(), mapFunction, reduceFunction, options,
returnType);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithCollection#inCollection(java.lang.String)
*/
@Override
public MapReduceWithProjection<T> inCollection(String collection) {
Assert.hasText(collection, "Collection name must not be null nor empty!");
return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithQuery#query(org.springframework.data.mongodb.core.query.Query)
*/
@Override
public TerminatingMapReduce<T> matching(Query query) {
Assert.notNull(query, "Query must not be null!");
return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithProjection#as(java.lang.Class)
*/
@Override
public <R> MapReduceWithQuery<R> as(Class<R> resultType) {
Assert.notNull(resultType, "ResultType must not be null!");
return new ExecutableMapReduceSupport<>(template, domainType, resultType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithOptions#with(org.springframework.data.mongodb.core.mapreduce.MapReduceOptions)
*/
@Override
public ExecutableMapReduce<T> with(MapReduceOptions options) {
Assert.notNull(options, "Options must not be null! Please consider empty MapReduceOptions#options() instead.");
return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithMapFunction#map(java.lang.String)
*/
@Override
public MapReduceWithReduceFunction<T> map(String mapFunction) {
Assert.hasText(mapFunction, "MapFunction name must not be null nor empty!");
return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.MapReduceWithReduceFunction#reduce(java.lang.String)
*/
@Override
public ExecutableMapReduce<T> reduce(String reduceFunction) {
Assert.hasText(reduceFunction, "ReduceFunction name must not be null nor empty!");
return new ExecutableMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType);
}
}
}

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FluentMongoOperations.java

@ -22,4 +22,4 @@ package org.springframework.data.mongodb.core; @@ -22,4 +22,4 @@ package org.springframework.data.mongodb.core;
* @since 2.0
*/
public interface FluentMongoOperations extends ExecutableFindOperation, ExecutableInsertOperation,
ExecutableUpdateOperation, ExecutableRemoveOperation, ExecutableAggregationOperation {}
ExecutableUpdateOperation, ExecutableRemoveOperation, ExecutableAggregationOperation, ExecutableMapReduceOperation {}

62
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java

@ -71,7 +71,16 @@ import org.springframework.data.mongodb.core.aggregation.AggregationResults; @@ -71,7 +71,16 @@ import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.aggregation.Fields;
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.*;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.JsonSchemaMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper;
import org.springframework.data.mongodb.core.convert.MongoWriter;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.convert.UpdateMapper;
import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.data.mongodb.core.index.IndexOperationsProvider;
import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher;
@ -132,7 +141,16 @@ import com.mongodb.client.MongoCollection; @@ -132,7 +141,16 @@ import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoIterable;
import com.mongodb.client.model.*;
import com.mongodb.client.model.CountOptions;
import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.client.model.DeleteOptions;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.FindOneAndDeleteOptions;
import com.mongodb.client.model.FindOneAndUpdateOptions;
import com.mongodb.client.model.ReturnDocument;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.model.ValidationAction;
import com.mongodb.client.model.ValidationLevel;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import com.mongodb.session.ClientSession;
@ -1798,9 +1816,29 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -1798,9 +1816,29 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
public <T> MapReduceResults<T> mapReduce(Query query, String inputCollectionName, String mapFunction,
String reduceFunction, @Nullable MapReduceOptions mapReduceOptions, Class<T> entityClass) {
return new MapReduceResults<T>(
mapReduce(query, entityClass, inputCollectionName, mapFunction, reduceFunction, mapReduceOptions, entityClass),
new Document());
}
/**
* @param query
* @param domainType
* @param inputCollectionName
* @param mapFunction
* @param reduceFunction
* @param mapReduceOptions
* @param resultType
* @param <T>
* @return
* @since 2.1
*/
public <T> List<T> mapReduce(Query query, Class<?> domainType, String inputCollectionName, String mapFunction,
String reduceFunction, @Nullable MapReduceOptions mapReduceOptions, Class<T> resultType) {
Assert.notNull(query, "Query must not be null!");
Assert.notNull(inputCollectionName, "InputCollectionName must not be null!");
Assert.notNull(entityClass, "EntityClass must not be null!");
Assert.notNull(resultType, "EntityClass must not be null!");
Assert.notNull(reduceFunction, "ReduceFunction must not be null!");
Assert.notNull(mapFunction, "MapFunction must not be null!");
@ -1818,9 +1856,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -1818,9 +1856,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
if (query.getMeta() != null && query.getMeta().getMaxTimeMsec() != null) {
result = result.maxTime(query.getMeta().getMaxTimeMsec(), TimeUnit.MILLISECONDS);
}
result = result.sort(getMappedSortObject(query, entityClass));
result = result.sort(getMappedSortObject(query, domainType));
result = result.filter(queryMapper.getMappedObject(query.getQueryObject(), Optional.empty()));
result = result
.filter(queryMapper.getMappedObject(query.getQueryObject(), mappingContext.getPersistentEntity(domainType)));
}
Optional<Collation> collation = query.getCollation();
@ -1856,13 +1895,13 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -1856,13 +1895,13 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
result = collation.map(Collation::toMongoCollation).map(result::collation).orElse(result);
List<T> mappedResults = new ArrayList<T>();
DocumentCallback<T> callback = new ReadDocumentCallback<T>(mongoConverter, entityClass, inputCollectionName);
DocumentCallback<T> callback = new ReadDocumentCallback<T>(mongoConverter, resultType, inputCollectionName);
for (Document document : result) {
mappedResults.add(callback.doWith(document));
}
return new MapReduceResults<T>(mappedResults, new Document());
return mappedResults;
}
public <T> GroupByResults<T> group(String inputCollectionName, GroupBy groupBy, Class<T> entityClass) {
@ -2184,6 +2223,15 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -2184,6 +2223,15 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
return new ExecutableAggregationOperationSupport(this).aggregateAndReturn(domainType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ExecutableAggregationOperation#aggregateAndReturn(java.lang.Class)
*/
@Override
public <T> ExecutableMapReduce<T> mapReduce(Class<T> domainType) {
return new ExecutableMapReduceOperationSupport(this).mapReduce(domainType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ExecutableInsertOperation#insert(java.lang.Class)

3
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFluentMongoOperations.java

@ -19,7 +19,8 @@ package org.springframework.data.mongodb.core; @@ -19,7 +19,8 @@ package org.springframework.data.mongodb.core;
* Stripped down interface providing access to a fluent API that specifies a basic set of reactive MongoDB operations.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 2.0
*/
public interface ReactiveFluentMongoOperations extends ReactiveFindOperation, ReactiveInsertOperation,
ReactiveUpdateOperation, ReactiveRemoveOperation, ReactiveAggregationOperation {}
ReactiveUpdateOperation, ReactiveRemoveOperation, ReactiveAggregationOperation, ReactiveMapReduceOperation {}

199
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperation.java

@ -0,0 +1,199 @@ @@ -0,0 +1,199 @@
/*
* Copyright 2018 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.mongodb.core;
import reactor.core.publisher.Flux;
import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.Query;
/**
* {@link ReactiveMapReduceOperation} allows creation and execution of MongoDB mapReduce operations in a fluent API
* style. The starting {@literal domainType} is used for mapping an optional {@link Query} provided via {@code matching}
* into the MongoDB specific representation. By default, the originating {@literal domainType} is also used for mapping
* back the results from the {@link org.bson.Document}. However, it is possible to define an different
* {@literal returnType} via {@code as} to mapping the result.<br />
* The collection to operate on is by default derived from the initial {@literal domainType} and can be defined there
* via {@link org.springframework.data.mongodb.core.mapping.Document}. Using {@code inCollection} allows to override the
* collection name for the execution.
*
* <pre>
* <code>
* mapReduce(Human.class)
* .map("function() { emit(this.id, this.firstname) }")
* .reduce("function(id, name) { return sum(id, name); }")
* .inCollection("star-wars")
* .as(Jedi.class)
* .matching(query(where("lastname").is("skywalker")))
* .all();
* </code>
* </pre>
*
* @author Christoph Strobl
* @since 2.1
*/
public interface ReactiveMapReduceOperation {
/**
* Start creating a mapReduce operation for the given {@literal domainType}.
*
* @param domainType must not be {@literal null}.
* @return new instance of {@link ExecutableFind}.
* @throws IllegalArgumentException if domainType is {@literal null}.
*/
<T> MapReduceWithMapFunction<T> mapReduce(Class<T> domainType);
/**
* Trigger mapReduce execution by calling one of the terminating methods.
*
* @author Christoph Strobl
* @since 2.1
*/
interface TerminatingMapReduce<T> {
/**
* Get the {@link Flux} emitting mapReduce results.
*
* @return a {@link Flux} emitting the already mapped operation results.
*/
Flux<T> all();
}
/**
* Provide the Javascript {@code function()} used to map matching documents.
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithMapFunction<T> {
/**
* Set the Javascript map {@code function()}.
*
* @param mapFunction must not be {@literal null} nor empty.
* @return new instance of {@link MapReduceWithReduceFunction}.
* @throws IllegalArgumentException if {@literal mapFunction} is {@literal null} or empty.
*/
MapReduceWithReduceFunction<T> map(String mapFunction);
}
/**
* Provide the Javascript {@code function()} used to reduce matching documents.
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithReduceFunction<T> {
/**
* Set the Javascript map {@code function()}.
*
* @param reduceFunction must not be {@literal null} nor empty.
* @return new instance of {@link ReactiveMapReduce}.
* @throws IllegalArgumentException if {@literal reduceFunction} is {@literal null} or empty.
*/
ReactiveMapReduce<T> reduce(String reduceFunction);
}
/**
* Collection override (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithCollection<T> extends MapReduceWithQuery<T> {
/**
* Explicitly set the name of the collection to perform the mapReduce operation on. <br />
* Skip this step to use the default collection derived from the domain type.
*
* @param collection must not be {@literal null} nor {@literal empty}.
* @return new instance of {@link MapReduceWithProjection}.
* @throws IllegalArgumentException if collection is {@literal null}.
*/
MapReduceWithProjection<T> inCollection(String collection);
}
/**
* Input document filter query (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithQuery<T> extends TerminatingMapReduce<T> {
/**
* Set the filter query to be used.
*
* @param query must not be {@literal null}.
* @return new instance of {@link TerminatingMapReduce}.
* @throws IllegalArgumentException if query is {@literal null}.
*/
TerminatingMapReduce<T> matching(Query query);
}
/**
* Result type override (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithProjection<T> extends MapReduceWithQuery<T> {
/**
* Define the target type fields should be mapped to. <br />
* Skip this step if you are anyway only interested in the original domain type.
*
* @param resultType must not be {@literal null}.
* @param <R> result type.
* @return new instance of {@link TerminatingMapReduce}.
* @throws IllegalArgumentException if resultType is {@literal null}.
*/
<R> MapReduceWithQuery<R> as(Class<R> resultType);
}
/**
* Additional mapReduce options (Optional).
*
* @author Christoph Strobl
* @since 2.1
*/
interface MapReduceWithOptions<T> {
/**
* Set additional options to apply to the mapReduce operation.
*
* @param options must not be {@literal null}.
* @return new instance of {@link ReactiveMapReduce}.
* @throws IllegalArgumentException if options is {@literal null}.
*/
ReactiveMapReduce<T> with(MapReduceOptions options);
}
/**
* {@link ReactiveMapReduce} provides methods for constructing reactive mapReduce operations in a fluent way.
*
* @author Christoph Strobl
* @since 2.1
*/
interface ReactiveMapReduce<T> extends MapReduceWithMapFunction<T>, MapReduceWithReduceFunction<T>,
MapReduceWithCollection<T>, MapReduceWithProjection<T>, MapReduceWithOptions<T> {
}
}

186
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java

@ -0,0 +1,186 @@ @@ -0,0 +1,186 @@
/*
* Copyright 2018 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.mongodb.core;
import reactor.core.publisher.Flux;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Implementation of {@link ReactiveMapReduceOperation}.
*
* @author Christoph Strobl
* @since 2.1
*/
class ReactiveMapReduceOperationSupport implements ReactiveMapReduceOperation {
private static final Query ALL_QUERY = new Query();
private final ReactiveMongoTemplate template;
/**
* Create new {@link ReactiveMapReduceOperationSupport}.
*
* @param template must not be {@literal null}.
* @throws IllegalArgumentException if template is {@literal null}.
*/
ReactiveMapReduceOperationSupport(ReactiveMongoTemplate template) {
Assert.notNull(template, "Template must not be null!");
this.template = template;
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation#mapReduce(java.lang.Class)
*/
@Override
public <T> ReactiveMapReduceSupport<T> mapReduce(Class<T> domainType) {
Assert.notNull(domainType, "DomainType must not be null!");
return new ReactiveMapReduceSupport(template, domainType, domainType, null, ALL_QUERY, null, null, null);
}
/**
* @author Christoph Strobl
* @since 2.1
*/
static class ReactiveMapReduceSupport<T>
implements ReactiveMapReduce<T>, MapReduceWithOptions<T>, MapReduceWithCollection<T>, MapReduceWithProjection<T>,
MapReduceWithQuery<T>, MapReduceWithReduceFunction<T>, MapReduceWithMapFunction<T> {
private final ReactiveMongoTemplate template;
private final Class<?> domainType;
private final Class<T> returnType;
private final @Nullable String collection;
private final Query query;
private final @Nullable String mapFunction;
private final @Nullable String reduceFunction;
private final @Nullable MapReduceOptions options;
ReactiveMapReduceSupport(ReactiveMongoTemplate template, Class<?> domainType, Class<T> returnType,
String collection, Query query, @Nullable String mapFunction, @Nullable String reduceFunction,
MapReduceOptions options) {
this.template = template;
this.domainType = domainType;
this.returnType = returnType;
this.collection = collection;
this.query = query;
this.mapFunction = mapFunction;
this.reduceFunction = reduceFunction;
this.options = options;
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.TerminatingMapReduce#all()
*/
@Override
public Flux<T> all() {
return template.mapReduce(query, domainType, getCollectionName(), returnType, mapFunction, reduceFunction,
options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithCollection#inCollection(java.lang.String)
*/
@Override
public MapReduceWithProjection<T> inCollection(String collection) {
Assert.hasText(collection, "Collection name must not be null nor empty!");
return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithQuery#query(org.springframework.data.mongodb.core.query.Query)
*/
@Override
public TerminatingMapReduce<T> matching(Query query) {
Assert.notNull(query, "Query must not be null!");
return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithProjection#as(java.lang.Class)
*/
@Override
public <R> MapReduceWithQuery<R> as(Class<R> resultType) {
Assert.notNull(resultType, "ResultType must not be null!");
return new ReactiveMapReduceSupport<>(template, domainType, resultType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithOptions#with(org.springframework.data.mongodb.core.mapreduce.MapReduceOptions)
*/
@Override
public ReactiveMapReduce<T> with(MapReduceOptions options) {
Assert.notNull(options, "Options must not be null! Please consider empty MapReduceOptions#options() instead.");
return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithMapFunction#map(java.lang.String)
*/
@Override
public MapReduceWithReduceFunction<T> map(String mapFunction) {
Assert.hasText(mapFunction, "MapFunction name must not be null nor empty!");
return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
/*
* (non-Javascript)
* @see in org.springframework.data.mongodb.core.ReactiveMapReduceOperation.MapReduceWithReduceFunction#reduce(java.lang.String)
*/
@Override
public ReactiveMapReduce<T> reduce(String reduceFunction) {
Assert.hasText(reduceFunction, "ReduceFunction name must not be null nor empty!");
return new ReactiveMapReduceSupport<>(template, domainType, returnType, collection, query, mapFunction,
reduceFunction, options);
}
private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType);
}
}
}

12
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java

@ -2001,7 +2001,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -2001,7 +2001,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
if (filterQuery.getLimit() > 0 || (options.getLimit() != null)) {
if (filterQuery.getLimit() > 0 && (options.getLimit() != null)) {
throw new IllegalArgumentException("Both Query and MapReduceOptions define a limit. Please provide the limit only via one of the two.");
throw new IllegalArgumentException(
"Both Query and MapReduceOptions define a limit. Please provide the limit only via one of the two.");
}
if (filterQuery.getLimit() > 0) {
@ -2105,6 +2106,15 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -2105,6 +2106,15 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return new ReactiveAggregationOperationSupport(this).aggregateAndReturn(domainType);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMapReduceOperation#mapReduce(java.lang.Class)
*/
@Override
public <T> ReactiveMapReduce<T> mapReduce(Class<T> domainType) {
return new ReactiveMapReduceOperationSupport(this).mapReduce(domainType);
}
/**
* Retrieve and remove all documents matching the given {@code query} by calling {@link #find(Query, Class, String)}
* and {@link #remove(Query, Class, String)}, whereas the {@link Query} for {@link #remove(Query, Class, String)} is

141
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupportUnitTests.java

@ -0,0 +1,141 @@ @@ -0,0 +1,141 @@
/*
* Copyright 2018 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.mongodb.core;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
/**
* Unit tests for {@link ExecutableMapReduceOperationSupport}.
*
* @author Christoph Strobl
* @currentRead Beyond the Shadows - Brent Weeks
*/
@RunWith(MockitoJUnitRunner.class)
public class ExecutableMapReduceOperationSupportUnitTests {
private static final String STAR_WARS = "star-wars";
private static final String MAP_FUNCTION = "function() { emit(this.id, this.firstname) }";
private static final String REDUCE_FUNCTION = "function(id, name) { return sum(id, name); }";
@Mock MongoTemplate template;
ExecutableMapReduceOperationSupport mapReduceOpsSupport;
@Before
public void setUp() {
when(template.determineCollectionName(eq(Person.class))).thenReturn(STAR_WARS);
mapReduceOpsSupport = new ExecutableMapReduceOperationSupport(template);
}
@Test(expected = IllegalArgumentException.class) // DATAMONGO-1929
public void throwsExceptionOnNullTemplate() {
new ExecutableMapReduceOperationSupport(null);
}
@Test(expected = IllegalArgumentException.class) // DATAMONGO-1929
public void throwsExceptionOnNullDomainType() {
mapReduceOpsSupport.mapReduce(null);
}
@Test // DATAMONGO-1929
public void usesExtractedCollectionName() {
mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).all();
verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(MAP_FUNCTION), eq(REDUCE_FUNCTION),
isNull(), eq(Person.class));
}
@Test // DATAMONGO-1929
public void usesExplicitCollectionName() {
mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION)
.inCollection("the-night-angel").all();
verify(template).mapReduce(any(Query.class), eq(Person.class), eq("the-night-angel"), eq(MAP_FUNCTION),
eq(REDUCE_FUNCTION), isNull(), eq(Person.class));
}
@Test // DATAMONGO-1929
public void usesMapReduceOptionsWhenPresent() {
MapReduceOptions options = MapReduceOptions.options();
mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).with(options).all();
verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(MAP_FUNCTION), eq(REDUCE_FUNCTION),
eq(options), eq(Person.class));
}
@Test // DATAMONGO-1929
public void usesQueryWhenPresent() {
Query query = new BasicQuery("{ 'lastname' : 'skywalker' }");
mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).matching(query).all();
verify(template).mapReduce(eq(query), eq(Person.class), eq(STAR_WARS), eq(MAP_FUNCTION), eq(REDUCE_FUNCTION),
isNull(), eq(Person.class));
}
@Test // DATAMONGO-1929
public void usesProjectionWhenPresent() {
mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).as(Jedi.class).all();
verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(MAP_FUNCTION), eq(REDUCE_FUNCTION),
isNull(), eq(Jedi.class));
}
interface Contact {}
@Data
@org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS)
static class Person implements Contact {
@Id String id;
String firstname;
String lastname;
Object ability;
Person father;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Jedi {
@Field("firstname") String name;
}
}

141
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java

@ -0,0 +1,141 @@ @@ -0,0 +1,141 @@
/*
* Copyright 2018 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.mongodb.core;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Query;
/**
* Unit tests for {@link ReactiveMapReduceOperationSupport}.
*
* @author Christoph Strobl
* @currentRead Beyond the Shadows - Brent Weeks
*/
@RunWith(MockitoJUnitRunner.class)
public class ReactiveMapReduceOperationSupportUnitTests {
private static final String STAR_WARS = "star-wars";
private static final String MAP_FUNCTION = "function() { emit(this.id, this.firstname) }";
private static final String REDUCE_FUNCTION = "function(id, name) { return sum(id, name); }";
@Mock ReactiveMongoTemplate template;
ReactiveMapReduceOperationSupport mapReduceOpsSupport;
@Before
public void setUp() {
when(template.determineCollectionName(eq(Person.class))).thenReturn(STAR_WARS);
mapReduceOpsSupport = new ReactiveMapReduceOperationSupport(template);
}
@Test(expected = IllegalArgumentException.class) // DATAMONGO-1929
public void throwsExceptionOnNullTemplate() {
new ExecutableMapReduceOperationSupport(null);
}
@Test(expected = IllegalArgumentException.class) // DATAMONGO-1929
public void throwsExceptionOnNullDomainType() {
mapReduceOpsSupport.mapReduce(null);
}
@Test // DATAMONGO-1929
public void usesExtractedCollectionName() {
mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).all();
verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(Person.class), eq(MAP_FUNCTION),
eq(REDUCE_FUNCTION), isNull());
}
@Test // DATAMONGO-1929
public void usesExplicitCollectionName() {
mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION)
.inCollection("the-night-angel").all();
verify(template).mapReduce(any(Query.class), eq(Person.class), eq("the-night-angel"), eq(Person.class),
eq(MAP_FUNCTION), eq(REDUCE_FUNCTION), isNull());
}
@Test // DATAMONGO-1929
public void usesMapReduceOptionsWhenPresent() {
MapReduceOptions options = MapReduceOptions.options();
mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).with(options).all();
verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(Person.class), eq(MAP_FUNCTION),
eq(REDUCE_FUNCTION), eq(options));
}
@Test // DATAMONGO-1929
public void usesQueryWhenPresent() {
Query query = new BasicQuery("{ 'lastname' : 'skywalker' }");
mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).matching(query).all();
verify(template).mapReduce(eq(query), eq(Person.class), eq(STAR_WARS), eq(Person.class), eq(MAP_FUNCTION),
eq(REDUCE_FUNCTION), isNull());
}
@Test // DATAMONGO-1929
public void usesProjectionWhenPresent() {
mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).as(Jedi.class).all();
verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(Jedi.class), eq(MAP_FUNCTION),
eq(REDUCE_FUNCTION), isNull());
}
interface Contact {}
@Data
@org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS)
static class Person implements Contact {
@Id String id;
String firstname;
String lastname;
Object ability;
Person father;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class Jedi {
@Field("firstname") String name;
}
}
Loading…
Cancel
Save