Skip to content

Commit bacbd71

Browse files
christophstroblmp911de
authored andcommitted
Add support for creating Time Series collection.
Introduce time series to CollectionOptions and add dedicated TimeSeries annotation to derive values from. Closes #3731 Original pull request: #3732.
1 parent f38f6d6 commit bacbd71

File tree

12 files changed

+547
-20
lines changed

12 files changed

+547
-20
lines changed

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java

+135-9
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717

1818
import java.util.Optional;
1919

20+
import org.springframework.data.mongodb.core.mapping.Field;
2021
import org.springframework.data.mongodb.core.query.Collation;
2122
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
23+
import org.springframework.data.mongodb.core.timeseries.Granularities;
24+
import org.springframework.data.mongodb.core.timeseries.Granularity;
2225
import org.springframework.data.mongodb.core.validation.Validator;
2326
import org.springframework.data.util.Optionals;
2427
import org.springframework.lang.Nullable;
@@ -42,6 +45,7 @@ public class CollectionOptions {
4245
private @Nullable Boolean capped;
4346
private @Nullable Collation collation;
4447
private ValidationOptions validationOptions;
48+
private @Nullable TimeSeriesOptions timeSeriesOptions;
4549

4650
/**
4751
* Constructs a new <code>CollectionOptions</code> instance.
@@ -54,17 +58,19 @@ public class CollectionOptions {
5458
*/
5559
@Deprecated
5660
public CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped) {
57-
this(size, maxDocuments, capped, null, ValidationOptions.none());
61+
this(size, maxDocuments, capped, null, ValidationOptions.none(), null);
5862
}
5963

6064
private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped,
61-
@Nullable Collation collation, ValidationOptions validationOptions) {
65+
@Nullable Collation collation, ValidationOptions validationOptions,
66+
@Nullable TimeSeriesOptions timeSeriesOptions) {
6267

6368
this.maxDocuments = maxDocuments;
6469
this.size = size;
6570
this.capped = capped;
6671
this.collation = collation;
6772
this.validationOptions = validationOptions;
73+
this.timeSeriesOptions = timeSeriesOptions;
6874
}
6975

7076
/**
@@ -78,7 +84,7 @@ public static CollectionOptions just(Collation collation) {
7884

7985
Assert.notNull(collation, "Collation must not be null!");
8086

81-
return new CollectionOptions(null, null, null, collation, ValidationOptions.none());
87+
return new CollectionOptions(null, null, null, collation, ValidationOptions.none(), null);
8288
}
8389

8490
/**
@@ -88,7 +94,21 @@ public static CollectionOptions just(Collation collation) {
8894
* @since 2.0
8995
*/
9096
public static CollectionOptions empty() {
91-
return new CollectionOptions(null, null, null, null, ValidationOptions.none());
97+
return new CollectionOptions(null, null, null, null, ValidationOptions.none(), null);
98+
}
99+
100+
/**
101+
* Quick way to set up {@link CollectionOptions} for a Time Series collection. For more advanced settings use
102+
* {@link #timeSeries(TimeSeriesOptions)}.
103+
*
104+
* @param timeField The name of the property which contains the date in each time series document. Must not be
105+
* {@literal null}.
106+
* @return new instance of {@link CollectionOptions}.
107+
* @see #timeSeries(TimeSeriesOptions)
108+
* @since 3.3
109+
*/
110+
public static CollectionOptions timeSeries(String timeField) {
111+
return empty().timeSeries(TimeSeriesOptions.timeSeries(timeField));
92112
}
93113

94114
/**
@@ -99,7 +119,7 @@ public static CollectionOptions empty() {
99119
* @since 2.0
100120
*/
101121
public CollectionOptions capped() {
102-
return new CollectionOptions(size, maxDocuments, true, collation, validationOptions);
122+
return new CollectionOptions(size, maxDocuments, true, collation, validationOptions, null);
103123
}
104124

105125
/**
@@ -110,7 +130,7 @@ public CollectionOptions capped() {
110130
* @since 2.0
111131
*/
112132
public CollectionOptions maxDocuments(long maxDocuments) {
113-
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions);
133+
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions);
114134
}
115135

116136
/**
@@ -121,7 +141,7 @@ public CollectionOptions maxDocuments(long maxDocuments) {
121141
* @since 2.0
122142
*/
123143
public CollectionOptions size(long size) {
124-
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions);
144+
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions);
125145
}
126146

127147
/**
@@ -132,7 +152,7 @@ public CollectionOptions size(long size) {
132152
* @since 2.0
133153
*/
134154
public CollectionOptions collation(@Nullable Collation collation) {
135-
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions);
155+
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions);
136156
}
137157

138158
/**
@@ -252,7 +272,20 @@ public CollectionOptions schemaValidationAction(ValidationAction validationActio
252272
public CollectionOptions validation(ValidationOptions validationOptions) {
253273

254274
Assert.notNull(validationOptions, "ValidationOptions must not be null!");
255-
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions);
275+
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions);
276+
}
277+
278+
/**
279+
* Create new {@link CollectionOptions} with the given {@link TimeSeriesOptions}.
280+
*
281+
* @param timeSeriesOptions must not be {@literal null}.
282+
* @return new instance of {@link CollectionOptions}.
283+
* @since 3.3
284+
*/
285+
public CollectionOptions timeSeries(TimeSeriesOptions timeSeriesOptions) {
286+
287+
Assert.notNull(timeSeriesOptions, "TimeSeriesOptions must not be null!");
288+
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions);
256289
}
257290

258291
/**
@@ -303,6 +336,16 @@ public Optional<ValidationOptions> getValidationOptions() {
303336
return validationOptions.isEmpty() ? Optional.empty() : Optional.of(validationOptions);
304337
}
305338

339+
/**
340+
* Get the {@link TimeSeriesOptions} if available.
341+
*
342+
* @return {@link Optional#empty()} if not specified.
343+
* @since 3.3
344+
*/
345+
public Optional<TimeSeriesOptions> getTimeSeriesOptions() {
346+
return Optional.ofNullable(timeSeriesOptions);
347+
}
348+
306349
/**
307350
* Encapsulation of ValidationOptions options.
308351
*
@@ -398,4 +441,87 @@ boolean isEmpty() {
398441
return !Optionals.isAnyPresent(getValidator(), getValidationAction(), getValidationLevel());
399442
}
400443
}
444+
445+
/**
446+
* Options applicable to Time Series collections.
447+
*
448+
* @author Christoph Strobl
449+
* @since 3.3
450+
* @see <a href=
451+
* "https://docs.mongodb.com/manual/core/timeseries-collections">https://docs.mongodb.com/manual/core/timeseries-collections</a>
452+
*/
453+
public static class TimeSeriesOptions {
454+
455+
private final String timeField;
456+
457+
@Nullable //
458+
private String metaField;
459+
460+
private Granularity granularity;
461+
462+
private TimeSeriesOptions(String timeField, @Nullable String metaField, Granularity granularity) {
463+
464+
this.timeField = timeField;
465+
this.metaField = metaField;
466+
this.granularity = granularity;
467+
}
468+
469+
/**
470+
* Create a new instance of {@link TimeSeriesOptions} using the given field as its {@literal timeField}. The one,
471+
* that contains the date in each time series document. <br />
472+
* {@link Field#name() Annotated fieldnames} will be considered during the mapping process.
473+
*
474+
* @param timeField must not be {@literal null}.
475+
* @return new instance of {@link TimeSeriesOptions}.
476+
*/
477+
public static TimeSeriesOptions timeSeries(String timeField) {
478+
return new TimeSeriesOptions(timeField, null, Granularities.DEFAULT);
479+
}
480+
481+
/**
482+
* Set the name of the field which contains metadata in each time series document. Should not be the {@literal id}
483+
* nor {@link TimeSeriesOptions#timeSeries(String)} timeField} nor point to an {@literal array} or
484+
* {@link java.util.Collection}. <br />
485+
* {@link Field#name() Annotated fieldnames} will be considered during the mapping process.
486+
*
487+
* @param metaField must not be {@literal null}.
488+
* @return new instance of {@link TimeSeriesOptions}.
489+
*/
490+
public TimeSeriesOptions metaField(String metaField) {
491+
return new TimeSeriesOptions(timeField, metaField, granularity);
492+
}
493+
494+
/**
495+
* Select the {@link Granularity} parameter to define how data in the time series collection is organized. Select
496+
* one that is closest to the time span between incoming measurements.
497+
*
498+
* @return new instance of {@link TimeSeriesOptions}.
499+
*/
500+
public TimeSeriesOptions granularity(Granularity granularity) {
501+
return new TimeSeriesOptions(timeField, metaField, granularity);
502+
}
503+
504+
/**
505+
* @return never {@literal null}.
506+
*/
507+
public String getTimeField() {
508+
return timeField;
509+
}
510+
511+
/**
512+
* @return can be {@literal null}. Might be an {@literal empty} {@link String} as well, so maybe check via
513+
* {@link org.springframework.util.StringUtils#hasText(String)}.
514+
*/
515+
@Nullable
516+
public String getMetaField() {
517+
return metaField;
518+
}
519+
520+
/**
521+
* @return never {@literal null}.
522+
*/
523+
public Granularity getGranularity() {
524+
return granularity;
525+
}
526+
}
401527
}

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java

+72
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,23 @@
2929
import org.springframework.data.mapping.PersistentPropertyAccessor;
3030
import org.springframework.data.mapping.context.MappingContext;
3131
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
32+
import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions;
3233
import org.springframework.data.mongodb.core.convert.MongoWriter;
3334
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
3435
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
3536
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
37+
import org.springframework.data.mongodb.core.mapping.TimeSeries;
3638
import org.springframework.data.mongodb.core.query.Collation;
3739
import org.springframework.data.mongodb.core.query.Criteria;
3840
import org.springframework.data.mongodb.core.query.Query;
41+
import org.springframework.data.mongodb.core.timeseries.Granularities;
3942
import org.springframework.lang.Nullable;
4043
import org.springframework.util.Assert;
4144
import org.springframework.util.ClassUtils;
4245
import org.springframework.util.LinkedMultiValueMap;
4346
import org.springframework.util.MultiValueMap;
4447
import org.springframework.util.ObjectUtils;
48+
import org.springframework.util.StringUtils;
4549

4650
/**
4751
* Common operations performed on an entity in the context of it's mapping metadata.
@@ -778,6 +782,24 @@ interface TypedOperations<T> {
778782
* @return
779783
*/
780784
Optional<Collation> getCollation(Query query);
785+
786+
/**
787+
* Derive the applicable {@link CollectionOptions} for the given type.
788+
*
789+
* @return never {@literal null}.
790+
* @since 3.3
791+
*/
792+
CollectionOptions getCollectionOptions();
793+
794+
/**
795+
* Map the fields of a given {@link TimeSeriesOptions} against the target domain type to consider potentially
796+
* annotated field names.
797+
*
798+
* @param options must not be {@literal null}.
799+
* @return never {@literal null}.
800+
* @since 3.3
801+
*/
802+
TimeSeriesOptions mapTimeSeriesOptions(TimeSeriesOptions options);
781803
}
782804

783805
/**
@@ -817,6 +839,16 @@ public Optional<Collation> getCollation(Query query) {
817839

818840
return query.getCollation();
819841
}
842+
843+
@Override
844+
public CollectionOptions getCollectionOptions() {
845+
return CollectionOptions.empty();
846+
}
847+
848+
@Override
849+
public TimeSeriesOptions mapTimeSeriesOptions(TimeSeriesOptions options) {
850+
return options;
851+
}
820852
}
821853

822854
/**
@@ -854,6 +886,46 @@ public Optional<Collation> getCollation(Query query) {
854886

855887
return Optional.ofNullable(entity.getCollation());
856888
}
889+
890+
@Override
891+
public CollectionOptions getCollectionOptions() {
892+
893+
CollectionOptions collectionOptions = CollectionOptions.empty();
894+
if (entity.hasCollation()) {
895+
collectionOptions = collectionOptions.collation(entity.getCollation());
896+
}
897+
898+
if (entity.isAnnotationPresent(TimeSeries.class)) {
899+
900+
TimeSeries timeSeries = entity.getRequiredAnnotation(TimeSeries.class);
901+
TimeSeriesOptions options = TimeSeriesOptions.timeSeries(timeSeries.timeField());
902+
if (StringUtils.hasText(timeSeries.metaField())) {
903+
options = options.metaField(timeSeries.metaField());
904+
}
905+
if (!Granularities.DEFAULT.equals(timeSeries.granularity())) {
906+
options = options.granularity(timeSeries.granularity());
907+
}
908+
collectionOptions = collectionOptions.timeSeries(options);
909+
}
910+
911+
return collectionOptions;
912+
}
913+
914+
@Override
915+
public TimeSeriesOptions mapTimeSeriesOptions(TimeSeriesOptions source) {
916+
917+
TimeSeriesOptions target = TimeSeriesOptions.timeSeries(mappedNameOrDefault(source.getTimeField()));
918+
919+
if (StringUtils.hasText(source.getMetaField())) {
920+
target = target.metaField(mappedNameOrDefault(source.getMetaField()));
921+
}
922+
return target.granularity(source.getGranularity());
923+
}
924+
925+
private String mappedNameOrDefault(String name) {
926+
MongoPersistentProperty persistentProperty = entity.getPersistentProperty(name);
927+
return persistentProperty != null ? persistentProperty.getFieldName() : name;
928+
}
857929
}
858930

859931
}

0 commit comments

Comments
 (0)