Browse Source

Add support for collection expiration to @TimeSeries.

Closes: #4099
Original Pull Request: #4114
pull/4007/merge
Ben Foster 3 years ago committed by Christoph Strobl
parent
commit
197998f7cb
No known key found for this signature in database
GPG Key ID: E6054036D0C37A4B
  1. 29
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
  2. 137
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java
  3. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DurationStyle.java
  4. 39
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/TimeSeries.java
  5. 127
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java
  6. 126
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java
  7. 30
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexingIntegrationTests.java

29
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java

@ -15,6 +15,7 @@ @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -697,6 +711,13 @@ public class CollectionOptions {
return granularity;
}
/**
* @return {@literal -1} if not specified
*/
public long getExpireAfterSeconds() {
return expireAfterSeconds;
}
@Override
public String toString() {

137
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java

@ -15,11 +15,13 @@ @@ -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; @@ -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; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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;
}
}

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DurationStyle.java

@ -33,7 +33,7 @@ import org.springframework.util.StringUtils; @@ -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'.

39
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/TimeSeries.java

@ -28,6 +28,7 @@ import org.springframework.data.mongodb.core.timeseries.Granularity; @@ -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 { @@ -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">
*
* &#0064;Indexed(expireAfter = "10s") String expireAfterTenSeconds;
*
* &#0064;Indexed(expireAfter = "1d") String expireAfterOneDay;
*
* &#0064;Indexed(expireAfter = "P2D") String expireAfterTwoDays;
*
* &#0064;Indexed(expireAfter = "#{&#0064;mySpringBean.timeout}") String expireAfterTimeoutObtainedFromSpringBean;
* </pre>
*
* @return empty by default.
*/
String expireAfter() default "";
}

127
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java

@ -141,6 +141,7 @@ import com.mongodb.client.result.UpdateResult; @@ -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 { @@ -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 { @@ -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

126
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java

@ -132,6 +132,7 @@ import com.mongodb.reactivestreams.client.MongoDatabase; @@ -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 { @@ -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 { @@ -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);

30
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/IndexingIntegrationTests.java

@ -21,6 +21,7 @@ import java.lang.annotation.ElementType; @@ -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; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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.
*

Loading…
Cancel
Save