Add support for collection expiration to @TimeSeries.
Closes: #4099 Original Pull Request: #4114
This commit is contained in:
committed by
Christoph Strobl
parent
7fbd4966d7
commit
197998f7cb
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
@@ -38,6 +39,7 @@ import com.mongodb.client.model.ValidationLevel;
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @author Andreas Zink
|
||||
* @author Ben Foster
|
||||
*/
|
||||
public class CollectionOptions {
|
||||
|
||||
@@ -629,13 +631,15 @@ public class CollectionOptions {
|
||||
|
||||
private final GranularityDefinition granularity;
|
||||
|
||||
private TimeSeriesOptions(String timeField, @Nullable String metaField, GranularityDefinition granularity) {
|
||||
private final long expireAfterSeconds;
|
||||
|
||||
private TimeSeriesOptions(String timeField, @Nullable String metaField, GranularityDefinition granularity, long expireAfterSeconds) {
|
||||
Assert.hasText(timeField, "Time field must not be empty or null");
|
||||
|
||||
this.timeField = timeField;
|
||||
this.metaField = metaField;
|
||||
this.granularity = granularity;
|
||||
this.expireAfterSeconds = expireAfterSeconds;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -647,7 +651,7 @@ public class CollectionOptions {
|
||||
* @return new instance of {@link TimeSeriesOptions}.
|
||||
*/
|
||||
public static TimeSeriesOptions timeSeries(String timeField) {
|
||||
return new TimeSeriesOptions(timeField, null, Granularity.DEFAULT);
|
||||
return new TimeSeriesOptions(timeField, null, Granularity.DEFAULT, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -660,7 +664,7 @@ public class CollectionOptions {
|
||||
* @return new instance of {@link TimeSeriesOptions}.
|
||||
*/
|
||||
public TimeSeriesOptions metaField(String metaField) {
|
||||
return new TimeSeriesOptions(timeField, metaField, granularity);
|
||||
return new TimeSeriesOptions(timeField, metaField, granularity, expireAfterSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -671,7 +675,17 @@ public class CollectionOptions {
|
||||
* @see Granularity
|
||||
*/
|
||||
public TimeSeriesOptions granularity(GranularityDefinition granularity) {
|
||||
return new TimeSeriesOptions(timeField, metaField, granularity);
|
||||
return new TimeSeriesOptions(timeField, metaField, granularity, expireAfterSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the expire parameter to define automatic removal of documents older than a specified
|
||||
* duration.
|
||||
*
|
||||
* @return new instance of {@link TimeSeriesOptions}.
|
||||
*/
|
||||
public TimeSeriesOptions expireAfter(Duration timeout) {
|
||||
return new TimeSeriesOptions(timeField, metaField, granularity, timeout.getSeconds());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -697,6 +711,13 @@ public class CollectionOptions {
|
||||
return granularity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal -1} if not specified
|
||||
*/
|
||||
public long getExpireAfterSeconds() {
|
||||
return expireAfterSeconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.bson.BsonNull;
|
||||
import org.bson.Document;
|
||||
@@ -40,10 +42,8 @@ import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper;
|
||||
import org.springframework.data.mongodb.core.convert.MongoWriter;
|
||||
import org.springframework.data.mongodb.core.convert.QueryMapper;
|
||||
import org.springframework.data.mongodb.core.mapping.FieldName;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
|
||||
import org.springframework.data.mongodb.core.mapping.TimeSeries;
|
||||
import org.springframework.data.mongodb.core.index.DurationStyle;
|
||||
import org.springframework.data.mongodb.core.mapping.*;
|
||||
import org.springframework.data.mongodb.core.query.Collation;
|
||||
import org.springframework.data.mongodb.core.query.Criteria;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
@@ -54,7 +54,13 @@ import org.springframework.data.projection.EntityProjection;
|
||||
import org.springframework.data.projection.EntityProjectionIntrospector;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.projection.TargetAware;
|
||||
import org.springframework.data.spel.EvaluationContextProvider;
|
||||
import org.springframework.data.util.Optionals;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ParserContext;
|
||||
import org.springframework.expression.common.LiteralExpression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
@@ -74,6 +80,7 @@ import com.mongodb.client.model.ValidationOptions;
|
||||
* @author Oliver Gierke
|
||||
* @author Mark Paluch
|
||||
* @author Christoph Strobl
|
||||
* @author Ben Foster
|
||||
* @since 2.1
|
||||
* @see MongoTemplate
|
||||
* @see ReactiveMongoTemplate
|
||||
@@ -89,6 +96,8 @@ class EntityOperations {
|
||||
|
||||
private final MongoJsonSchemaMapper schemaMapper;
|
||||
|
||||
private EvaluationContextProvider evaluationContextProvider = EvaluationContextProvider.DEFAULT;
|
||||
|
||||
EntityOperations(MongoConverter converter) {
|
||||
this(converter, new QueryMapper(converter));
|
||||
}
|
||||
@@ -276,7 +285,7 @@ class EntityOperations {
|
||||
MongoPersistentEntity<?> entity = context.getPersistentEntity(entityClass);
|
||||
|
||||
if (entity != null) {
|
||||
return new TypedEntityOperations(entity);
|
||||
return new TypedEntityOperations(entity, evaluationContextProvider);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -354,6 +363,10 @@ class EntityOperations {
|
||||
options.granularity(TimeSeriesGranularity.valueOf(it.getGranularity().name().toUpperCase()));
|
||||
}
|
||||
|
||||
if (it.getExpireAfterSeconds() >= 0) {
|
||||
result.expireAfter(it.getExpireAfterSeconds(), TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
result.timeSeriesOptions(options);
|
||||
});
|
||||
|
||||
@@ -1026,10 +1039,13 @@ class EntityOperations {
|
||||
*/
|
||||
static class TypedEntityOperations<T> implements TypedOperations<T> {
|
||||
|
||||
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
|
||||
private final MongoPersistentEntity<T> entity;
|
||||
private final EvaluationContextProvider evaluationContextProvider;
|
||||
|
||||
protected TypedEntityOperations(MongoPersistentEntity<T> entity) {
|
||||
protected TypedEntityOperations(MongoPersistentEntity<T> entity, EvaluationContextProvider evaluationContextProvider) {
|
||||
this.entity = entity;
|
||||
this.evaluationContextProvider = evaluationContextProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1077,6 +1093,26 @@ class EntityOperations {
|
||||
if (!Granularity.DEFAULT.equals(timeSeries.granularity())) {
|
||||
options = options.granularity(timeSeries.granularity());
|
||||
}
|
||||
|
||||
if (timeSeries.expireAfterSeconds() >= 0) {
|
||||
options = options.expireAfter(Duration.ofSeconds(timeSeries.expireAfterSeconds()));
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(timeSeries.expireAfter())) {
|
||||
|
||||
if (timeSeries.expireAfterSeconds() >= 0) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"@TimeSeries already defines an expiration timeout of %s seconds via TimeSeries#expireAfterSeconds; Please make to use either expireAfterSeconds or expireAfter",
|
||||
timeSeries.expireAfterSeconds()));
|
||||
}
|
||||
|
||||
Duration timeout = computeIndexTimeout(timeSeries.expireAfter(),
|
||||
getEvaluationContextForProperty(entity));
|
||||
if (!timeout.isZero() && !timeout.isNegative()) {
|
||||
options = options.expireAfter(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
collectionOptions = collectionOptions.timeSeries(options);
|
||||
}
|
||||
|
||||
@@ -1091,7 +1127,8 @@ class EntityOperations {
|
||||
if (StringUtils.hasText(source.getMetaField())) {
|
||||
target = target.metaField(mappedNameOrDefault(source.getMetaField()));
|
||||
}
|
||||
return target.granularity(source.getGranularity());
|
||||
return target.granularity(source.getGranularity())
|
||||
.expireAfter(Duration.ofSeconds(source.getExpireAfterSeconds()));
|
||||
}
|
||||
|
||||
private String mappedNameOrDefault(String name) {
|
||||
@@ -1105,4 +1142,90 @@ class EntityOperations {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compute the index timeout value by evaluating a potential
|
||||
* {@link org.springframework.expression.spel.standard.SpelExpression} and parsing the final value.
|
||||
*
|
||||
* @param timeoutValue must not be {@literal null}.
|
||||
* @param evaluationContext must not be {@literal null}.
|
||||
* @return never {@literal null}
|
||||
* @since 2.2
|
||||
* @throws IllegalArgumentException for invalid duration values.
|
||||
*/
|
||||
private static Duration computeIndexTimeout(String timeoutValue, EvaluationContext evaluationContext) {
|
||||
|
||||
Object evaluatedTimeout = evaluate(timeoutValue, evaluationContext);
|
||||
|
||||
if (evaluatedTimeout == null) {
|
||||
return Duration.ZERO;
|
||||
}
|
||||
|
||||
if (evaluatedTimeout instanceof Duration) {
|
||||
return (Duration) evaluatedTimeout;
|
||||
}
|
||||
|
||||
String val = evaluatedTimeout.toString();
|
||||
|
||||
if (val == null) {
|
||||
return Duration.ZERO;
|
||||
}
|
||||
|
||||
return DurationStyle.detectAndParse(val);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Object evaluate(String value, EvaluationContext evaluationContext) {
|
||||
|
||||
Expression expression = PARSER.parseExpression(value, ParserContext.TEMPLATE_EXPRESSION);
|
||||
if (expression instanceof LiteralExpression) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return expression.getValue(evaluationContext, Object.class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the {@link EvaluationContext} for a given {@link PersistentEntity entity} the default one.
|
||||
*
|
||||
* @param persistentEntity can be {@literal null}
|
||||
* @return
|
||||
*/
|
||||
private EvaluationContext getEvaluationContextForProperty(@Nullable PersistentEntity<?, ?> persistentEntity) {
|
||||
|
||||
if (!(persistentEntity instanceof BasicMongoPersistentEntity)) {
|
||||
return getEvaluationContext();
|
||||
}
|
||||
|
||||
EvaluationContext contextFromEntity = ((BasicMongoPersistentEntity<?>) persistentEntity).getEvaluationContext(null);
|
||||
|
||||
if (!EvaluationContextProvider.DEFAULT.equals(contextFromEntity)) {
|
||||
return contextFromEntity;
|
||||
}
|
||||
|
||||
return getEvaluationContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default {@link EvaluationContext}.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
* @since 2.2
|
||||
*/
|
||||
protected EvaluationContext getEvaluationContext() {
|
||||
return evaluationContextProvider.getEvaluationContext(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link EvaluationContextProvider} used for obtaining the {@link EvaluationContext} used to compute
|
||||
* {@link org.springframework.expression.spel.standard.SpelExpression expressions}.
|
||||
*
|
||||
* @param evaluationContextProvider must not be {@literal null}.
|
||||
* @since 2.2
|
||||
*/
|
||||
public void setEvaluationContextProvider(EvaluationContextProvider evaluationContextProvider) {
|
||||
this.evaluationContextProvider = evaluationContextProvider;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Phillip Webb
|
||||
* @since 2.2
|
||||
*/
|
||||
enum DurationStyle {
|
||||
public enum DurationStyle {
|
||||
|
||||
/**
|
||||
* Simple formatting, for example '1s'.
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.springframework.data.mongodb.core.timeseries.Granularity;
|
||||
* Identifies a domain object to be persisted to a MongoDB Time Series collection.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Ben Foster
|
||||
* @since 3.3
|
||||
* @see <a href="https://docs.mongodb.com/manual/core/timeseries-collections">https://docs.mongodb.com/manual/core/timeseries-collections</a>
|
||||
*/
|
||||
@@ -83,4 +84,42 @@ public @interface TimeSeries {
|
||||
@AliasFor(annotation = Document.class, attribute = "collation")
|
||||
String collation() default "";
|
||||
|
||||
/**
|
||||
* Configures the number of seconds after which the document should expire. Defaults to -1 for no expiry.
|
||||
*
|
||||
* @return {@literal -1} by default.
|
||||
* @see <a href=
|
||||
* "https://www.mongodb.com/docs/manual/core/timeseries/timeseries-automatic-removal/#set-up-automatic-removal-for-time-series-collections--ttl-</a>
|
||||
*/
|
||||
int expireAfterSeconds() default -1;
|
||||
|
||||
|
||||
/**
|
||||
* Alternative for {@link #expireAfterSeconds()} to configure the timeout after which the document should expire.
|
||||
* Defaults to an empty {@link String} for no expiry. Accepts numeric values followed by their unit of measure:
|
||||
* <ul>
|
||||
* <li><b>d</b>: Days</li>
|
||||
* <li><b>h</b>: Hours</li>
|
||||
* <li><b>m</b>: Minutes</li>
|
||||
* <li><b>s</b>: Seconds</li>
|
||||
* <li>Alternatively: A Spring {@literal template expression}. The expression can result in a
|
||||
* {@link java.time.Duration} or a valid expiration {@link String} according to the already mentioned
|
||||
* conventions.</li>
|
||||
* </ul>
|
||||
* Supports ISO-8601 style.
|
||||
*
|
||||
* <pre class="code">
|
||||
*
|
||||
* @Indexed(expireAfter = "10s") String expireAfterTenSeconds;
|
||||
*
|
||||
* @Indexed(expireAfter = "1d") String expireAfterOneDay;
|
||||
*
|
||||
* @Indexed(expireAfter = "P2D") String expireAfterTwoDays;
|
||||
*
|
||||
* @Indexed(expireAfter = "#{@mySpringBean.timeout}") String expireAfterTimeoutObtainedFromSpringBean;
|
||||
* </pre>
|
||||
*
|
||||
* @return empty by default.
|
||||
*/
|
||||
String expireAfter() default "";
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ import com.mongodb.client.result.UpdateResult;
|
||||
* @author Roman Puchkovskiy
|
||||
* @author Yadhukrishna S Pai
|
||||
* @author Jakub Zurawa
|
||||
* @author Ben Foster
|
||||
*/
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
|
||||
@@ -2386,6 +2387,82 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
|
||||
.granularity(TimeSeriesGranularity.HOURS).toString());
|
||||
}
|
||||
|
||||
@Test // GH-4099
|
||||
void createCollectionShouldSetUpTimeSeriesWithExpiration() {
|
||||
|
||||
template.createCollection(TimeSeriesTypeWithExpireAfterSeconds.class);
|
||||
|
||||
ArgumentCaptor<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
|
||||
verify(db).createCollection(any(), options.capture());
|
||||
|
||||
assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS))
|
||||
.isEqualTo(60);
|
||||
}
|
||||
|
||||
@Test // GH-4099
|
||||
void createCollectionShouldSetUpTimeSeriesWithExpirationFromString() {
|
||||
|
||||
template.createCollection(ReactiveMongoTemplateUnitTests.TimeSeriesTypeWithExpireAfterAsPlainString.class);
|
||||
|
||||
ArgumentCaptor<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
|
||||
verify(db).createCollection(any(), options.capture());
|
||||
|
||||
assertThat(options.getValue().getExpireAfter(TimeUnit.MINUTES))
|
||||
.isEqualTo(10);
|
||||
}
|
||||
|
||||
@Test // GH-4099
|
||||
void createCollectionShouldSetUpTimeSeriesWithExpirationFromIso8601String() {
|
||||
|
||||
template.createCollection(ReactiveMongoTemplateUnitTests.TimeSeriesTypeWithExpireAfterAsIso8601Style.class);
|
||||
|
||||
ArgumentCaptor<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
|
||||
verify(db).createCollection(any(), options.capture());
|
||||
|
||||
assertThat(options.getValue().getExpireAfter(TimeUnit.DAYS))
|
||||
.isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test // GH-4099
|
||||
void createCollectionShouldSetUpTimeSeriesWithExpirationFromExpression() {
|
||||
|
||||
template.createCollection(ReactiveMongoTemplateUnitTests.TimeSeriesTypeWithExpireAfterAsExpression.class);
|
||||
|
||||
ArgumentCaptor<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
|
||||
verify(db).createCollection(any(), options.capture());
|
||||
|
||||
assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS))
|
||||
.isEqualTo(11);
|
||||
}
|
||||
|
||||
@Test // GH-4099
|
||||
void createCollectionShouldSetUpTimeSeriesWithExpirationFromExpressionReturningDuration() {
|
||||
|
||||
template.createCollection(ReactiveMongoTemplateUnitTests.TimeSeriesTypeWithExpireAfterAsExpressionResultingInDuration.class);
|
||||
|
||||
ArgumentCaptor<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
|
||||
verify(db).createCollection(any(), options.capture());
|
||||
|
||||
assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS))
|
||||
.isEqualTo(100);
|
||||
}
|
||||
|
||||
@Test // GH-4099
|
||||
void createCollectionShouldSetUpTimeSeriesWithInvalidTimeoutExpiration() {
|
||||
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() ->
|
||||
template.createCollection(ReactiveMongoTemplateUnitTests.TimeSeriesTypeWithInvalidExpireAfter.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test // GH-4099
|
||||
void createCollectionShouldSetUpTimeSeriesWithDuplicateTimeoutExpiration() {
|
||||
|
||||
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() ->
|
||||
template.createCollection(ReactiveMongoTemplateUnitTests.TimeSeriesTypeWithDuplicateExpireAfter.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test // GH-3522
|
||||
void usedCountDocumentsForEmptyQueryByDefault() {
|
||||
|
||||
@@ -2705,6 +2782,56 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
|
||||
Object meta;
|
||||
}
|
||||
|
||||
@TimeSeries(timeField = "timestamp", expireAfterSeconds = 60)
|
||||
static class TimeSeriesTypeWithExpireAfterSeconds {
|
||||
|
||||
String id;
|
||||
Instant timestamp;
|
||||
}
|
||||
|
||||
@TimeSeries(timeField = "timestamp", expireAfter = "10m")
|
||||
static class TimeSeriesTypeWithExpireAfterAsPlainString {
|
||||
|
||||
String id;
|
||||
Instant timestamp;
|
||||
}
|
||||
|
||||
@TimeSeries(timeField = "timestamp", expireAfter = "P1D")
|
||||
static class TimeSeriesTypeWithExpireAfterAsIso8601Style {
|
||||
|
||||
String id;
|
||||
Instant timestamp;
|
||||
}
|
||||
|
||||
@TimeSeries(timeField = "timestamp", expireAfter = "#{10 + 1 + 's'}")
|
||||
static class TimeSeriesTypeWithExpireAfterAsExpression {
|
||||
|
||||
String id;
|
||||
Instant timestamp;
|
||||
}
|
||||
|
||||
@TimeSeries(timeField = "timestamp", expireAfter = "#{T(java.time.Duration).ofSeconds(100)}")
|
||||
static class TimeSeriesTypeWithExpireAfterAsExpressionResultingInDuration {
|
||||
|
||||
String id;
|
||||
Instant timestamp;
|
||||
}
|
||||
|
||||
@TimeSeries(timeField = "timestamp", expireAfter = "123ops")
|
||||
static class TimeSeriesTypeWithInvalidExpireAfter {
|
||||
|
||||
String id;
|
||||
Instant timestamp;
|
||||
}
|
||||
|
||||
@TimeSeries(timeField = "timestamp", expireAfter = "1s", expireAfterSeconds = 2)
|
||||
static class TimeSeriesTypeWithDuplicateExpireAfter {
|
||||
|
||||
String id;
|
||||
Instant timestamp;
|
||||
}
|
||||
|
||||
|
||||
static class TypeImplementingIterator implements Iterator {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -132,6 +132,7 @@ import com.mongodb.reactivestreams.client.MongoDatabase;
|
||||
* @author Roman Puchkovskiy
|
||||
* @author Mathieu Ouellet
|
||||
* @author Yadhukrishna S Pai
|
||||
* @author Ben Foster
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
@@ -1739,6 +1740,82 @@ public class ReactiveMongoTemplateUnitTests {
|
||||
verify(collection).withWriteConcern(eq(WriteConcern.UNACKNOWLEDGED));
|
||||
}
|
||||
|
||||
@Test // GH-4099
|
||||
void createCollectionShouldSetUpTimeSeriesWithExpirationSeconds() {
|
||||
|
||||
template.createCollection(TimeSeriesTypeWithExpireAfterSeconds.class).subscribe();
|
||||
|
||||
ArgumentCaptor<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
|
||||
verify(db).createCollection(any(), options.capture());
|
||||
|
||||
assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS))
|
||||
.isEqualTo(60);
|
||||
}
|
||||
|
||||
@Test // GH-4099
|
||||
void createCollectionShouldSetUpTimeSeriesWithExpirationFromString() {
|
||||
|
||||
template.createCollection(TimeSeriesTypeWithExpireAfterAsPlainString.class).subscribe();
|
||||
|
||||
ArgumentCaptor<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
|
||||
verify(db).createCollection(any(), options.capture());
|
||||
|
||||
assertThat(options.getValue().getExpireAfter(TimeUnit.MINUTES))
|
||||
.isEqualTo(10);
|
||||
}
|
||||
|
||||
@Test // GH-4099
|
||||
void createCollectionShouldSetUpTimeSeriesWithExpirationFromIso8601String() {
|
||||
|
||||
template.createCollection(TimeSeriesTypeWithExpireAfterAsIso8601Style.class).subscribe();
|
||||
|
||||
ArgumentCaptor<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
|
||||
verify(db).createCollection(any(), options.capture());
|
||||
|
||||
assertThat(options.getValue().getExpireAfter(TimeUnit.DAYS))
|
||||
.isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test // GH-4099
|
||||
void createCollectionShouldSetUpTimeSeriesWithExpirationFromExpression() {
|
||||
|
||||
template.createCollection(TimeSeriesTypeWithExpireAfterAsExpression.class).subscribe();
|
||||
|
||||
ArgumentCaptor<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
|
||||
verify(db).createCollection(any(), options.capture());
|
||||
|
||||
assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS))
|
||||
.isEqualTo(11);
|
||||
}
|
||||
|
||||
@Test // GH-4099
|
||||
void createCollectionShouldSetUpTimeSeriesWithExpirationFromExpressionReturningDuration() {
|
||||
|
||||
template.createCollection(TimeSeriesTypeWithExpireAfterAsExpressionResultingInDuration.class).subscribe();
|
||||
|
||||
ArgumentCaptor<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
|
||||
verify(db).createCollection(any(), options.capture());
|
||||
|
||||
assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS))
|
||||
.isEqualTo(100);
|
||||
}
|
||||
|
||||
@Test // GH-4099
|
||||
void createCollectionShouldSetUpTimeSeriesWithInvalidTimeoutExpiration() {
|
||||
|
||||
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() ->
|
||||
template.createCollection(TimeSeriesTypeWithInvalidExpireAfter.class).subscribe()
|
||||
);
|
||||
}
|
||||
|
||||
@Test // GH-4099
|
||||
void createCollectionShouldSetUpTimeSeriesWithDuplicateTimeoutExpiration() {
|
||||
|
||||
assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() ->
|
||||
template.createCollection(TimeSeriesTypeWithDuplicateExpireAfter.class).subscribe()
|
||||
);
|
||||
}
|
||||
|
||||
private void stubFindSubscribe(Document document) {
|
||||
stubFindSubscribe(document, new AtomicLong());
|
||||
}
|
||||
@@ -1887,6 +1964,55 @@ public class ReactiveMongoTemplateUnitTests {
|
||||
Object meta;
|
||||
}
|
||||
|
||||
@TimeSeries(timeField = "timestamp", expireAfterSeconds = 60)
|
||||
static class TimeSeriesTypeWithExpireAfterSeconds {
|
||||
|
||||
String id;
|
||||
Instant timestamp;
|
||||
}
|
||||
|
||||
@TimeSeries(timeField = "timestamp", expireAfter = "10m")
|
||||
static class TimeSeriesTypeWithExpireAfterAsPlainString {
|
||||
|
||||
String id;
|
||||
Instant timestamp;
|
||||
}
|
||||
|
||||
@TimeSeries(timeField = "timestamp", expireAfter = "P1D")
|
||||
static class TimeSeriesTypeWithExpireAfterAsIso8601Style {
|
||||
|
||||
String id;
|
||||
Instant timestamp;
|
||||
}
|
||||
|
||||
@TimeSeries(timeField = "timestamp", expireAfter = "#{10 + 1 + 's'}")
|
||||
static class TimeSeriesTypeWithExpireAfterAsExpression {
|
||||
|
||||
String id;
|
||||
Instant timestamp;
|
||||
}
|
||||
|
||||
@TimeSeries(timeField = "timestamp", expireAfter = "#{T(java.time.Duration).ofSeconds(100)}")
|
||||
static class TimeSeriesTypeWithExpireAfterAsExpressionResultingInDuration {
|
||||
|
||||
String id;
|
||||
Instant timestamp;
|
||||
}
|
||||
|
||||
@TimeSeries(timeField = "timestamp", expireAfter = "123ops")
|
||||
static class TimeSeriesTypeWithInvalidExpireAfter {
|
||||
|
||||
String id;
|
||||
Instant timestamp;
|
||||
}
|
||||
|
||||
@TimeSeries(timeField = "timestamp", expireAfter = "1s", expireAfterSeconds = 2)
|
||||
static class TimeSeriesTypeWithDuplicateExpireAfter {
|
||||
|
||||
String id;
|
||||
Instant timestamp;
|
||||
}
|
||||
|
||||
static class ValueCapturingEntityCallback<T> {
|
||||
|
||||
private final List<T> values = new ArrayList<>(1);
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -44,6 +45,7 @@ import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
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.test.util.Client;
|
||||
import org.springframework.data.mongodb.test.util.MongoClientExtension;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
@@ -59,6 +61,7 @@ import com.mongodb.client.MongoClient;
|
||||
* @author Christoph Strobl
|
||||
* @author Jordi Llach
|
||||
* @author Mark Paluch
|
||||
* @author Ben Foster
|
||||
*/
|
||||
@ExtendWith({ MongoClientExtension.class, SpringExtension.class })
|
||||
@ContextConfiguration
|
||||
@@ -102,6 +105,7 @@ public class IndexingIntegrationTests {
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
operations.dropCollection(IndexedPerson.class);
|
||||
operations.dropCollection(TimeSeriesWithSpelIndexTimeout.class);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-237
|
||||
@@ -159,6 +163,27 @@ public class IndexingIntegrationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test // GH-4099
|
||||
@DirtiesContext
|
||||
public void evaluatesTimeSeriesTimeoutSpelExpresssionWithBeanReference() {
|
||||
|
||||
operations.createCollection(TimeSeriesWithSpelIndexTimeout.class);
|
||||
|
||||
final Optional<org.bson.Document> collectionInfo = operations.execute(db -> {
|
||||
return db.listCollections().into(new ArrayList<>())
|
||||
.stream()
|
||||
.filter(c -> "timeSeriesWithSpelIndexTimeout".equals(c.get("name")))
|
||||
.findFirst();
|
||||
});
|
||||
|
||||
assertThat(collectionInfo).isPresent();
|
||||
assertThat(collectionInfo.get()).hasEntrySatisfying("options", options -> {
|
||||
final org.bson.Document optionsDoc = (org.bson.Document) options;
|
||||
// MongoDB 5 returns int not long
|
||||
assertThat(optionsDoc.get("expireAfterSeconds")).isIn(11, 11L);
|
||||
});
|
||||
}
|
||||
|
||||
@Target({ ElementType.FIELD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Indexed
|
||||
@@ -189,6 +214,11 @@ public class IndexingIntegrationTests {
|
||||
@Indexed(expireAfter = "#{@myTimeoutResolver?.timeout}") String someString;
|
||||
}
|
||||
|
||||
@TimeSeries(expireAfter = "#{@myTimeoutResolver?.timeout}", timeField = "timestamp")
|
||||
class TimeSeriesWithSpelIndexTimeout {
|
||||
Instant timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether an index with the given name exists for the given entity type.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user