From 91f613749f0088530779007b1517080a143618cb Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 28 May 2025 15:09:42 +0200 Subject: [PATCH] Update `$out` stage rendering to documented format. This commit makes sure to use the documented command format when rendering the $out aggregation stage. Original pull request: #4986 Closes #4969 --- .../core/aggregation/OutOperation.java | 188 +----------------- .../aggregation/OutOperationUnitTest.java | 59 +----- 2 files changed, 15 insertions(+), 232 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java index 7dbed3a85..9d0ddf6d6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java @@ -17,7 +17,7 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; import org.jspecify.annotations.Nullable; -import org.springframework.data.mongodb.util.BsonUtils; + import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -37,33 +37,25 @@ public class OutOperation implements AggregationOperation { private final @Nullable String databaseName; private final String collectionName; - private final @Nullable Document uniqueKey; - private final @Nullable OutMode mode; /** * @param outCollectionName Collection name to export the results. Must not be {@literal null}. */ public OutOperation(String outCollectionName) { - this(null, outCollectionName, null, null); + this(null, outCollectionName); } /** * @param databaseName Optional database name the target collection is located in. Can be {@literal null}. * @param collectionName Collection name to export the results. Must not be {@literal null}. Can be {@literal null}. - * @param uniqueKey Optional unique key spec identify a document in the to collection for replacement or merge. - * @param mode The mode for merging the aggregation pipeline output with the target collection. Can be - * {@literal null}. {@literal null}. * @since 2.2 */ - private OutOperation(@Nullable String databaseName, String collectionName, @Nullable Document uniqueKey, - @Nullable OutMode mode) { + private OutOperation(@Nullable String databaseName, String collectionName) { Assert.notNull(collectionName, "Collection name must not be null"); this.databaseName = databaseName; this.collectionName = collectionName; - this.uniqueKey = uniqueKey; - this.mode = mode; } /** @@ -76,187 +68,21 @@ public class OutOperation implements AggregationOperation { */ @Contract("_ -> new") public OutOperation in(@Nullable String database) { - return new OutOperation(database, collectionName, uniqueKey, mode); - } - - /** - * Optionally specify the field that uniquely identifies a document in the target collection.
- * For convenience the given {@literal key} can either be a single field name or the Json representation of a key - * {@link Document}. - * - *
-	 *
-	 * // {
-	 * //    "field-1" : 1
-	 * // }
-	 * .uniqueKey("field-1")
-	 *
-	 * // {
-	 * //    "field-1" : 1,
-	 * //    "field-2" : 1
-	 * // }
-	 * .uniqueKey("{ 'field-1' : 1, 'field-2' : 1}")
-	 * 
- * - * NOTE: Requires MongoDB 4.2 or later. - * - * @param key can be {@literal null}. Server uses {@literal _id} when {@literal null}. - * @return new instance of {@link OutOperation}. - * @since 2.2 - */ - @Contract("_ -> new") - public OutOperation uniqueKey(@Nullable String key) { - - Document uniqueKey = key == null ? null : BsonUtils.toDocumentOrElse(key, it -> new Document(it, 1)); - return new OutOperation(databaseName, collectionName, uniqueKey, mode); - } - - /** - * Optionally specify the fields that uniquely identifies a document in the target collection.
- * - *
-	 *
-	 * // {
-	 * //    "field-1" : 1
-	 * //    "field-2" : 1
-	 * // }
-	 * .uniqueKeyOf(Arrays.asList("field-1", "field-2"))
-	 * 
- * - * NOTE: Requires MongoDB 4.2 or later. - * - * @param fields must not be {@literal null}. - * @return new instance of {@link OutOperation}. - * @since 2.2 - */ - @Contract("_ -> new") - public OutOperation uniqueKeyOf(Iterable fields) { - - Assert.notNull(fields, "Fields must not be null"); - - Document uniqueKey = new Document(); - fields.forEach(it -> uniqueKey.append(it, 1)); - - return new OutOperation(databaseName, collectionName, uniqueKey, mode); - } - - /** - * Specify how to merge the aggregation output with the target collection.
- * NOTE: Requires MongoDB 4.2 or later. - * - * @param mode must not be {@literal null}. - * @return new instance of {@link OutOperation}. - * @since 2.2 - */ - @Contract("_ -> new") - public OutOperation mode(OutMode mode) { - - Assert.notNull(mode, "Mode must not be null"); - return new OutOperation(databaseName, collectionName, uniqueKey, mode); - } - - /** - * Replace the target collection.
- * NOTE: Requires MongoDB 4.2 or later. - * - * @return new instance of {@link OutOperation}. - * @see OutMode#REPLACE_COLLECTION - * @since 2.2 - */ - @Contract("-> new") - public OutOperation replaceCollection() { - return mode(OutMode.REPLACE_COLLECTION); - } - - /** - * Replace/Upsert documents in the target collection.
- * NOTE: Requires MongoDB 4.2 or later. - * - * @return new instance of {@link OutOperation}. - * @see OutMode#REPLACE - * @since 2.2 - */ - @Contract("-> new") - public OutOperation replaceDocuments() { - return mode(OutMode.REPLACE); - } - - /** - * Insert documents to the target collection.
- * NOTE: Requires MongoDB 4.2 or later. - * - * @return new instance of {@link OutOperation}. - * @see OutMode#INSERT - * @since 2.2 - */ - @Contract("-> new") - public OutOperation insertDocuments() { - return mode(OutMode.INSERT); + return new OutOperation(database, collectionName); } @Override public Document toDocument(AggregationOperationContext context) { - if (!requiresMongoDb42Format()) { - return new Document("$out", collectionName); + if (!StringUtils.hasText(databaseName)) { + return new Document(getOperator(), collectionName); } - Assert.state(mode != null, "Mode must not be null"); - - Document $out = new Document("to", collectionName) // - .append("mode", mode.getMongoMode()); - - if (StringUtils.hasText(databaseName)) { - $out.append("db", databaseName); - } - - if (uniqueKey != null) { - $out.append("uniqueKey", uniqueKey); - } - - return new Document(getOperator(), $out); + return new Document(getOperator(), new Document("db", databaseName).append("coll", collectionName)); } @Override public String getOperator() { return "$out"; } - - private boolean requiresMongoDb42Format() { - return StringUtils.hasText(databaseName) || mode != null || uniqueKey != null; - } - - /** - * The mode for merging the aggregation pipeline output. - * - * @author Christoph Strobl - * @since 2.2 - */ - public enum OutMode { - - /** - * Write documents to the target collection. Errors if a document same uniqueKey already exists. - */ - INSERT("insertDocuments"), - - /** - * Update on any document in the target collection with the same uniqueKey. - */ - REPLACE("replaceDocuments"), - - /** - * Replaces the to collection with the output from the aggregation pipeline. Cannot be in a different database. - */ - REPLACE_COLLECTION("replaceCollection"); - - private final String mode; - - OutMode(String mode) { - this.mode = mode; - } - - public String getMongoMode() { - return mode; - } - } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/OutOperationUnitTest.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/OutOperationUnitTest.java index f8812448b..a86433175 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/OutOperationUnitTest.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/OutOperationUnitTest.java @@ -18,8 +18,6 @@ package org.springframework.data.mongodb.core.aggregation; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; import static org.springframework.data.mongodb.test.util.Assertions.*; -import java.util.Arrays; - import org.bson.Document; import org.junit.jupiter.api.Test; @@ -30,65 +28,24 @@ import org.junit.jupiter.api.Test; * @author Christoph Strobl * @author Mark Paluch */ -public class OutOperationUnitTest { +class OutOperationUnitTest { @Test // DATAMONGO-1418 - public void shouldCheckNPEInCreation() { + void shouldCheckNPEInCreation() { assertThatIllegalArgumentException().isThrownBy(() -> new OutOperation(null)); } @Test // DATAMONGO-2259 - public void shouldUsePreMongoDB42FormatWhenOnlyCollectionIsPresent() { + void shouldUsePreMongoDB42FormatWhenOnlyCollectionIsPresent() { assertThat(out("out-col").toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo(new Document("$out", "out-col")); } - @Test // DATAMONGO-2259 - public void shouldUseMongoDB42ExtendedFormatWhenAdditionalParametersPresent() { - - assertThat(out("out-col").insertDocuments().toDocument(Aggregation.DEFAULT_CONTEXT)) - .isEqualTo(new Document("$out", new Document("to", "out-col").append("mode", "insertDocuments"))); - } - - @Test // DATAMONGO-2259 - public void shouldRenderExtendedFormatWithJsonStringKey() { - - assertThat(out("out-col").insertDocuments() // - .in("database-2") // - .uniqueKey("{ 'field-1' : 1, 'field-2' : 1}") // - .toDocument(Aggregation.DEFAULT_CONTEXT)) // - .containsEntry("$out.to", "out-col") // - .containsEntry("$out.mode", "insertDocuments") // - .containsEntry("$out.db", "database-2") // - .containsEntry("$out.uniqueKey", new Document("field-1", 1).append("field-2", 1)); - } - - @Test // DATAMONGO-2259 - public void shouldRenderExtendedFormatWithSingleFieldKey() { - - assertThat(out("out-col").insertDocuments().in("database-2") // - .uniqueKey("field-1").toDocument(Aggregation.DEFAULT_CONTEXT)) // - .containsEntry("$out.to", "out-col") // - .containsEntry("$out.mode", "insertDocuments") // - .containsEntry("$out.db", "database-2") // - .containsEntry("$out.uniqueKey", new Document("field-1", 1)); - } - - @Test // DATAMONGO-2259 - public void shouldRenderExtendedFormatWithMultiFieldKey() { - - assertThat(out("out-col").insertDocuments().in("database-2") // - .uniqueKeyOf(Arrays.asList("field-1", "field-2")) // - .toDocument(Aggregation.DEFAULT_CONTEXT)).containsEntry("$out.to", "out-col") // - .containsEntry("$out.mode", "insertDocuments") // - .containsEntry("$out.db", "database-2") // - .containsEntry("$out.uniqueKey", new Document("field-1", 1).append("field-2", 1)); - } - - @Test // DATAMONGO-2259 - public void shouldErrorOnExtendedFormatWithoutMode() { + @Test // DATAMONGO-2259, GH-4969 + void shouldRenderDocument() { - assertThatThrownBy(() -> out("out-col").in("database-2").toDocument(Aggregation.DEFAULT_CONTEXT)) - .isInstanceOf(IllegalStateException.class); + assertThat(out("out-col").in("database-2").toDocument(Aggregation.DEFAULT_CONTEXT)) + .containsEntry("$out.coll", "out-col") // + .containsEntry("$out.db", "database-2"); } }