From f00991dc293dceee172b1ece6613dde599a0665d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 16 Jul 2021 09:41:16 +0200 Subject: [PATCH] Polishing. Rename Granularities/Granularity to Granularity and GranularityDefinition to proivide a more natural wording towards using predefined granularities. Validate presence of referenced properties through the TimeSeries annotation. Tweak Javadoc, reformat code, add unit tests. See #3731 Original pull request: #3732. --- .../data/mongodb/core/CollectionOptions.java | 24 ++++---- .../data/mongodb/core/EntityOperations.java | 16 ++++- .../data/mongodb/core/MongoTemplate.java | 58 +++++++++--------- .../mongodb/core/ReactiveMongoTemplate.java | 22 +++---- .../data/mongodb/core/mapping/TimeSeries.java | 17 +++--- .../mongodb/core/timeseries/Granularity.java | 24 +++++++- ...rities.java => GranularityDefinition.java} | 24 +------- .../core/EntityOperationsUnitTests.java | 60 +++++++++++++++++++ .../mongodb/core/MongoTemplateUnitTests.java | 26 +++++--- .../core/ReactiveMongoTemplateUnitTests.java | 24 ++++---- src/main/asciidoc/reference/time-series.adoc | 15 ++--- 11 files changed, 198 insertions(+), 112 deletions(-) rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/timeseries/{Granularities.java => GranularityDefinition.java} (66%) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java index 3e509e54f..edff52bb7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java @@ -20,8 +20,8 @@ import java.util.Optional; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.schema.MongoJsonSchema; -import org.springframework.data.mongodb.core.timeseries.Granularities; import org.springframework.data.mongodb.core.timeseries.Granularity; +import org.springframework.data.mongodb.core.timeseries.GranularityDefinition; import org.springframework.data.mongodb.core.validation.Validator; import org.springframework.data.util.Optionals; import org.springframework.lang.Nullable; @@ -100,7 +100,7 @@ public class CollectionOptions { /** * Quick way to set up {@link CollectionOptions} for a Time Series collection. For more advanced settings use * {@link #timeSeries(TimeSeriesOptions)}. - * + * * @param timeField The name of the property which contains the date in each time series document. Must not be * {@literal null}. * @return new instance of {@link CollectionOptions}. @@ -454,12 +454,13 @@ public class CollectionOptions { private final String timeField; - @Nullable // - private String metaField; + private @Nullable final String metaField; - private Granularity granularity; + private final GranularityDefinition granularity; - private TimeSeriesOptions(String timeField, @Nullable String metaField, Granularity granularity) { + private TimeSeriesOptions(String timeField, @Nullable String metaField, GranularityDefinition granularity) { + + Assert.hasText(timeField, "Time field must not be empty or null!"); this.timeField = timeField; this.metaField = metaField; @@ -475,7 +476,7 @@ public class CollectionOptions { * @return new instance of {@link TimeSeriesOptions}. */ public static TimeSeriesOptions timeSeries(String timeField) { - return new TimeSeriesOptions(timeField, null, Granularities.DEFAULT); + return new TimeSeriesOptions(timeField, null, Granularity.DEFAULT); } /** @@ -492,12 +493,13 @@ public class CollectionOptions { } /** - * Select the {@link Granularity} parameter to define how data in the time series collection is organized. Select - * one that is closest to the time span between incoming measurements. + * Select the {@link GranularityDefinition} parameter to define how data in the time series collection is organized. + * Select one that is closest to the time span between incoming measurements. * * @return new instance of {@link TimeSeriesOptions}. + * @see Granularity */ - public TimeSeriesOptions granularity(Granularity granularity) { + public TimeSeriesOptions granularity(GranularityDefinition granularity) { return new TimeSeriesOptions(timeField, metaField, granularity); } @@ -520,7 +522,7 @@ public class CollectionOptions { /** * @return never {@literal null}. */ - public Granularity getGranularity() { + public GranularityDefinition getGranularity() { return granularity; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java index 9fb8836e1..3bba17aae 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java @@ -38,7 +38,7 @@ import org.springframework.data.mongodb.core.mapping.TimeSeries; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.mongodb.core.timeseries.Granularities; +import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -898,11 +898,23 @@ class EntityOperations { if (entity.isAnnotationPresent(TimeSeries.class)) { TimeSeries timeSeries = entity.getRequiredAnnotation(TimeSeries.class); + + if (entity.getPersistentProperty(timeSeries.timeField()) == null) { + throw new MappingException(String.format("Time series field '%s' does not exist in type %s", + timeSeries.timeField(), entity.getName())); + } + TimeSeriesOptions options = TimeSeriesOptions.timeSeries(timeSeries.timeField()); if (StringUtils.hasText(timeSeries.metaField())) { + + if (entity.getPersistentProperty(timeSeries.metaField()) == null) { + throw new MappingException( + String.format("Meta field '%s' does not exist in type %s", timeSeries.metaField(), entity.getName())); + } + options = options.metaField(timeSeries.metaField()); } - if (!Granularities.DEFAULT.equals(timeSeries.granularity())) { + if (!Granularity.DEFAULT.equals(timeSeries.granularity())) { options = options.granularity(timeSeries.granularity()); } collectionOptions = collectionOptions.timeSeries(options); 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 c833e511b..fb0780c5c 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 @@ -99,7 +99,7 @@ import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter; -import org.springframework.data.mongodb.core.timeseries.Granularities; +import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.core.validation.Validator; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; @@ -2436,14 +2436,15 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, co.validationOptions(options); } - if(collectionOptions.containsKey("timeseries")) { + if (collectionOptions.containsKey("timeseries")) { Document timeSeries = collectionOptions.get("timeseries", Document.class); - com.mongodb.client.model.TimeSeriesOptions options = new com.mongodb.client.model.TimeSeriesOptions(timeSeries.getString("timeField")); - if(timeSeries.containsKey("metaField")) { + com.mongodb.client.model.TimeSeriesOptions options = new com.mongodb.client.model.TimeSeriesOptions( + timeSeries.getString("timeField")); + if (timeSeries.containsKey("metaField")) { options.metaField(timeSeries.getString("metaField")); } - if(timeSeries.containsKey("granularity")) { + if (timeSeries.containsKey("granularity")) { options.granularity(TimeSeriesGranularity.valueOf(timeSeries.getString("granularity").toUpperCase())); } co.timeSeriesOptions(options); @@ -2604,17 +2605,18 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, collectionOptions.getValidationOptions().ifPresent(it -> it.getValidator() // .ifPresent(val -> doc.put("validator", getMappedValidator(val, targetType)))); - collectionOptions.getTimeSeriesOptions().map(operations.forType(targetType)::mapTimeSeriesOptions).ifPresent(it -> { + collectionOptions.getTimeSeriesOptions().map(operations.forType(targetType)::mapTimeSeriesOptions) + .ifPresent(it -> { - Document timeseries = new Document("timeField", it.getTimeField()); - if(StringUtils.hasText(it.getMetaField())) { - timeseries.append("metaField", it.getMetaField()); - } - if(!Granularities.DEFAULT.equals(it.getGranularity())) { - timeseries.append("granularity", it.getGranularity().name().toLowerCase()); - } - doc.put("timeseries", timeseries); - }); + Document timeseries = new Document("timeField", it.getTimeField()); + if (StringUtils.hasText(it.getMetaField())) { + timeseries.append("metaField", it.getMetaField()); + } + if (!Granularity.DEFAULT.equals(it.getGranularity())) { + timeseries.append("granularity", it.getGranularity().name().toLowerCase()); + } + doc.put("timeseries", timeseries); + }); } return doc; @@ -2849,9 +2851,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, .initiateFind(getAndPrepareCollection(doGetDatabase(), collectionName), collectionCallback::doInCollection) .iterator()) { - while (cursor.hasNext()) { - callbackHandler.processDocument(cursor.next()); - } + while (cursor.hasNext()) { + callbackHandler.processDocument(cursor.next()); + } } catch (RuntimeException e) { throw potentiallyConvertRuntimeException(e, exceptionTranslator); } @@ -3175,17 +3177,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, public T doWith(Document document) { - maybeEmitEvent(new AfterLoadEvent<>(document, type, collectionName)); - T entity = reader.read(type, document); + maybeEmitEvent(new AfterLoadEvent<>(document, type, collectionName)); + T entity = reader.read(type, document); - if (entity == null) { - throw new MappingException(String.format("EntityReader %s returned null", reader)); - } + if (entity == null) { + throw new MappingException(String.format("EntityReader %s returned null", reader)); + } - maybeEmitEvent(new AfterConvertEvent<>(document, entity, collectionName)); - entity = maybeCallAfterConvert(entity, document, collectionName); + maybeEmitEvent(new AfterConvertEvent<>(document, entity, collectionName)); + entity = maybeCallAfterConvert(entity, document, collectionName); - return entity; + return entity; } } @@ -3237,8 +3239,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Object result = targetType.isInterface() ? projectionFactory.createProjection(targetType, entity) : entity; - maybeEmitEvent(new AfterConvertEvent<>(document, result, collectionName)); - return (T) maybeCallAfterConvert(result, document, collectionName); + maybeEmitEvent(new AfterConvertEvent<>(document, result, collectionName)); + return (T) maybeCallAfterConvert(result, document, collectionName); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 2403e9a39..82a3d1226 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -17,7 +17,6 @@ package org.springframework.data.mongodb.core; import static org.springframework.data.mongodb.core.query.SerializationUtils.*; -import org.springframework.data.mongodb.core.timeseries.Granularities; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -111,6 +110,7 @@ import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter; +import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.core.validation.Validator; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; @@ -975,7 +975,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return doAggregate(aggregation, collectionName, null, outputType); } - protected Flux doAggregate(Aggregation aggregation, String collectionName, @Nullable Class inputType, Class outputType) { + protected Flux doAggregate(Aggregation aggregation, String collectionName, @Nullable Class inputType, + Class outputType) { Assert.notNull(aggregation, "Aggregation pipeline must not be null!"); Assert.hasText(collectionName, "Collection name must not be null or empty!"); @@ -987,19 +988,18 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati AggregationDefinition ctx = queryOperations.createAggregation(aggregation, inputType); if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Streaming aggregation: {} in collection {}", serializeToJsonSafely(ctx.getAggregationPipeline()), collectionName); + LOGGER.debug("Streaming aggregation: {} in collection {}", serializeToJsonSafely(ctx.getAggregationPipeline()), + collectionName); } ReadDocumentCallback readCallback = new ReadDocumentCallback<>(mongoConverter, outputType, collectionName); - return execute(collectionName, - collection -> aggregateAndMap(collection, ctx.getAggregationPipeline(), ctx.isOutOrMerge(), options, - readCallback, - ctx.getInputType())); + return execute(collectionName, collection -> aggregateAndMap(collection, ctx.getAggregationPipeline(), + ctx.isOutOrMerge(), options, readCallback, ctx.getInputType())); } private Flux aggregateAndMap(MongoCollection collection, List pipeline, - boolean isOutOrMerge, - AggregationOptions options, ReadDocumentCallback readCallback, @Nullable Class inputType) { + boolean isOutOrMerge, AggregationOptions options, ReadDocumentCallback readCallback, + @Nullable Class inputType) { AggregatePublisher cursor = collection.aggregate(pipeline, Document.class) .allowDiskUse(options.isAllowDiskUse()); @@ -2510,10 +2510,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati TimeSeriesOptions options = new TimeSeriesOptions(it.getTimeField()); - if(StringUtils.hasText(it.getMetaField())) { + if (StringUtils.hasText(it.getMetaField())) { options.metaField(it.getMetaField()); } - if(!Granularities.DEFAULT.equals(it.getGranularity())) { + if (!Granularity.DEFAULT.equals(it.getGranularity())) { options.granularity(TimeSeriesGranularity.valueOf(it.getGranularity().name().toUpperCase())); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/TimeSeries.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/TimeSeries.java index 8a5fe255e..d3f694f53 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/TimeSeries.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/TimeSeries.java @@ -22,7 +22,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.core.annotation.AliasFor; -import org.springframework.data.mongodb.core.timeseries.Granularities; +import org.springframework.data.mongodb.core.timeseries.Granularity; /** * Identifies a domain object to be persisted to a MongoDB Time Series collection. @@ -50,8 +50,9 @@ public @interface TimeSeries { String collection() default ""; /** - * The name of the property which contains the date in each time series document.
- * {@link Field#name() Annotated fieldnames} will be considered during the mapping process. + * Name of the property which contains the date in each time series document.
+ * Translation of property names to {@link Field#name() annotated fieldnames} will be considered during the mapping + * process. * * @return never {@literal null}. */ @@ -60,19 +61,19 @@ public @interface TimeSeries { /** * The name of the field which contains metadata in each time series document. Should not be the {@literal id} nor * {@link #timeField()} nor point to an {@literal array} or {@link java.util.Collection}.
- * {@link Field#name() Annotated fieldnames} will be considered during the mapping process. + * Translation of property names to {@link Field#name() annotated fieldnames} will be considered during the mapping + * process. * * @return empty {@link String} by default. */ String metaField() default ""; /** - * Select the {@link Granularities granularity} parameter to define how data in the time series collection is - * organized. + * Select the {@link Granularity granularity} parameter to define how data in the time series collection is organized. * - * @return {@link Granularities#DEFAULT server default} by default. + * @return {@link Granularity#DEFAULT server default} by default. */ - Granularities granularity() default Granularities.DEFAULT; + Granularity granularity() default Granularity.DEFAULT; /** * Defines the collation to apply when executing a query or creating indexes. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/timeseries/Granularity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/timeseries/Granularity.java index c8fe496ad..30ae007fc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/timeseries/Granularity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/timeseries/Granularity.java @@ -16,12 +16,30 @@ package org.springframework.data.mongodb.core.timeseries; /** - * The Granularity of time series data that is closest to the time span between incoming measurements. + * {@link GranularityDefinition Granularities} available for Time Series data. * * @author Christoph Strobl * @since 3.3 */ -public interface Granularity { +public enum Granularity implements GranularityDefinition { - String name(); + /** + * Server default value to indicate no explicit value should be sent. + */ + DEFAULT, + + /** + * High frequency ingestion. + */ + SECONDS, + + /** + * Medium frequency ingestion. + */ + MINUTES, + + /** + * Low frequency ingestion. + */ + HOURS } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/timeseries/Granularities.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/timeseries/GranularityDefinition.java similarity index 66% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/timeseries/Granularities.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/timeseries/GranularityDefinition.java index f4cac5232..06f77cb59 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/timeseries/Granularities.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/timeseries/GranularityDefinition.java @@ -16,30 +16,12 @@ package org.springframework.data.mongodb.core.timeseries; /** - * {@link Granularity Granularities} available for Time Series data. + * The Granularity of time series data that is closest to the time span between incoming measurements. * * @author Christoph Strobl * @since 3.3 */ -public enum Granularities implements Granularity { +public interface GranularityDefinition { - /** - * Server default value to indicate no explicit value should be sent. - */ - DEFAULT, - - /** - * High frequency ingestion. - */ - SECONDS, - - /** - * Medium frequency ingestion. - */ - MINUTES, - - /** - * Low frequency ingestion. - */ - HOURS + String name(); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java new file mode 100644 index 000000000..901ac1f0d --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2021 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; + +import static org.assertj.core.api.Assertions.*; + +import java.time.Instant; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.mapping.MappingException; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.TimeSeries; + +/** + * Unit tests for {@link EntityOperations}. + * + * @author Mark Paluch + */ +class EntityOperationsUnitTests { + + EntityOperations operations = new EntityOperations(new MongoMappingContext()); + + @Test // GH-3731 + void shouldReportInvalidTimeField() { + assertThatExceptionOfType(MappingException.class) + .isThrownBy(() -> operations.forType(InvalidTimeField.class).getCollectionOptions()) + .withMessageContaining("Time series field 'foo' does not exist"); + } + + @Test // GH-3731 + void shouldReportInvalidMetaField() { + assertThatExceptionOfType(MappingException.class) + .isThrownBy(() -> operations.forType(InvalidMetaField.class).getCollectionOptions()) + .withMessageContaining("Meta field 'foo' does not exist"); + } + + @TimeSeries(timeField = "foo") + static class InvalidTimeField { + + } + + @TimeSeries(timeField = "time", metaField = "foo") + static class InvalidMetaField { + Instant time; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index cc215c956..147d2e49c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -19,7 +19,6 @@ import static org.mockito.Mockito.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; import static org.springframework.data.mongodb.test.util.Assertions.*; -import com.mongodb.client.model.*; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -101,7 +100,7 @@ import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; -import org.springframework.data.mongodb.core.timeseries.Granularities; +import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.CollectionUtils; @@ -121,6 +120,16 @@ import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.CountOptions; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.DeleteOptions; +import com.mongodb.client.model.FindOneAndDeleteOptions; +import com.mongodb.client.model.FindOneAndReplaceOptions; +import com.mongodb.client.model.FindOneAndUpdateOptions; +import com.mongodb.client.model.MapReduceAction; +import com.mongodb.client.model.ReplaceOptions; +import com.mongodb.client.model.TimeSeriesGranularity; +import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; @@ -1982,7 +1991,8 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { ArgumentCaptor filter = ArgumentCaptor.forClass(Bson.class); verify(collection).replaceOne(filter.capture(), any(), any()); - assertThat(filter.getValue()).isEqualTo(new Document("_id", "id-1").append("value", "v1").append("nested.custom-named-field", "cname")); + assertThat(filter.getValue()) + .isEqualTo(new Document("_id", "id-1").append("value", "v1").append("nested.custom-named-field", "cname")); } @Test // DATAMONGO-2341 @@ -2272,7 +2282,8 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { verify(db).createCollection(any(), options.capture()); assertThat(options.getValue().getTimeSeriesOptions().toString()) - .isEqualTo(new com.mongodb.client.model.TimeSeriesOptions("time_stamp").metaField("meta").granularity(TimeSeriesGranularity.HOURS).toString()); + .isEqualTo(new com.mongodb.client.model.TimeSeriesOptions("time_stamp").metaField("meta") + .granularity(TimeSeriesGranularity.HOURS).toString()); } class AutogenerateableId { @@ -2370,7 +2381,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @Field("firstname") String name; } - @Sharded(shardKey = {"value", "nested.customName"}) + @Sharded(shardKey = { "value", "nested.customName" }) static class WithShardKeyPointingToNested { String id; String value; @@ -2384,13 +2395,12 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { Instant timestamp; } - @TimeSeries(timeField = "timestamp", metaField = "meta", granularity = Granularities.HOURS) + @TimeSeries(timeField = "timestamp", metaField = "meta", granularity = Granularity.HOURS) static class TimeSeriesType { String id; - @Field("time_stamp") - Instant timestamp; + @Field("time_stamp") Instant timestamp; Object meta; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java index 17fde7ec3..10e4f1cfc 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java @@ -20,15 +20,9 @@ import static org.mockito.Mockito.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; import static org.springframework.data.mongodb.test.util.Assertions.assertThat; -import com.mongodb.client.model.TimeSeriesGranularity; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.springframework.data.mongodb.core.MongoTemplateUnitTests.TimeSeriesType; -import org.springframework.data.mongodb.core.MongoTemplateUnitTests.TimeSeriesTypeWithDefaults; -import org.springframework.data.mongodb.core.convert.MongoCustomConversions; -import org.springframework.data.mongodb.core.mapping.TimeSeries; -import org.springframework.data.mongodb.core.timeseries.Granularities; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -77,9 +71,11 @@ import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Sw import org.springframework.data.mongodb.core.aggregation.Fields; import org.springframework.data.mongodb.core.aggregation.SetOperation; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.TimeSeries; import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener; import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent; import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterConvertCallback; @@ -93,6 +89,7 @@ import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.CollectionUtils; @@ -106,6 +103,7 @@ import com.mongodb.client.model.FindOneAndDeleteOptions; import com.mongodb.client.model.FindOneAndReplaceOptions; import com.mongodb.client.model.FindOneAndUpdateOptions; import com.mongodb.client.model.ReplaceOptions; +import com.mongodb.client.model.TimeSeriesGranularity; import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.InsertManyResult; @@ -951,7 +949,8 @@ public class ReactiveMongoTemplateUnitTests { @Test // DATAMONGO-2344, DATAMONGO-2572 void allowSecondaryReadsQueryOptionShouldApplyPrimaryPreferredReadPreferenceForFindDistinct() { - template.findDistinct(new Query().allowSecondaryReads(), "name", AutogenerateableId.class, String.class).subscribe(); + template.findDistinct(new Query().allowSecondaryReads(), "name", AutogenerateableId.class, String.class) + .subscribe(); verify(collection).withReadPreference(eq(ReadPreference.primaryPreferred())); } @@ -1428,8 +1427,7 @@ public class ReactiveMongoTemplateUnitTests { Publisher publisher = Mono.just("data"); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> template.insert(publisher)); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> template.insert(publisher)); } @Test // GH-3731 @@ -1453,7 +1451,8 @@ public class ReactiveMongoTemplateUnitTests { verify(db).createCollection(any(), options.capture()); assertThat(options.getValue().getTimeSeriesOptions().toString()) - .isEqualTo(new com.mongodb.client.model.TimeSeriesOptions("time_stamp").metaField("meta").granularity(TimeSeriesGranularity.HOURS).toString()); + .isEqualTo(new com.mongodb.client.model.TimeSeriesOptions("time_stamp").metaField("meta") + .granularity(TimeSeriesGranularity.HOURS).toString()); } private void stubFindSubscribe(Document document) { @@ -1520,13 +1519,12 @@ public class ReactiveMongoTemplateUnitTests { Instant timestamp; } - @TimeSeries(timeField = "timestamp", metaField = "meta", granularity = Granularities.HOURS) + @TimeSeries(timeField = "timestamp", metaField = "meta", granularity = Granularity.HOURS) static class TimeSeriesType { String id; - @Field("time_stamp") - Instant timestamp; + @Field("time_stamp") Instant timestamp; Object meta; } diff --git a/src/main/asciidoc/reference/time-series.adoc b/src/main/asciidoc/reference/time-series.adoc index ac36e4026..54601a8ed 100644 --- a/src/main/asciidoc/reference/time-series.adoc +++ b/src/main/asciidoc/reference/time-series.adoc @@ -1,13 +1,14 @@ [[time-series]] == Time Series -MongoDB 5.0 introduced https://docs.mongodb.com/manual/core/timeseries-collections/[Time Series] collections optimized to efficiently store sequences of measurements. -Those collections need to be actively created before inserting any data. This can be done by manually executing the command, defining time series collection options or extracting options from a `@TimeSeries` annotation as shown in the examples below. +MongoDB 5.0 introduced https://docs.mongodb.com/manual/core/timeseries-collections/[Time Series] collections that are optimized to efficiently store documents over time such as measurements or events. +Those collections need to be created as such before inserting any data. +Collections can be created by either running the `createCollection` command, defining time series collection options or extracting options from a `@TimeSeries` annotation as shown in the examples below. .Create a Time Series Collection ==== .Create a Time Series via the MongoDB Driver -[code, java] +[code,java] ---- template.execute(db -> { @@ -19,14 +20,14 @@ template.execute(db -> { }); ---- -.Create a Time Series Collection with CollectionOptions -[code, java] +.Create a Time Series Collection with `CollectionOptions` +[code,java] ---- template.createCollection("weather", CollectionOptions.timeSeries("timestamp")); ---- .Create a Time Series Collection derived from an Annotation -[code, java] +[code,java] ---- @TimeSeries(collection="weather", timeField = "timestamp") public class Measurement { @@ -41,5 +42,5 @@ template.createCollection(Measurement.class); ==== The snippets above can easily be transferred to the reactive API offering the very same methods. -Just make sure to _subscribe_. +Make sure to properly _subscribe_ to the returned publishers.