diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java
new file mode 100644
index 000000000..2fb319c34
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2017 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 org.springframework.data.mongodb.core.aggregation.Aggregation;
+import org.springframework.data.mongodb.core.aggregation.AggregationResults;
+import org.springframework.data.util.CloseableIterator;
+
+/**
+ * {@link ExecutableAggregationOperation} allows creation and execution of MongoDB aggregation operations in a fluent
+ * API style.
+ * The starting {@literal domainType} is used for mapping the {@link Aggregation} provided via {@code by} into the
+ * MongoDB specific representation, as well as mapping back the resulting {@link org.bson.Document}. An alternative
+ * input type for mapping the {@link Aggregation} can be provided by using
+ * {@link org.springframework.data.mongodb.core.aggregation.TypedAggregation}.
+ *
+ *
+ *
+ * aggregateAndReturn(Jedi.class)
+ * .by(newAggregation(Human.class, project("These are not the droids you are looking for")))
+ * .get();
+ *
+ *
+ *
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+public interface ExecutableAggregationOperation {
+
+ /**
+ * Start creating an aggregation operation that returns results mapped to the given domain type.
+ * Use {@link org.springframework.data.mongodb.core.aggregation.TypedAggregation} to specify a potentially different
+ * input type for he aggregation.
+ *
+ * @param domainType must not be {@literal null}.
+ * @return new instance of {@link AggregationOperation}.
+ * @throws IllegalArgumentException if domainType is {@literal null}.
+ */
+ AggregationOperation aggregateAndReturn(Class domainType);
+
+ /**
+ * Collection override (Optional).
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface AggregationOperationWithCollection {
+
+ /**
+ * Explicitly set the name of the collection to perform the query on.
+ * 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 AggregationOperationWithAggregation}.
+ * @throws IllegalArgumentException if collection is {@literal null}.
+ */
+ AggregationOperationWithAggregation inCollection(String collection);
+ }
+
+ /**
+ * Trigger execution by calling one of the terminating methods.
+ *
+ * @param
+ */
+ interface TerminatingAggregationOperation {
+
+ /**
+ * Apply pipeline operations as specified.
+ *
+ * @return never {@literal null}.
+ */
+ AggregationResults get();
+
+ /**
+ * Apply pipeline operations as specified.
+ * Returns a {@link CloseableIterator} that wraps the a Mongo DB {@link com.mongodb.Cursor}
+ *
+ * @return a {@link CloseableIterator} that wraps the a Mongo DB {@link com.mongodb.Cursor} that needs to be closed.
+ * Never {@literal null}.
+ */
+ CloseableIterator stream();
+ }
+
+ /**
+ * Define the aggregation with pipeline stages.
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface AggregationOperationWithAggregation {
+
+ /**
+ * Set the aggregation to be used.
+ *
+ * @param aggregation must not be {@literal null}.
+ * @return new instance of {@link TerminatingAggregationOperation}.
+ * @throws IllegalArgumentException if aggregation is {@literal null}.
+ */
+ TerminatingAggregationOperation by(Aggregation aggregation);
+ }
+
+ /**
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface AggregationOperation
+ extends AggregationOperationWithCollection, AggregationOperationWithAggregation {}
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java
new file mode 100644
index 000000000..c4fb33e33
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2017 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 lombok.RequiredArgsConstructor;
+
+import org.springframework.data.mongodb.core.aggregation.Aggregation;
+import org.springframework.data.mongodb.core.aggregation.AggregationResults;
+import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
+import org.springframework.data.util.CloseableIterator;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Implementation of {@link ExecutableAggregationOperation} operating directly on {@link MongoTemplate}.
+ *
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+class ExecutableAggregationOperationSupport implements ExecutableAggregationOperation {
+
+ private final MongoTemplate template;
+
+ /**
+ * Create new instance of ExecutableAggregationOperationSupport.
+ *
+ * @param template must not be {@literal null}.
+ * @throws IllegalArgumentException if template is {@literal null}.
+ */
+ ExecutableAggregationOperationSupport(MongoTemplate template) {
+
+ Assert.notNull(template, "Template must not be null!");
+ this.template = template;
+ }
+
+ @Override
+ public AggregationOperation aggregateAndReturn(Class domainType) {
+
+ Assert.notNull(domainType, "DomainType must not be null!");
+ return new AggregationOperationSupport(template, null, domainType, null);
+ }
+
+ /**
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ @RequiredArgsConstructor
+ static class AggregationOperationSupport
+ implements AggregationOperationWithAggregation, AggregationOperation, TerminatingAggregationOperation {
+
+ private final MongoTemplate template;
+ private final Aggregation aggregation;
+ private final Class domainType;
+ private final String collection;
+
+ @Override
+ public AggregationOperationWithAggregation inCollection(String collection) {
+
+ Assert.hasText(collection, "Collection must not be null nor empty!");
+ return new AggregationOperationSupport(template, aggregation, domainType, collection);
+ }
+
+ @Override
+ public TerminatingAggregationOperation by(Aggregation aggregation) {
+
+ Assert.notNull(aggregation, "Aggregation must not be null!");
+ return new AggregationOperationSupport(template, aggregation, domainType, collection);
+ }
+
+ @Override
+ public AggregationResults get() {
+ return template.aggregate(aggregation, getCollectionName(aggregation), domainType);
+ }
+
+ @Override
+ public CloseableIterator stream() {
+ return template.aggregateStream(aggregation, getCollectionName(aggregation), domainType);
+ }
+
+ private String getCollectionName(Aggregation aggregation) {
+
+ if (StringUtils.hasText(collection)) {
+ return collection;
+ }
+
+ if (aggregation instanceof TypedAggregation) {
+
+ if (((TypedAggregation>) aggregation).getInputType() != null) {
+ return template.determineCollectionName(((TypedAggregation>) aggregation).getInputType());
+ }
+ }
+
+ return template.determineCollectionName(domainType);
+ }
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java
new file mode 100644
index 000000000..f218c4abb
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2017 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 java.util.Optional;
+
+import org.springframework.data.geo.GeoResults;
+import org.springframework.data.mongodb.core.query.NearQuery;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.util.CloseableIterator;
+
+/**
+ * {@link ExecutableFindOperation} allows creation and execution of MongoDB find operations in a fluent API style.
+ *
+ * The starting {@literal domainType} is used for mapping the {@link Query} provided via {@code matching} into the
+ * MongoDB specific representation. By default this originating {@literal domainType} is also used for mapping back the
+ * result from the {@link org.bson.Document}. However it is possible to define an different {@literal returnType} via
+ * {@code as} that is then used for mapping the result mapping.
+ * 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.
+ *
+ *
+ *
+ * query(Human.class)
+ * .inCollection("star-wars")
+ * .as(Jedi.class)
+ * .matching(query(where("firstname").is("luke")))
+ * .all();
+ *
+ *
+ *
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+public interface ExecutableFindOperation {
+
+ /**
+ * Start creating a find operation for the given {@literal domainType}.
+ *
+ * @param domainType must not be {@literal null}.
+ * @return new instance of {@link FindOperation}.
+ * @throws IllegalArgumentException if domainType is {@literal null}.
+ */
+ FindOperation query(Class domainType);
+
+ /**
+ * Trigger find execution by calling one of the terminating methods.
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface TerminatingFindOperation {
+
+ /**
+ * Get exactly zero or one result.
+ *
+ * @return {@link Optional#empty()} if no match found.
+ * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
+ */
+ Optional one();
+
+ /**
+ * Get the first or no result.
+ *
+ * @return {@link Optional#empty()} if no match found.
+ */
+ Optional first();
+
+ /**
+ * Get all matching elements.
+ *
+ * @return never {@literal}.
+ */
+ List all();
+
+ /**
+ * Stream all matching elements.
+ *
+ * @return a {@link CloseableIterator} that wraps the a Mongo DB {@link com.mongodb.Cursor} that needs to be closed.
+ * Never {@literal null}.
+ */
+ CloseableIterator stream();
+ }
+
+ /**
+ * Trigger geonear execution by calling one of the terminating methods.
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface TerminatingFindNearOperation {
+
+ /**
+ * Find all matching elements and return them as {@link org.springframework.data.geo.GeoResult}.
+ *
+ * @return never {@literal null}.
+ */
+ GeoResults all();
+ }
+
+ /**
+ * Terminating operations invoking the actual query execution.
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface FindOperationWithQuery extends TerminatingFindOperation {
+
+ /**
+ * Set the filter query to be used.
+ *
+ * @param query must not be {@literal null}.
+ * @return new instance of {@link TerminatingFindOperation}.
+ * @throws IllegalArgumentException if query is {@literal null}.
+ */
+ TerminatingFindOperation matching(Query query);
+
+ /**
+ * Set the filter query for the geoNear execution.
+ *
+ * @param nearQuery must not be {@literal null}.
+ * @return new instance of {@link TerminatingFindNearOperation}.
+ * @throws IllegalArgumentException if nearQuery is {@literal null}.
+ */
+ TerminatingFindNearOperation near(NearQuery nearQuery);
+ }
+
+ /**
+ * Collection override (Optional).
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface FindOperationWithCollection extends FindOperationWithQuery {
+
+ /**
+ * Explicitly set the name of the collection to perform the query on.
+ * 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 FindOperationWithProjection}.
+ * @throws IllegalArgumentException if collection is {@literal null}.
+ */
+ FindOperationWithProjection inCollection(String collection);
+ }
+
+ /**
+ * Result type override (Optional).
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface FindOperationWithProjection extends FindOperationWithQuery {
+
+ /**
+ * Define the target type fields should be mapped to.
+ * Skip this step if you are anyway only interested in the original domain type.
+ *
+ * @param resultType must not be {@literal null}.
+ * @param result type.
+ * @return new instance of {@link FindOperationWithProjection}.
+ * @throws IllegalArgumentException if resultType is {@literal null}.
+ */
+ FindOperationWithQuery as(Class resultType);
+ }
+
+ /**
+ * {@link FindOperation} provides methods for constructing lookup operations in a fluent way.
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface FindOperation extends FindOperationWithCollection, FindOperationWithProjection {
+
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java
new file mode 100644
index 000000000..4b1943a4b
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2017 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 lombok.RequiredArgsConstructor;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.bson.Document;
+import org.springframework.dao.IncorrectResultSizeDataAccessException;
+import org.springframework.data.mongodb.core.query.BasicQuery;
+import org.springframework.data.mongodb.core.query.NearQuery;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.SerializationUtils;
+import org.springframework.data.util.CloseableIterator;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+import com.mongodb.client.FindIterable;
+
+/**
+ * Implementation of {@link ExecutableFindOperation}.
+ *
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+class ExecutableFindOperationSupport implements ExecutableFindOperation {
+
+ private final MongoTemplate template;
+
+ /**
+ * Create new {@link ExecutableFindOperationSupport}.
+ *
+ * @param template must not be {@literal null}.
+ * @throws IllegalArgumentException if template is {@literal null}.
+ */
+ ExecutableFindOperationSupport(MongoTemplate template) {
+
+ Assert.notNull(template, "Template must not be null!");
+
+ this.template = template;
+ }
+
+ @Override
+ public FindOperation query(Class domainType) {
+
+ Assert.notNull(domainType, "DomainType must not be null!");
+
+ return new FindOperationSupport<>(template, domainType, domainType, null, null);
+ }
+
+ /**
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ @RequiredArgsConstructor
+ static class FindOperationSupport implements FindOperation, FindOperationWithCollection,
+ FindOperationWithProjection, FindOperationWithQuery {
+
+ private final MongoTemplate template;
+ private final Class> domainType;
+ private final Class returnType;
+ private final String collection;
+ private final Query query;
+
+ @Override
+ public FindOperationWithProjection inCollection(String collection) {
+
+ Assert.hasText(collection, "Collection name must not be null nor empty!");
+
+ return new FindOperationSupport<>(template, domainType, returnType, collection, query);
+ }
+
+ @Override
+ public FindOperationWithQuery as(Class returnType) {
+
+ Assert.notNull(returnType, "ReturnType must not be null!");
+
+ return new FindOperationSupport<>(template, domainType, returnType, collection, query);
+ }
+
+ @Override
+ public TerminatingFindOperation matching(Query query) {
+
+ Assert.notNull(query, "Query must not be null!");
+
+ return new FindOperationSupport<>(template, domainType, returnType, collection, query);
+ }
+
+ @Override
+ public Optional one() {
+
+ List result = doFind(new DelegatingQueryCursorPreparer(getCursorPreparer(query, null)).limit(2));
+
+ if (ObjectUtils.isEmpty(result)) {
+ return Optional.empty();
+ }
+
+ if (result.size() > 1) {
+ throw new IncorrectResultSizeDataAccessException("Query " + asString() + " returned non unique result.", 1);
+ }
+
+ return Optional.of(result.iterator().next());
+ }
+
+ @Override
+ public Optional first() {
+
+ List result = doFind(new DelegatingQueryCursorPreparer(getCursorPreparer(query, null)).limit(1));
+ return ObjectUtils.isEmpty(result) ? Optional.empty() : Optional.of(result.iterator().next());
+ }
+
+ @Override
+ public List all() {
+ return doFind(null);
+ }
+
+ @Override
+ public CloseableIterator stream() {
+ return doStream();
+ }
+
+ @Override
+ public TerminatingFindNearOperation near(NearQuery nearQuery) {
+ return () -> template.geoNear(nearQuery, domainType, getCollectionName(), returnType);
+ }
+
+ private List doFind(CursorPreparer preparer) {
+
+ Document queryObject = query != null ? query.getQueryObject() : new Document();
+ Document fieldsObject = query != null ? query.getFieldsObject() : new Document();
+
+ return template.doFind(getCollectionName(), queryObject, fieldsObject, domainType, returnType,
+ getCursorPreparer(query, preparer));
+ }
+
+ private CloseableIterator doStream() {
+
+ return template.doStream(query != null ? query : new BasicQuery(new Document()), domainType, getCollectionName(),
+ returnType);
+ }
+
+ private CursorPreparer getCursorPreparer(Query query, CursorPreparer preparer) {
+ return query == null || preparer != null ? preparer : template.new QueryCursorPreparer(query, domainType);
+ }
+
+ private String getCollectionName() {
+ return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType);
+ }
+
+ private String asString() {
+ return SerializationUtils.serializeToJsonSafely(query);
+ }
+ }
+
+ /**
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ static class DelegatingQueryCursorPreparer implements CursorPreparer {
+
+ private final CursorPreparer delegate;
+ private Optional limit = Optional.empty();
+
+ public DelegatingQueryCursorPreparer(CursorPreparer delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public FindIterable prepare(FindIterable cursor) {
+
+ FindIterable target = delegate.prepare(cursor);
+ return limit.map(it -> target.limit(it)).orElse(target);
+ }
+
+ CursorPreparer limit(int limit) {
+
+ this.limit = Optional.of(limit);
+ return this;
+ }
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperation.java
new file mode 100644
index 000000000..083f80ea5
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperation.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2017 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.Collection;
+
+import org.springframework.data.mongodb.core.BulkOperations.BulkMode;
+
+import com.mongodb.bulk.BulkWriteResult;
+
+/**
+ * {@link ExecutableFindOperation} allows creation and execution of MongoDB insert and bulk insert operations in a
+ * fluent API style.
+ * 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.
+ *
+ *
+ *
+ * insert(Jedi.class)
+ * .inCollection("star-wars")
+ * .one(luke);
+ *
+ *
+ *
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+public interface ExecutableInsertOperation {
+
+ /**
+ * Start creating an insert operation for given {@literal domainType}.
+ *
+ * @param domainType must not be {@literal null}.
+ * @return new instance of {@link InsertOperation}.
+ * @throws IllegalArgumentException if domainType is {@literal null}.
+ */
+ InsertOperation insert(Class domainType);
+
+ /**
+ * Trigger insert execution by calling one of the terminating methods.
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface TerminatingInsertOperation extends TerminatingBulkInsertOperation {
+
+ /**
+ * Insert exactly one object.
+ *
+ * @param object must not be {@literal null}.
+ * @throws IllegalArgumentException if object is {@literal null}.
+ */
+ void one(T object);
+
+ /**
+ * Insert a collection of objects.
+ *
+ * @param objects must not be {@literal null}.
+ * @throws IllegalArgumentException if objects is {@literal null}.
+ */
+ void all(Collection extends T> objects);
+ }
+
+ /**
+ * Trigger bulk insert execution by calling one of the terminating methods.
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface TerminatingBulkInsertOperation {
+
+ /**
+ * Bulk write collection of objects.
+ *
+ * @param objects must not be {@literal null}.
+ * @return resulting {@link BulkWriteResult}.
+ * @throws IllegalArgumentException if objects is {@literal null}.
+ */
+ BulkWriteResult bulk(Collection extends T> objects);
+ }
+
+ /**
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface InsertOperation
+ extends TerminatingInsertOperation, InsertOperationWithCollection, InsertOperationWithBulkMode {
+
+ }
+
+ /**
+ * Collection override (Optional).
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface InsertOperationWithCollection {
+
+ /**
+ * Explicitly set the name of the collection.
+ * 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 InsertOperationWithBulkMode}.
+ * @throws IllegalArgumentException if collection is {@literal null}.
+ */
+ InsertOperationWithBulkMode inCollection(String collection);
+ }
+
+ /**
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface InsertOperationWithBulkMode extends TerminatingInsertOperation {
+
+ /**
+ * Define the {@link BulkMode} to use for bulk insert operation.
+ *
+ * @param mode must not be {@literal null}.
+ * @return new instance of {@link TerminatingBulkInsertOperation}.
+ * @throws IllegalArgumentException if bulkMode is {@literal null}.
+ */
+ TerminatingBulkInsertOperation withBulkMode(BulkMode bulkMode);
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java
new file mode 100644
index 000000000..83ca96994
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2017 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 lombok.RequiredArgsConstructor;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import org.springframework.data.mongodb.core.BulkOperations.BulkMode;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import com.mongodb.bulk.BulkWriteResult;
+
+/**
+ * Implementation of {@link ExecutableInsertOperation}.
+ *
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+class ExecutableInsertOperationSupport implements ExecutableInsertOperation {
+
+ private final MongoTemplate template;
+
+ /**
+ * Create new {@link ExecutableInsertOperationSupport}.
+ *
+ * @param template must not be {@literal null}.
+ * @throws IllegalArgumentException if template is {@literal null}.
+ */
+ ExecutableInsertOperationSupport(MongoTemplate template) {
+
+ Assert.notNull(template, "Template must not be null!");
+
+ this.template = template;
+ }
+
+ @Override
+ public InsertOperation insert(Class domainType) {
+
+ Assert.notNull(domainType, "DomainType must not be null!");
+ return new InsertOperationSupport(template, domainType, null, null);
+ }
+
+ /**
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ @RequiredArgsConstructor
+ static class InsertOperationSupport implements InsertOperation {
+
+ private final MongoTemplate template;
+ private final Class domainType;
+ private final String collection;
+ private final BulkMode bulkMode;
+
+ @Override
+ public void one(T object) {
+
+ Assert.notNull(object, "Object must not be null!");
+ template.insert(object, getCollectionName());
+ }
+
+ @Override
+ public void all(Collection extends T> objects) {
+
+ Assert.notNull(objects, "Objects must not be null!");
+ template.insert(objects, getCollectionName());
+ }
+
+ @Override
+ public BulkWriteResult bulk(Collection extends T> objects) {
+
+ Assert.notNull(objects, "Objects must not be null!");
+ return template.bulkOps(bulkMode != null ? bulkMode : BulkMode.ORDERED, domainType, getCollectionName())
+ .insert(new ArrayList<>(objects)).execute();
+ }
+
+ @Override
+ public InsertOperationWithBulkMode inCollection(String collection) {
+
+ Assert.hasText(collection, "Collection must not be null nor empty.");
+ return new InsertOperationSupport(template, domainType, collection, bulkMode);
+ }
+
+ @Override
+ public TerminatingBulkInsertOperation withBulkMode(BulkMode bulkMode) {
+
+ Assert.notNull(bulkMode, "BulkMode must not be null!");
+ return new InsertOperationSupport(template, domainType, collection, bulkMode);
+ }
+
+ private String getCollectionName() {
+ return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType);
+ }
+
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java
new file mode 100644
index 000000000..87be5e0f5
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2017 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.query.Query;
+
+import com.mongodb.client.result.DeleteResult;
+
+/**
+ * {@link ExecutableRemoveOperation} allows creation and execution of MongoDB remove / findAndRemove operations in a
+ * fluent API style.
+ * The starting {@literal domainType} is used for mapping the {@link Query} provided via {@code matching} into the
+ * MongoDB specific representation. 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.
+ *
+ *
+ *
+ * remove(Jedi.class)
+ * .inCollection("star-wars")
+ * .matching(query(where("firstname").is("luke")))
+ * .all();
+ *
+ *
+ *
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+public interface ExecutableRemoveOperation {
+
+ /**
+ * Start creating a remove operation for the given {@literal domainType}.
+ *
+ * @param domainType must not be {@literal null}.
+ * @return new instance of {@link RemoveOperation}.
+ * @throws IllegalArgumentException if domainType is {@literal null}.
+ */
+ RemoveOperation remove(Class domainType);
+
+ /**
+ * Collection override (Optional).
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface RemoveOperationWithCollection extends RemoveOperationWithQuery {
+
+ /**
+ * Explicitly set the name of the collection to perform the query on.
+ * 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 RemoveOperationWithCollection}.
+ * @throws IllegalArgumentException if collection is {@literal null}.
+ */
+ RemoveOperationWithQuery inCollection(String collection);
+ }
+
+ /**
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface TerminatingRemoveOperation {
+
+ /**
+ * Remove all documents matching.
+ *
+ * @return
+ */
+ DeleteResult all();
+
+ /**
+ * Remove and return all matching documents.
+ * NOTE The entire list of documents will be fetched before sending the actual delete commands.
+ * Also, {@link org.springframework.context.ApplicationEvent}s will be published for each and every delete
+ * operation.
+ *
+ * @return empty {@link List} if no match found. Never {@literal null}.
+ */
+ List findAndRemove();
+ }
+
+ /**
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface RemoveOperationWithQuery extends TerminatingRemoveOperation {
+
+ /**
+ * Define the query filtering elements.
+ *
+ * @param query must not be {@literal null}.
+ * @return new instance of {@link TerminatingRemoveOperation}.
+ * @throws IllegalArgumentException if query is {@literal null}.
+ */
+ TerminatingRemoveOperation matching(Query query);
+ }
+
+ /**
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface RemoveOperation extends RemoveOperationWithCollection {
+
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java
new file mode 100644
index 000000000..bc3885279
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017 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 lombok.RequiredArgsConstructor;
+
+import java.util.List;
+
+import org.bson.Document;
+import org.springframework.data.mongodb.core.query.BasicQuery;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import com.mongodb.client.result.DeleteResult;
+
+/**
+ * Implementation of {@link ExecutableRemoveOperation}.
+ *
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+class ExecutableRemoveOperationSupport implements ExecutableRemoveOperation {
+
+ private final MongoTemplate tempate;
+
+ /**
+ * Create new {@link ExecutableRemoveOperationSupport}.
+ *
+ * @param template must not be {@literal null}.
+ * @throws IllegalArgumentException if template is {@literal null}.
+ */
+ ExecutableRemoveOperationSupport(MongoTemplate template) {
+
+ Assert.notNull(template, "Template must not be null!");
+ this.tempate = template;
+ }
+
+ @Override
+ public RemoveOperation remove(Class domainType) {
+
+ Assert.notNull(domainType, "DomainType must not be null!");
+ return new RemoveOperationSupport<>(tempate, null, domainType, null);
+ }
+
+ /**
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ @RequiredArgsConstructor
+ static class RemoveOperationSupport implements RemoveOperation, RemoveOperationWithCollection {
+
+ private final MongoTemplate template;
+ private final Query query;
+ private final Class domainType;
+ private final String collection;
+
+ @Override
+ public RemoveOperationWithQuery inCollection(String collection) {
+
+ Assert.hasText(collection, "Collection must not be null nor empty!");
+ return new RemoveOperationSupport<>(template, query, domainType, collection);
+ }
+
+ @Override
+ public TerminatingRemoveOperation matching(Query query) {
+
+ Assert.notNull(query, "Query must not be null!");
+ return new RemoveOperationSupport<>(template, query, domainType, collection);
+ }
+
+ @Override
+ public DeleteResult all() {
+
+ String collectionName = StringUtils.hasText(collection) ? collection
+ : template.determineCollectionName(domainType);
+
+ return template.doRemove(collectionName, query != null ? query : new BasicQuery(new Document()), domainType);
+ }
+
+ @Override
+ public List findAndRemove() {
+
+ String collectionName = StringUtils.hasText(collection) ? collection
+ : template.determineCollectionName(domainType);
+
+ return template.doFindAndDelete(collectionName, query != null ? query : new BasicQuery(new Document()),
+ domainType);
+ }
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java
new file mode 100644
index 000000000..9d57b02f5
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2017 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.Optional;
+
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
+
+import com.mongodb.client.result.UpdateResult;
+
+/**
+ * {@link ExecutableUpdateOperation} allows creation and execution of MongoDB update / findAndModify operations in a
+ * fluent API style.
+ * The starting {@literal domainType} is used for mapping the {@link Query} provided via {@code matching}, as well as
+ * the {@link Update} via {@code apply} into the MongoDB specific representations. 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.
+ *
+ *
+ *
+ * update(Jedi.class)
+ * .inCollection("star-wars")
+ * .matching(query(where("firstname").is("luke")))
+ * .apply(new Update().set("lastname", "skywalker"))
+ * .upsert();
+ *
+ *
+ *
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+public interface ExecutableUpdateOperation {
+
+ /**
+ * Start creating an update operation for the given {@literal domainType}.
+ *
+ * @param domainType must not be {@literal null}.
+ * @return
+ * @throws IllegalArgumentException if domainType is {@literal null}.
+ */
+ UpdateOperation update(Class domainType);
+
+ interface UpdateOperation
+ extends UpdateOperationWithCollection, UpdateOperationWithQuery, UpdateOperationWithUpdate {
+
+ }
+
+ /**
+ * Declare the {@link Update} to apply.
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface UpdateOperationWithUpdate {
+
+ /**
+ * Set the {@link Update} to be applied.
+ *
+ * @param update must not be {@literal null}.
+ * @return new instance of {@link TerminatingUpdateOperation}.
+ * @throws IllegalArgumentException if update is {@literal null}.
+ */
+ TerminatingUpdateOperation apply(Update update);
+ }
+
+ /**
+ * Explicitly define the name of the collection to perform operation in.
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface UpdateOperationWithCollection {
+
+ /**
+ * Explicitly set the name of the collection to perform the query on.
+ * 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 UpdateOperationWithCollection}.
+ * @throws IllegalArgumentException if collection is {@literal null}.
+ */
+ UpdateOperationWithQuery inCollection(String collection);
+ }
+
+ /**
+ * Define a filter query for the {@link Update}.
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface UpdateOperationWithQuery extends UpdateOperationWithUpdate {
+
+ /**
+ * Filter documents by given {@literal query}.
+ *
+ * @param query must not be {@literal null}.
+ * @return new instance of {@link UpdateOperationWithQuery}.
+ * @throws IllegalArgumentException if query is {@literal null}.
+ */
+ UpdateOperationWithUpdate matching(Query query);
+ }
+
+ /**
+ * Define {@link FindAndModifyOptions}.
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface FindAndModifyWithOptions {
+
+ /**
+ * Explicitly define {@link FindAndModifyOptions} for the {@link Update}.
+ *
+ * @param options must not be {@literal null}.
+ * @return new instance of {@link FindAndModifyWithOptions}.
+ * @throws IllegalArgumentException if options is {@literal null}.
+ */
+ TerminatingFindAndModifyOperation withOptions(FindAndModifyOptions options);
+ }
+
+ /**
+ * Trigger findAndModify execution by calling one of the terminating methods.
+ *
+ * @param
+ */
+ interface TerminatingFindAndModifyOperation {
+
+ /**
+ * Find, modify and return the first matching document.
+ *
+ * @return {@link Optional#empty()} if nothing found.
+ */
+ Optional findAndModify();
+ }
+
+ /**
+ * Trigger update execution by calling one of the terminating methods.
+ *
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ interface TerminatingUpdateOperation extends TerminatingFindAndModifyOperation, FindAndModifyWithOptions {
+
+ /**
+ * Update all matching documents in the collection.
+ *
+ * @return never {@literal null}.
+ */
+ UpdateResult all();
+
+ /**
+ * Update the first document in the collection.
+ *
+ * @return never {@literal null}.
+ */
+ UpdateResult first();
+
+ /**
+ * Creates a new document if no documents match the filter query or updates the matching ones.
+ *
+ * @return never {@literal null}.
+ */
+ UpdateResult upsert();
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java
new file mode 100644
index 000000000..885677682
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2017 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 lombok.RequiredArgsConstructor;
+
+import java.util.Optional;
+
+import org.bson.Document;
+import org.springframework.data.mongodb.core.query.BasicQuery;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import com.mongodb.client.result.UpdateResult;
+
+/**
+ * Implementation of {@link ExecutableUpdateOperation}.
+ *
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
+
+ private final MongoTemplate template;
+
+ /**
+ * Creates new {@link ExecutableUpdateOperationSupport}.
+ *
+ * @param template must not be {@literal null}.
+ */
+ ExecutableUpdateOperationSupport(MongoTemplate template) {
+
+ Assert.notNull(template, "Template must not be null!");
+ this.template = template;
+ }
+
+ @Override
+ public UpdateOperation update(Class domainType) {
+
+ Assert.notNull(domainType, "DomainType must not be null!");
+ return new UpdateOperationSupport(template, null, domainType, null, null, null);
+ }
+
+ /**
+ * @param
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+ @RequiredArgsConstructor
+ static class UpdateOperationSupport implements UpdateOperation, UpdateOperationWithCollection,
+ UpdateOperationWithQuery, TerminatingUpdateOperation {
+
+ private final MongoTemplate template;
+ private final Query query;
+ private final Class domainType;
+ private final Update update;
+ private final String collection;
+ private final FindAndModifyOptions options;
+
+ @Override
+ public TerminatingUpdateOperation apply(Update update) {
+
+ Assert.notNull(update, "Update must not be null!");
+ return new UpdateOperationSupport(template, query, domainType, update, collection, options);
+ }
+
+ @Override
+ public UpdateOperationWithQuery inCollection(String collection) {
+
+ Assert.hasText(collection, "Collection must not be null nor empty!");
+ return new UpdateOperationSupport(template, query, domainType, update, collection, options);
+ }
+
+ @Override
+ public UpdateResult first() {
+ return doUpdate(false, false);
+ }
+
+ @Override
+ public UpdateResult upsert() {
+ return doUpdate(true, true);
+ }
+
+ @Override
+ public Optional findAndModify() {
+
+ String collectionName = StringUtils.hasText(collection) ? collection
+ : template.determineCollectionName(domainType);
+
+ return Optional.ofNullable(template.findAndModify(query != null ? query : new BasicQuery(new Document()), update,
+ options, domainType, collectionName));
+ }
+
+ @Override
+ public UpdateOperationWithUpdate matching(Query query) {
+
+ Assert.notNull(query, "Query must not be null!");
+ return new UpdateOperationSupport(template, query, domainType, update, collection, options);
+ }
+
+ @Override
+ public UpdateResult all() {
+ return doUpdate(true, false);
+ }
+
+ @Override
+ public TerminatingFindAndModifyOperation withOptions(FindAndModifyOptions options) {
+
+ Assert.notNull(options, "Options must not be null!");
+ return new UpdateOperationSupport(template, query, domainType, update, collection, options);
+ }
+
+ private UpdateResult doUpdate(boolean multi, boolean upsert) {
+
+ String collectionName = StringUtils.hasText(collection) ? collection
+ : template.determineCollectionName(domainType);
+
+ Query query = this.query != null ? this.query : new BasicQuery(new Document());
+
+ return template.doUpdate(collectionName, query, update, domainType, upsert, multi);
+ }
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FluentMongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FluentMongoOperations.java
new file mode 100644
index 000000000..0daa36e6c
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FluentMongoOperations.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2017 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;
+
+/**
+ * Stripped down interface providing access to a fluent API that specifies a basic set of MongoDB operations.
+ *
+ * @author Christoph Strobl
+ * @since 2.0
+ */
+public interface FluentMongoOperations extends ExecutableFindOperation, ExecutableInsertOperation,
+ ExecutableUpdateOperation, ExecutableRemoveOperation, ExecutableAggregationOperation {
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
index cf0c737d1..2935e167d 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
@@ -58,7 +58,7 @@ import com.mongodb.client.result.UpdateResult;
* @author Thomas Darimont
* @author Maninder Singh
*/
-public interface MongoOperations {
+public interface MongoOperations extends FluentMongoOperations {
/**
* The collection name used for the specified class by this template.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
index ade633bf5..01f5120f8 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
@@ -344,10 +344,16 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
*/
@Override
public CloseableIterator stream(final Query query, final Class entityType, final String collectionName) {
+ return doStream(query, entityType, collectionName, entityType);
+ }
+
+ protected CloseableIterator doStream(final Query query, final Class> entityType, final String collectionName,
+ Class returnType) {
Assert.notNull(query, "Query must not be null!");
Assert.notNull(entityType, "Entity type must not be null!");
Assert.hasText(collectionName, "Collection name must not be null or empty!");
+ Assert.notNull(returnType, "ReturnType must not be null!");
return execute(collectionName, new CollectionCallback>() {
@@ -364,7 +370,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
.prepare(collection.find(mappedQuery).projection(mappedFields));
return new CloseableIterableCursorAdapter(cursor, exceptionTranslator,
- new ReadDocumentCallback(mongoConverter, entityType, collectionName));
+ new ReadDocumentCallback(mongoConverter, returnType, collectionName));
}
});
}
@@ -644,17 +650,21 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
@SuppressWarnings("unchecked")
- public GeoResults geoNear(NearQuery near, Class entityClass, String collectionName) {
+ public GeoResults geoNear(NearQuery near, Class domainType, String collectionName) {
+ return geoNear(near, domainType, collectionName, domainType);
+ }
+
+ public GeoResults geoNear(NearQuery near, Class> domainType, String collectionName, Class returnType) {
if (near == null) {
throw new InvalidDataAccessApiUsageException("NearQuery must not be null!");
}
- if (entityClass == null) {
+ if (domainType == null) {
throw new InvalidDataAccessApiUsageException("Entity class must not be null!");
}
- String collection = StringUtils.hasText(collectionName) ? collectionName : determineCollectionName(entityClass);
+ String collection = StringUtils.hasText(collectionName) ? collectionName : determineCollectionName(domainType);
Document nearDocument = near.toDocument();
Document command = new Document("geoNear", collection);
@@ -662,12 +672,12 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
if (nearDocument.containsKey("query")) {
Document query = (Document) nearDocument.get("query");
- command.put("query", queryMapper.getMappedObject(query, getPersistentEntity(entityClass)));
+ command.put("query", queryMapper.getMappedObject(query, getPersistentEntity(domainType)));
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Executing geoNear using: {} for class: {} in collection: {}", serializeToJsonSafely(command),
- entityClass, collectionName);
+ domainType, collectionName);
}
Document commandResult = executeCommand(command, this.readPreference);
@@ -675,7 +685,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
results = results == null ? Collections.emptyList() : results;
DocumentCallback> callback = new GeoNearResultDocumentCallback(
- new ReadDocumentCallback(mongoConverter, entityClass, collectionName), near.getMetric());
+ new ReadDocumentCallback(mongoConverter, returnType, collectionName), near.getMetric());
List> result = new ArrayList>(results.size());
int index = 0;
@@ -1776,6 +1786,31 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
});
}
+ @Override
+ public FindOperation query(Class domainType) {
+ return new ExecutableFindOperationSupport(this).query(domainType);
+ }
+
+ @Override
+ public UpdateOperation update(Class domainType) {
+ return new ExecutableUpdateOperationSupport(this).update(domainType);
+ }
+
+ @Override
+ public RemoveOperation remove(Class domainType) {
+ return new ExecutableRemoveOperationSupport(this).remove(domainType);
+ }
+
+ @Override
+ public AggregationOperation aggregateAndReturn(Class domainType) {
+ return new ExecutableAggregationOperationSupport(this).aggregateAndReturn(domainType);
+ }
+
+ @Override
+ public InsertOperation insert(Class domainType) {
+ return new ExecutableInsertOperationSupport(this).insert(domainType);
+ }
+
/**
* Assert that the {@link Document} does not enable Aggregation explain mode.
*
@@ -1958,6 +1993,38 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
collectionName);
}
+ /**
+ * Map the results of an ad-hoc query on the default MongoDB collection to a List of the specified targetClass while
+ * using sourceClass for mapping the query.
+ *
+ * @param collectionName
+ * @param query
+ * @param fields
+ * @param sourceClass
+ * @param targetClass
+ * @param objectCallback
+ * @param
+ * @param
+ * @return
+ * @since 2.0
+ */
+ List doFind(String collectionName, Document query, Document fields, Class sourceClass,
+ Class targetClass, CursorPreparer preparer) {
+
+ MongoPersistentEntity> entity = mappingContext.getRequiredPersistentEntity(sourceClass);
+
+ Document mappedFields = queryMapper.getMappedFields(fields, entity);
+ Document mappedQuery = queryMapper.getMappedObject(query, entity);
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("find using query: {} fields: {} for class: {} in collection: {}",
+ serializeToJsonSafely(mappedQuery), mappedFields, sourceClass, collectionName);
+ }
+
+ return executeFindMultiInternal(new FindCallback(mappedQuery, mappedFields), preparer,
+ new ReadDocumentCallback(mongoConverter, targetClass, collectionName), collectionName);
+ }
+
protected Document convertToDocument(CollectionOptions collectionOptions) {
Document document = new Document();
@@ -2674,4 +2741,5 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
public MongoDbFactory getMongoDbFactory() {
return mongoDbFactory;
}
+
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java
new file mode 100644
index 000000000..1a01e4452
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2017 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.assertj.core.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.data.mongodb.core.aggregation.Aggregation;
+
+/**
+ * @author Christoph Strobl
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class ExecutableAggregationOperationSupportUnitTests {
+
+ @Mock MongoTemplate template;
+ ExecutableAggregationOperationSupport opSupport;
+
+ @Before
+ public void setUp() {
+ opSupport = new ExecutableAggregationOperationSupport(template);
+ }
+
+ @Test(expected = IllegalArgumentException.class) // DATAMONGO-1563
+ public void throwsExceptionOnNullDomainType() {
+ opSupport.aggregateAndReturn(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class) // DATAMONGO-1563
+ public void throwsExceptionOnNullCollectionWhenUsed() {
+ opSupport.aggregateAndReturn(Person.class).inCollection(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class) // DATAMONGO-1563
+ public void throwsExceptionOnEmptyCollectionWhenUsed() {
+ opSupport.aggregateAndReturn(Person.class).inCollection("");
+ }
+
+ @Test(expected = IllegalArgumentException.class) // DATAMONGO-1563
+ public void throwsExceptionOnNullAggregation() {
+ opSupport.aggregateAndReturn(Person.class).by(null);
+ }
+
+ @Test // DATAMONGO-1563
+ public void aggregateWithUntypedAggregationAndExplicitCollection() {
+
+ opSupport.aggregateAndReturn(Person.class).inCollection("star-wars").by(newAggregation(project("foo"))).get();
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class);
+ verify(template).aggregate(any(Aggregation.class), eq("star-wars"), captor.capture());
+ assertThat(captor.getValue()).isEqualTo(Person.class);
+ }
+
+ @Test // DATAMONGO-1563
+ public void aggregateWithUntypedAggregation() {
+
+ when(template.determineCollectionName(any(Class.class))).thenReturn("person");
+
+ opSupport.aggregateAndReturn(Person.class).by(newAggregation(project("foo"))).get();
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class);
+
+ verify(template).determineCollectionName(captor.capture());
+ verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture());
+
+ assertThat(captor.getAllValues()).containsExactly(Person.class, Person.class);
+ }
+
+ @Test // DATAMONGO-1563
+ public void aggregateWithTypeAggregation() {
+
+ when(template.determineCollectionName(any(Class.class))).thenReturn("person");
+
+ opSupport.aggregateAndReturn(Jedi.class).by(newAggregation(Person.class, project("foo"))).get();
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class);
+
+ verify(template).determineCollectionName(captor.capture());
+ verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture());
+
+ assertThat(captor.getAllValues()).containsExactly(Person.class, Jedi.class);
+ }
+
+ @Test // DATAMONGO-1563
+ public void aggregateStreamWithUntypedAggregationAndExplicitCollection() {
+
+ opSupport.aggregateAndReturn(Person.class).inCollection("star-wars").by(newAggregation(project("foo"))).stream();
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class);
+ verify(template).aggregateStream(any(Aggregation.class), eq("star-wars"), captor.capture());
+ assertThat(captor.getValue()).isEqualTo(Person.class);
+ }
+
+ @Test // DATAMONGO-1563
+ public void aggregateStreamWithUntypedAggregation() {
+
+ when(template.determineCollectionName(any(Class.class))).thenReturn("person");
+
+ opSupport.aggregateAndReturn(Person.class).by(newAggregation(project("foo"))).stream();
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class);
+
+ verify(template).determineCollectionName(captor.capture());
+ verify(template).aggregateStream(any(Aggregation.class), eq("person"), captor.capture());
+
+ assertThat(captor.getAllValues()).containsExactly(Person.class, Person.class);
+ }
+
+ @Test // DATAMONGO-1563
+ public void aggregateStreamWithTypeAggregation() {
+
+ when(template.determineCollectionName(any(Class.class))).thenReturn("person");
+
+ opSupport.aggregateAndReturn(Jedi.class).by(newAggregation(Person.class, project("foo"))).stream();
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class);
+
+ verify(template).determineCollectionName(captor.capture());
+ verify(template).aggregateStream(any(Aggregation.class), eq("person"), captor.capture());
+
+ assertThat(captor.getAllValues()).containsExactly(Person.class, Jedi.class);
+ }
+
+ static class Person {
+
+ }
+
+ static class Jedi {}
+
+}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java
new file mode 100644
index 000000000..0e68082d9
--- /dev/null
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2017 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.assertj.core.api.Assertions.*;
+import static org.springframework.data.mongodb.core.query.Criteria.*;
+import static org.springframework.data.mongodb.core.query.Query.*;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.dao.IncorrectResultSizeDataAccessException;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.geo.GeoResults;
+import org.springframework.data.geo.Point;
+import org.springframework.data.mongodb.core.index.GeoSpatialIndexType;
+import org.springframework.data.mongodb.core.index.GeospatialIndex;
+import org.springframework.data.mongodb.core.mapping.Field;
+import org.springframework.data.mongodb.core.query.NearQuery;
+import org.springframework.data.util.CloseableIterator;
+
+import com.mongodb.MongoClient;
+
+/**
+ * @author Christoph Strobl
+ */
+public class ExecutableFindOperationSupportTests {
+
+ private static final String STAR_WARS = "star-wars";
+ MongoTemplate template;
+
+ Person han;
+ Person luke;
+
+ @Before
+ public void setUp() {
+
+ template = new MongoTemplate(new SimpleMongoDbFactory(new MongoClient(), "ExecutableFindOperationSupportTests"));
+ template.dropCollection(STAR_WARS);
+
+ han = new Person();
+ han.firstname = "han";
+ han.id = "id-1";
+
+ luke = new Person();
+ luke.firstname = "luke";
+ luke.id = "id-2";
+
+ template.save(han);
+ template.save(luke);
+ }
+
+ @Test(expected = IllegalArgumentException.class) // DATAMONGO-1563
+ public void domainTypeIsRequired() {
+ template.query(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class) // DATAMONGO-1563
+ public void returnTypeIsRequiredOnSet() {
+ template.query(Person.class).as(null);
+ }
+
+ @Test(expected = IllegalArgumentException.class) // DATAMONGO-1563
+ public void collectionIsRequiredOnSet() {
+ template.query(Person.class).inCollection(null);
+ }
+
+ @Test // DATAMONGO-1563
+ public void findAll() {
+ assertThat(template.query(Person.class).all()).containsExactlyInAnyOrder(han, luke);
+ }
+
+ @Test // DATAMONGO-1563
+ public void findAllWithCollection() {
+ assertThat(template.query(Human.class).inCollection(STAR_WARS).all()).hasSize(2);
+ }
+
+ @Test // DATAMONGO-1563
+ public void findAllWithProjection() {
+
+ assertThat(template.query(Person.class).as(Jedi.class).all()).hasOnlyElementsOfType(Jedi.class)
+ .hasSize(2);
+ }
+
+ @Test // DATAMONGO-1563
+ public void findAllBy() {
+
+ assertThat(template.query(Person.class).matching(query(where("firstname").is("luke"))).all())
+ .containsExactlyInAnyOrder(luke);
+ }
+
+ @Test // DATAMONGO-1563
+ public void findAllByWithCollectionUsingMappingInformation() {
+
+ assertThat(template.query(Jedi.class).inCollection(STAR_WARS).matching(query(where("name").is("luke"))).all()).hasSize(1)
+ .hasOnlyElementsOfType(Jedi.class);
+ }
+
+ @Test // DATAMONGO-1563
+ public void findAllByWithCollection() {
+ assertThat(template.query(Human.class).inCollection(STAR_WARS).matching(query(where("firstname").is("luke"))).all())
+ .hasSize(1);
+ }
+
+ @Test // DATAMONGO-1563
+ public void findAllByWithProjection() {
+
+ assertThat(template.query(Person.class).as(Jedi.class).matching(query(where("firstname").is("luke"))).all())
+ .hasOnlyElementsOfType(Jedi.class).hasSize(1);
+ }
+
+ @Test // DATAMONGO-1563
+ public void findBy() {
+ assertThat(template.query(Person.class).matching(query(where("firstname").is("luke"))).one()).contains(luke);
+ }
+
+ @Test // DATAMONGO-1563
+ public void findByNoMatch() {
+ assertThat(template.query(Person.class).matching(query(where("firstname").is("spock"))).one()).isEmpty();
+ }
+
+ @Test(expected = IncorrectResultSizeDataAccessException.class) // DATAMONGO-1563
+ public void findByTooManyResults() {
+ template.query(Person.class).matching(query(where("firstname").in("han", "luke"))).one();
+ }
+
+ @Test // DATAMONGO-1563
+ public void streamAll() {
+
+ try (CloseableIterator stream = template.query(Person.class).stream()) {
+ assertThat(stream).containsExactlyInAnyOrder(han, luke);
+ }
+ }
+
+ @Test // DATAMONGO-1563
+ public void streamAllWithCollection() {
+
+ try (CloseableIterator