Browse Source
With the introduction of AggregationStage we move the API closer to the MongoDB terminology removing kognitive overhead. Also the change allows us to switch back and forth with the default implementations of toDocument and toDocuments which let's us remove the deprecation warnings having dedicated interfaces that indicate what to implement in order to comply with the usage pattern.issue/4306
16 changed files with 467 additions and 80 deletions
@ -0,0 +1,45 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2023 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 |
||||||
|
* |
||||||
|
* https://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.aggregation; |
||||||
|
|
||||||
|
import org.bson.Document; |
||||||
|
|
||||||
|
/** |
||||||
|
* Abstraction for a single |
||||||
|
* <a href="https://www.mongodb.com/docs/manual/reference/operator/aggregation-pipeline/#stages">Aggregation Pipeline |
||||||
|
* Stage</a> to be used within an {@link AggregationPipeline}. |
||||||
|
* <p> |
||||||
|
* An {@link AggregationStage} may operate upon domain specific types but will render to a ready to use store native |
||||||
|
* representation within a given {@link AggregationOperationContext context}. The most straight forward way of writing a |
||||||
|
* custom {@link AggregationStage} is just returning the raw document. |
||||||
|
* |
||||||
|
* <pre class="code"> |
||||||
|
* AggregationStage stage = (ctx) -> Document.parse("{ $sort : { borough : 1 } }"); |
||||||
|
* </pre> |
||||||
|
* |
||||||
|
* @author Christoph Strobl |
||||||
|
* @since 4.1 |
||||||
|
*/ |
||||||
|
public interface AggregationStage { |
||||||
|
|
||||||
|
/** |
||||||
|
* Turns the {@link AggregationStage} into a {@link Document} by using the given {@link AggregationOperationContext}. |
||||||
|
* |
||||||
|
* @param context the {@link AggregationOperationContext} to operate within. Must not be {@literal null}. |
||||||
|
* @return the ready to use {@link Document} representing the stage. |
||||||
|
*/ |
||||||
|
Document toDocument(AggregationOperationContext context); |
||||||
|
} |
||||||
@ -0,0 +1,103 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2023 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 |
||||||
|
* |
||||||
|
* https://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.aggregation; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
import java.util.Map.Entry; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
import org.bson.Document; |
||||||
|
import org.springframework.util.LinkedMultiValueMap; |
||||||
|
import org.springframework.util.MultiValueMap; |
||||||
|
|
||||||
|
/** |
||||||
|
* An {@link AggregationStage} that may consist of a main operation and potential follow up stages for eg. {@code $sort} |
||||||
|
* or {@code $limit}. |
||||||
|
* <p> |
||||||
|
* The {@link MultiOperationAggregationStage} may operate upon domain specific types but will render to the store native |
||||||
|
* representation within a given {@link AggregationOperationContext context}. |
||||||
|
* <p> |
||||||
|
* {@link #toDocument(AggregationOperationContext)} will render a synthetic {@link Document} that contains the ordered |
||||||
|
* stages. The list returned from {@link #toPipelineStages(AggregationOperationContext)} |
||||||
|
* |
||||||
|
* <pre class="code"> |
||||||
|
* [ |
||||||
|
* { $match: { $text: { $search: "operating" } } }, |
||||||
|
* { $sort: { score: { $meta: "textScore" }, posts: -1 } } |
||||||
|
* ] |
||||||
|
* </pre> |
||||||
|
* |
||||||
|
* will be represented as |
||||||
|
* |
||||||
|
* <pre class="code"> |
||||||
|
* { |
||||||
|
* $match: { $text: { $search: "operating" } }, |
||||||
|
* $sort: { score: { $meta: "textScore" }, posts: -1 } |
||||||
|
* } |
||||||
|
* </pre> |
||||||
|
* |
||||||
|
* In case stages appear multiple times the order no longer can be guaranteed when calling |
||||||
|
* {@link #toDocument(AggregationOperationContext)}, so consumers of the API should rely on |
||||||
|
* {@link #toPipelineStages(AggregationOperationContext)}. Nevertheless, by default the values will be collected into a |
||||||
|
* list rendering to |
||||||
|
* |
||||||
|
* <pre class="code"> |
||||||
|
* { |
||||||
|
* $match: [{ $text: { $search: "operating" } }, { $text: ... }], |
||||||
|
* $sort: { score: { $meta: "textScore" }, posts: -1 } |
||||||
|
* } |
||||||
|
* </pre> |
||||||
|
* |
||||||
|
* @author Christoph Strobl |
||||||
|
* @since 4.1 |
||||||
|
*/ |
||||||
|
public interface MultiOperationAggregationStage extends AggregationStage { |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a synthetic {@link Document stage} that contains the {@link #toPipelineStages(AggregationOperationContext) |
||||||
|
* actual stages} by folding them into a single {@link Document}. In case of colliding entries, those used multiple |
||||||
|
* times thus having the same key, the entries will be held as a list for the given operator. |
||||||
|
* |
||||||
|
* @param context the {@link AggregationOperationContext} to operate within. Must not be {@literal null}. |
||||||
|
* @return never {@literal null}. |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
default Document toDocument(AggregationOperationContext context) { |
||||||
|
|
||||||
|
List<Document> documents = toPipelineStages(context); |
||||||
|
if (documents.size() == 1) { |
||||||
|
return documents.get(0); |
||||||
|
} |
||||||
|
|
||||||
|
MultiValueMap<String, Document> stages = new LinkedMultiValueMap<>(documents.size()); |
||||||
|
for (Document current : documents) { |
||||||
|
String key = current.keySet().iterator().next(); |
||||||
|
stages.add(key, current.get(key, Document.class)); |
||||||
|
} |
||||||
|
return new Document(stages.entrySet().stream() |
||||||
|
.collect(Collectors.toMap(Entry::getKey, v -> v.getValue().size() == 1 ? v.getValue().get(0) : v.getValue()))); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Turns the {@link MultiOperationAggregationStage} into list of {@link Document stages} by using the given |
||||||
|
* {@link AggregationOperationContext}. This allows an {@link AggregationStage} to add follow up stages for eg. |
||||||
|
* {@code $sort} or {@code $limit}. |
||||||
|
* |
||||||
|
* @param context the {@link AggregationOperationContext} to operate within. Must not be {@literal null}. |
||||||
|
* @return the pipeline stages to run through. Never {@literal null}. |
||||||
|
*/ |
||||||
|
List<Document> toPipelineStages(AggregationOperationContext context); |
||||||
|
} |
||||||
@ -0,0 +1,68 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2023 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 |
||||||
|
* |
||||||
|
* https://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.aggregation; |
||||||
|
|
||||||
|
import static org.springframework.data.mongodb.test.util.Assertions.*; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import org.bson.Document; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Christoph Strobl |
||||||
|
*/ |
||||||
|
class MultiOperationAggregationStageUnitTests { |
||||||
|
|
||||||
|
@Test // GH-4306
|
||||||
|
void toDocumentRendersSingleOperation() { |
||||||
|
|
||||||
|
MultiOperationAggregationStage stage = (ctx) -> List.of(Document.parse("{ $text: { $search: 'operating' } }")); |
||||||
|
|
||||||
|
assertThat(stage.toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo("{ $text: { $search: 'operating' } }"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // GH-4306
|
||||||
|
void toDocumentRendersMultiOperation() { |
||||||
|
|
||||||
|
MultiOperationAggregationStage stage = (ctx) -> List.of(Document.parse("{ $text: { $search: 'operating' } }"), |
||||||
|
Document.parse("{ $sort: { score: { $meta: 'textScore' } } }")); |
||||||
|
|
||||||
|
assertThat(stage.toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo(""" |
||||||
|
{ |
||||||
|
$text: { $search: 'operating' }, |
||||||
|
$sort: { score: { $meta: 'textScore' } } |
||||||
|
} |
||||||
|
"""); |
||||||
|
} |
||||||
|
|
||||||
|
@Test // GH-4306
|
||||||
|
void toDocumentCollectsDuplicateOperation() { |
||||||
|
|
||||||
|
MultiOperationAggregationStage stage = (ctx) -> List.of(Document.parse("{ $text: { $search: 'operating' } }"), |
||||||
|
Document.parse("{ $sort: { score: { $meta: 'textScore' } } }"), Document.parse("{ $sort: { posts: -1 } }")); |
||||||
|
|
||||||
|
assertThat(stage.toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo(""" |
||||||
|
{ |
||||||
|
$text: { $search: 'operating' }, |
||||||
|
$sort: [ |
||||||
|
{ score: { $meta: 'textScore' } }, |
||||||
|
{ posts: -1 } |
||||||
|
] |
||||||
|
} |
||||||
|
"""); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue