diff --git a/pom.xml b/pom.xml index cb4e66d37e..5656cb3f41 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 2.0.0.BUILD-SNAPSHOT + 2.0.0.DATAMONGO-1563-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index 4a49168713..c2ff64f291 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 2.0.0.BUILD-SNAPSHOT + 2.0.0.DATAMONGO-1563-SNAPSHOT ../pom.xml @@ -48,7 +48,7 @@ org.springframework.data spring-data-mongodb - 2.0.0.BUILD-SNAPSHOT + 2.0.0.DATAMONGO-1563-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 750ed23aa8..569160a582 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 2.0.0.BUILD-SNAPSHOT + 2.0.0.DATAMONGO-1563-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-log4j/pom.xml b/spring-data-mongodb-log4j/pom.xml index 50d0a6454a..526e5bb318 100644 --- a/spring-data-mongodb-log4j/pom.xml +++ b/spring-data-mongodb-log4j/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 2.0.0.BUILD-SNAPSHOT + 2.0.0.DATAMONGO-1563-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 99ef83a54b..cb73baabd8 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 2.0.0.BUILD-SNAPSHOT + 2.0.0.DATAMONGO-1563-SNAPSHOT ../pom.xml 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 0000000000..2fb319c347 --- /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 0000000000..c4fb33e335 --- /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 0000000000..f218c4abb6 --- /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 0000000000..4b1943a4b7 --- /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 0000000000..083f80ea5f --- /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 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 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 0000000000..83ca96994e --- /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 objects) { + + Assert.notNull(objects, "Objects must not be null!"); + template.insert(objects, getCollectionName()); + } + + @Override + public BulkWriteResult bulk(Collection 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 0000000000..87be5e0f5f --- /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 0000000000..bc38852794 --- /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 0000000000..9d57b02f58 --- /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 0000000000..8856776827 --- /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 0000000000..0daa36e6ce --- /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 cf0c737d14..2935e167de 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 @@ * @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 ade633bf5f..01f5120f81 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 CloseableIterator stream(final Query query, final Class entityT */ @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 CloseableIterator doInCollection(MongoCollection collection) .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 GeoResults geoNear(NearQuery near, Class entityClass) { } @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 GeoResults geoNear(NearQuery near, Class entityClass, String co 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 GeoResults geoNear(NearQuery near, Class entityClass, String co 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 CloseableIterator doInCollection(MongoCollection collection) }); } + @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 @@ protected List doFind(String collectionName, Document query, Document 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 void close() { 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 0000000000..1a01e44523 --- /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 0000000000..0e68082d95 --- /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 stream = template.query(Human.class).inCollection(STAR_WARS).stream()) { + assertThat(stream).hasSize(2); + } + } + + @Test // DATAMONGO-1563 + public void streamAllWithProjection() { + + try (CloseableIterator stream = template.query(Person.class).as(Jedi.class).stream()) { + assertThat(stream).hasOnlyElementsOfType(Jedi.class).hasSize(2); + } + } + + @Test // DATAMONGO-1563 + public void streamAllBy() { + + try (CloseableIterator stream = template.query(Person.class) + .matching(query(where("firstname").is("luke"))).stream()) { + + assertThat(stream).containsExactlyInAnyOrder(luke); + } + } + + @Test // DATAMONGO-1563 + public void findAllNearBy() { + + template.indexOps(Planet.class).ensureIndex( + new GeospatialIndex("coordinates").typed(GeoSpatialIndexType.GEO_2DSPHERE).named("planet-coordinate-idx")); + + Planet alderan = new Planet("alderan", new Point(-73.9836, 40.7538)); + Planet dantooine = new Planet("dantooine", new Point(-73.9928, 40.7193)); + + template.save(alderan); + template.save(dantooine); + + GeoResults results = template.query(Planet.class) + .near(NearQuery.near(-73.9667, 40.78).spherical(true)).all(); + assertThat(results.getContent()).hasSize(2); + assertThat(results.getContent().get(0).getDistance()).isNotNull(); + } + + @Test // DATAMONGO-1563 + public void findAllNearByWithCollectionAndProjection() { + + template.indexOps(Planet.class).ensureIndex( + new GeospatialIndex("coordinates").typed(GeoSpatialIndexType.GEO_2DSPHERE).named("planet-coordinate-idx")); + + Planet alderan = new Planet("alderan", new Point(-73.9836, 40.7538)); + Planet dantooine = new Planet("dantooine", new Point(-73.9928, 40.7193)); + + template.save(alderan); + template.save(dantooine); + + GeoResults results = template.query(Object.class).inCollection(STAR_WARS).as(Human.class) + .near(NearQuery.near(-73.9667, 40.78).spherical(true)).all(); + + assertThat(results.getContent()).hasSize(2); + assertThat(results.getContent().get(0).getDistance()).isNotNull(); + assertThat(results.getContent().get(0).getContent()).isInstanceOf(Human.class); + assertThat(results.getContent().get(0).getContent().getId()).isEqualTo("alderan"); + } + + @Data + @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) + static class Person { + @Id String id; + String firstname; + } + + @Data + static class Human { + @Id String id; + } + + @Data + static class Jedi { + + @Field("firstname") String name; + } + + @Data + @AllArgsConstructor + @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) + static class Planet { + + @Id String name; + Point coordinates; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupportUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupportUnitTests.java new file mode 100644 index 0000000000..326cf271ff --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupportUnitTests.java @@ -0,0 +1,145 @@ +/* + * 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.mockito.Mockito.anyList; + +import lombok.Data; + +import java.util.Arrays; + +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.annotation.Id; +import org.springframework.data.mongodb.core.BulkOperations.BulkMode; + +/** + * @author Christoph Strobl + * @since 2017/06 + */ +@RunWith(MockitoJUnitRunner.Silent.class) +public class ExecutableInsertOperationSupportUnitTests { + + private static final String STAR_WARS = "star-wars"; + + @Mock MongoTemplate template; + @Mock BulkOperations bulkOperations; + + ExecutableInsertOperationSupport ops; + + Person luke, han; + + @Before + public void setUp() { + + when(template.bulkOps(any(), any(), any())).thenReturn(bulkOperations); + when(template.determineCollectionName(any(Class.class))).thenReturn(STAR_WARS); + when(bulkOperations.insert(anyList())).thenReturn(bulkOperations); + + ops = new ExecutableInsertOperationSupport(template); + + luke = new Person(); + luke.id = "id-1"; + luke.firstname = "luke"; + + han = new Person(); + han.firstname = "han"; + han.id = "id-2"; + } + + @Test(expected = IllegalArgumentException.class) // DATAMONGO-1563 + public void nullCollectionShouldThrowException() { + ops.insert(Person.class).inCollection(null); + + } + + @Test(expected = IllegalArgumentException.class) // DATAMONGO-1563 + public void nullBulkModeShouldThrowException() { + ops.insert(Person.class).withBulkMode(null); + } + + @Test // DATAMONGO-1563 + public void insertShouldUseDerivedCollectionName() { + + ops.insert(Person.class).one(luke); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); + + verify(template).determineCollectionName(captor.capture()); + verify(template).insert(eq(luke), eq(STAR_WARS)); + + assertThat(captor.getAllValues()).containsExactly(Person.class); + } + + @Test // DATAMONGO-1563 + public void insertShouldUseExplicitCollectionName() { + + ops.insert(Person.class).inCollection(STAR_WARS).one(luke); + + verify(template, never()).determineCollectionName(any(Class.class)); + verify(template).insert(eq(luke), eq(STAR_WARS)); + } + + @Test // DATAMONGO-1563 + public void insertCollectionShouldDelegateCorrectly() { + + ops.insert(Person.class).all(Arrays.asList(luke, han)); + + verify(template).determineCollectionName(any(Class.class)); + verify(template).insert(anyList(), eq(STAR_WARS)); + } + + @Test // DATAMONGO-1563 + public void bulkInsertCollectionShouldDelegateCorrectly() { + + ops.insert(Person.class).bulk(Arrays.asList(luke, han)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); + + verify(template).determineCollectionName(any(Class.class)); + verify(template).bulkOps(eq(BulkMode.ORDERED), captor.capture(), eq(STAR_WARS)); + verify(bulkOperations).insert(anyList()); + verify(bulkOperations).execute(); + } + + @Test // DATAMONGO-1563 + public void bulkInsertWithBulkModeShouldDelegateCorrectly() { + + ops.insert(Person.class).withBulkMode(BulkMode.UNORDERED).bulk(Arrays.asList(luke, han)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); + + verify(template).determineCollectionName(any(Class.class)); + verify(template).bulkOps(eq(BulkMode.UNORDERED), captor.capture(), eq(STAR_WARS)); + verify(bulkOperations).insert(anyList()); + verify(bulkOperations).execute(); + } + + @Data + @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) + static class Person { + @Id String id; + String firstname; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java new file mode 100644 index 0000000000..5033c7b84d --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java @@ -0,0 +1,108 @@ +/* + * 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.Data; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Field; + +import com.mongodb.MongoClient; +import com.mongodb.client.result.DeleteResult; + +/** + * @author Christoph Strobl + */ +public class ExecutableRemoveOperationSupportTests { + + 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(), "ExecutableRemoveOperationSupportTests")); + 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 // DATAMONGO-1563 + public void removeAll() { + + DeleteResult result = template.remove(Person.class).all(); + + assertThat(result.getDeletedCount()).isEqualTo(2L); + } + + @Test // DATAMONGO-1563 + public void removeAllMatching() { + + DeleteResult result = template.remove(Person.class).matching(query(where("firstname").is("han"))).all(); + + assertThat(result.getDeletedCount()).isEqualTo(1L); + } + + @Test // DATAMONGO-1563 + public void removeAllMatchingWithAlternateDomainTypeAndCollection() { + + DeleteResult result = template.remove(Jedi.class).inCollection(STAR_WARS).matching(query(where("name").is("luke"))) + .all(); + + assertThat(result.getDeletedCount()).isEqualTo(1L); + } + + @Test // DATAMONGO-1563 + public void removeAndReturnAllMatching() { + + List result = template.remove(Person.class).matching(query(where("firstname").is("han"))).findAndRemove(); + + assertThat(result).containsExactly(han); + } + + @Data + @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) + static class Person { + @Id String id; + String firstname; + } + + @Data + static class Jedi { + + @Field("firstname") String name; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java new file mode 100644 index 0000000000..a960016f7e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java @@ -0,0 +1,190 @@ +/* + * 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.Data; + +import java.util.Optional; + +import org.bson.BsonString; +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; + +import com.mongodb.MongoClient; +import com.mongodb.client.result.UpdateResult; + +/** + * @author Christoph Strobl + */ +public class ExecutableUpdateOperationSupportTests { + + 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(), "ExecutableUpdateOperationSupportTests")); + 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.update(null); + } + + @Test(expected = IllegalArgumentException.class) // DATAMONGO-1563 + public void updateIsRequired() { + template.update(Person.class).apply(null); + } + + @Test(expected = IllegalArgumentException.class) // DATAMONGO-1563 + public void collectionIsRequiredOnSet() { + template.update(Person.class).inCollection(null); + } + + @Test(expected = IllegalArgumentException.class) // DATAMONGO-1563 + public void findAndModifyOptionsAreRequiredOnSet() { + template.update(Person.class).apply(new Update()).withOptions(null); + } + + @Test // DATAMONGO-1563 + public void updateFirst() { + + UpdateResult result = template.update(Person.class).apply(new Update().set("firstname", "Han")).first(); + + assertThat(result.getModifiedCount()).isEqualTo(1L); + assertThat(result.getUpsertedId()).isNull(); + } + + @Test // DATAMONGO-1563 + public void updateAll() { + + UpdateResult result = template.update(Person.class).apply(new Update().set("firstname", "Han")).all(); + + assertThat(result.getModifiedCount()).isEqualTo(2L); + assertThat(result.getUpsertedId()).isNull(); + } + + @Test // DATAMONGO-1563 + public void updateAllMatching() { + + UpdateResult result = template.update(Person.class).matching(queryHan()).apply(new Update().set("firstname", "Han")) + .all(); + + assertThat(result.getModifiedCount()).isEqualTo(1L); + assertThat(result.getUpsertedId()).isNull(); + } + + @Test // DATAMONGO-1563 + public void updateWithDifferentDomainClassAndCollection() { + + UpdateResult result = template.update(Jedi.class).inCollection(STAR_WARS) + .matching(query(where("_id").is(han.getId()))).apply(new Update().set("name", "Han")).all(); + + assertThat(result.getModifiedCount()).isEqualTo(1L); + assertThat(result.getUpsertedId()).isNull(); + assertThat(template.findOne(queryHan(), Person.class)).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", + "Han"); + } + + @Test // DATAMONGO-1563 + public void findAndModify() { + + Optional result = template.update(Person.class).matching(queryHan()) + .apply(new Update().set("firstname", "Han")).findAndModify(); + + assertThat(result).contains(han); + assertThat(template.findOne(queryHan(), Person.class)).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", + "Han"); + } + + @Test // DATAMONGO-1563 + public void findAndModifyWithDifferentDomainTypeAndCollection() { + + Optional result = template.update(Jedi.class).inCollection(STAR_WARS) + .matching(query(where("_id").is(han.getId()))).apply(new Update().set("name", "Han")).findAndModify(); + + assertThat(result.get()).hasFieldOrPropertyWithValue("name", "han"); + assertThat(template.findOne(queryHan(), Person.class)).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", + "Han"); + } + + @Test // DATAMONGO-1563 + public void findAndModifyWithOptions() { + + Optional result = template.update(Person.class).matching(queryHan()) + .apply(new Update().set("firstname", "Han")).withOptions(FindAndModifyOptions.options().returnNew(true)) + .findAndModify(); + + assertThat(result.get()).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Han"); + } + + @Test // DATAMONGO-1563 + public void upsert() { + + UpdateResult result = template.update(Person.class).matching(query(where("id").is("id-3"))) + .apply(new Update().set("firstname", "Chewbacca")).upsert(); + + assertThat(result.getModifiedCount()).isEqualTo(0L); + assertThat(result.getUpsertedId()).isEqualTo(new BsonString("id-3")); + } + + private Query queryHan() { + return query(where("id").is(han.getId())); + } + + @Data + @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) + static class Person { + @Id String id; + String firstname; + } + + @Data + static class Human { + @Id String id; + } + + @Data + static class Jedi { + + @Field("firstname") String name; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java index 5cb6c43df2..bbe47bfe6b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java @@ -1159,7 +1159,7 @@ public Object doInCollection(MongoCollection collection) @Test // DATADOC-166 public void removingNullIsANoOp() { - template.remove(null); + template.remove((Object) null); } @Test // DATADOC-240, DATADOC-212