diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
index 23ad63f1c..879a2425f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java
@@ -20,6 +20,7 @@ import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
+import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
@@ -269,15 +270,39 @@ public interface MongoOperations extends FluentMongoOperations {
*
TimeSeries time and meta fields, granularity and {@code expireAfter}
*
* Any other options such as change stream options, schema-based details (validation, encryption) are not considered
- * and must be provided through {@link #createCollection(Class, CollectionOptions)} or
- * {@link #createCollection(String, CollectionOptions)}.
+ * and must be provided through {@link #createCollection(Class, Function)} or
+ * {@link #createCollection(Class, CollectionOptions)}.
*
* @param entityClass class that determines the collection to create.
* @return the created collection.
+ * @see #createCollection(Class, Function)
* @see #createCollection(Class, CollectionOptions)
*/
MongoCollection createCollection(Class entityClass);
+ /**
+ * Create an uncapped collection with a name based on the provided entity class allowing to customize derived
+ * {@link CollectionOptions}.
+ *
+ * This method derives {@link CollectionOptions} from the given {@code entityClass} using
+ * {@link org.springframework.data.mongodb.core.mapping.Document} and
+ * {@link org.springframework.data.mongodb.core.mapping.TimeSeries} annotations to determine:
+ *
+ * - Collation
+ * - TimeSeries time and meta fields, granularity and {@code expireAfter}
+ *
+ * Any other options such as change stream options, schema-based details (validation, encryption) are not considered
+ * and must be provided through {@link CollectionOptions}.
+ *
+ * @param entityClass class that determines the collection to create.
+ * @param collectionOptionsCustomizer customizer function to customize the derived {@link CollectionOptions}.
+ * @return the created collection.
+ * @see #createCollection(Class, CollectionOptions)
+ * @since 5.0
+ */
+ MongoCollection createCollection(Class entityClass,
+ Function super CollectionOptions, ? extends CollectionOptions> collectionOptionsCustomizer);
+
/**
* Create a collection with a name based on the provided entity class using the 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 ab03b4142..8682f77ec 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
@@ -23,6 +23,7 @@ import java.math.RoundingMode;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BiPredicate;
+import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -650,7 +651,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
@Override
public MongoCollection createCollection(Class entityClass) {
- return createCollection(entityClass, operations.forType(entityClass).getCollectionOptions());
+ return createCollection(entityClass, Function.identity());
+ }
+
+ @Override
+ public MongoCollection createCollection(Class entityClass,
+ Function super CollectionOptions, ? extends CollectionOptions> collectionOptionsCustomizer) {
+
+ Assert.notNull(collectionOptionsCustomizer, "CollectionOptions customizer function must not be null");
+
+ return createCollection(entityClass,
+ collectionOptionsCustomizer.apply(operations.forType(entityClass).getCollectionOptions()));
}
@Override
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
index ab86c6083..3e2249201 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
@@ -20,6 +20,7 @@ import reactor.core.publisher.Mono;
import java.util.Collection;
import java.util.function.Consumer;
+import java.util.function.Function;
import java.util.function.Supplier;
import org.bson.Document;
@@ -223,15 +224,39 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* TimeSeries time and meta fields, granularity and {@code expireAfter}
*
* Any other options such as change stream options, schema-based details (validation, encryption) are not considered
- * and must be provided through {@link #createCollection(Class, CollectionOptions)} or
- * {@link #createCollection(String, CollectionOptions)}.
+ * and must be provided through {@link #createCollection(Class, Function)} or
+ * {@link #createCollection(Class, CollectionOptions)}.
*
* @param entityClass class that determines the collection to create.
* @return the created collection.
+ * @see #createCollection(Class, Function)
* @see #createCollection(Class, CollectionOptions)
*/
Mono> createCollection(Class entityClass);
+ /**
+ * Create an uncapped collection with a name based on the provided entity class allowing to customize derived
+ * {@link CollectionOptions}.
+ *
+ * This method derives {@link CollectionOptions} from the given {@code entityClass} using
+ * {@link org.springframework.data.mongodb.core.mapping.Document} and
+ * {@link org.springframework.data.mongodb.core.mapping.TimeSeries} annotations to determine:
+ *
+ * - Collation
+ * - TimeSeries time and meta fields, granularity and {@code expireAfter}
+ *
+ * Any other options such as change stream options, schema-based details (validation, encryption) are not considered
+ * and must be provided through {@link CollectionOptions}.
+ *
+ * @param entityClass class that determines the collection to create.
+ * @param collectionOptionsCustomizer customizer function to customize the derived {@link CollectionOptions}.
+ * @return the created collection.
+ * @see #createCollection(Class, CollectionOptions)
+ * @since 5.0
+ */
+ Mono> createCollection(Class entityClass,
+ Function super CollectionOptions, ? extends CollectionOptions> collectionOptionsCustomizer);
+
/**
* Create a collection with a name based on the provided entity class using the options.
*
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 0ad473b8b..232f35cd1 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
@@ -659,7 +659,17 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
@Override
public Mono> createCollection(Class entityClass) {
- return createCollection(entityClass, operations.forType(entityClass).getCollectionOptions());
+ return createCollection(entityClass, Function.identity());
+ }
+
+ @Override
+ public Mono> createCollection(Class entityClass,
+ Function super CollectionOptions, ? extends CollectionOptions> collectionOptionsCustomizer) {
+
+ Assert.notNull(collectionOptionsCustomizer, "CollectionOptions customizer function must not be null");
+
+ return createCollection(entityClass,
+ collectionOptionsCustomizer.apply(operations.forType(entityClass).getCollectionOptions()));
}
@Override
@@ -740,11 +750,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
@Override
public Mono collectionExists(String collectionName) {
- return createMono(
- db -> Flux.from(db.listCollectionNames()) //
- .filter(s -> s.equals(collectionName)) //
- .map(s -> true) //
- .single(false));
+ return createMono(db -> Flux.from(db.listCollectionNames()) //
+ .filter(s -> s.equals(collectionName)) //
+ .map(s -> true) //
+ .single(false));
}
@Override
@@ -2293,7 +2302,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
.flatMapSequential(deleteResult -> Flux.fromIterable(list)));
}
- @SuppressWarnings({"rawtypes", "unchecked", "NullAway"})
+ @SuppressWarnings({ "rawtypes", "unchecked", "NullAway" })
Flux doFindAndDelete(String collectionName, Query query, Class entityClass,
QueryResultConverter super S, ? extends T> resultConverter) {
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 ef72548fa..177764e4c 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
@@ -186,11 +186,13 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
when(collection.aggregate(any(List.class), any())).thenReturn(aggregateIterable);
when(collection.withReadConcern(any())).thenReturn(collection);
when(collection.withReadPreference(any())).thenReturn(collection);
- when(collection.replaceOne(any(), any(), any(com.mongodb.client.model.ReplaceOptions.class))).thenReturn(updateResult);
+ when(collection.replaceOne(any(), any(), any(com.mongodb.client.model.ReplaceOptions.class)))
+ .thenReturn(updateResult);
when(collection.withWriteConcern(any())).thenReturn(collectionWithWriteConcern);
when(collection.distinct(anyString(), any(Document.class), any())).thenReturn(distinctIterable);
when(collectionWithWriteConcern.deleteOne(any(Bson.class), any())).thenReturn(deleteResult);
- when(collectionWithWriteConcern.replaceOne(any(), any(), any(com.mongodb.client.model.ReplaceOptions.class))).thenReturn(updateResult);
+ when(collectionWithWriteConcern.replaceOne(any(), any(), any(com.mongodb.client.model.ReplaceOptions.class)))
+ .thenReturn(updateResult);
when(findIterable.projection(any())).thenReturn(findIterable);
when(findIterable.sort(any(org.bson.Document.class))).thenReturn(findIterable);
when(findIterable.collation(any())).thenReturn(findIterable);
@@ -1263,7 +1265,8 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
template.save(entity);
- verify(collection, times(1)).replaceOne(queryCaptor.capture(), updateCaptor.capture(), any(com.mongodb.client.model.ReplaceOptions.class));
+ verify(collection, times(1)).replaceOne(queryCaptor.capture(), updateCaptor.capture(),
+ any(com.mongodb.client.model.ReplaceOptions.class));
assertThat(queryCaptor.getValue()).isEqualTo(new Document("_id", 1).append("version", 10));
assertThat(updateCaptor.getValue())
@@ -1399,10 +1402,14 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
Assertions.assertThat(options.getValue().getCollation()).isNull();
}
- @Test // DATAMONGO-1854
+ @Test // DATAMONGO-1854, GH-4978
void createCollectionShouldApplyDefaultCollation() {
- template.createCollection(Sith.class);
+ template.createCollection(Sith.class, options -> {
+
+ assertThat(options.getCollation()).contains(Collation.of("de_AT"));
+ return options;
+ });
ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
@@ -1426,7 +1433,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
@Test // DATAMONGO-1854
void createCollectionShouldUseDefaultCollationIfCollectionOptionsAreNull() {
- template.createCollection(Sith.class, null);
+ template.createCollection(Sith.class, (CollectionOptions) null);
ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
@@ -2399,8 +2406,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
- assertThat(options.getValue().getExpireAfter(TimeUnit.MINUTES))
- .isEqualTo(10);
+ assertThat(options.getValue().getExpireAfter(TimeUnit.MINUTES)).isEqualTo(10);
}
@Test // GH-4099
@@ -2413,8 +2419,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
- assertThat(options.getValue().getExpireAfter(TimeUnit.MINUTES))
- .isEqualTo(12);
+ assertThat(options.getValue().getExpireAfter(TimeUnit.MINUTES)).isEqualTo(12);
}
@Test // GH-4099
@@ -2425,8 +2430,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
- assertThat(options.getValue().getExpireAfter(TimeUnit.DAYS))
- .isEqualTo(1);
+ assertThat(options.getValue().getExpireAfter(TimeUnit.DAYS)).isEqualTo(1);
}
@Test // GH-4099
@@ -2437,8 +2441,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
- assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS))
- .isEqualTo(11);
+ assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS)).isEqualTo(11);
}
@Test // GH-4099
@@ -2449,16 +2452,14 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
- assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS))
- .isEqualTo(100);
+ assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS)).isEqualTo(100);
}
@Test // GH-4099
void createCollectionShouldSetUpTimeSeriesWithInvalidTimeoutExpiration() {
- assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() ->
- template.createCollection(TimeSeriesTypeWithInvalidExpireAfter.class)
- );
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> template.createCollection(TimeSeriesTypeWithInvalidExpireAfter.class));
}
@Test // GH-3522
@@ -2611,32 +2612,31 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
verify(collection).withWriteConcern(eq(WriteConcern.UNACKNOWLEDGED));
}
- @Test // GH-4099
- void passOnTimeSeriesExpireOption() {
-
- template.createCollection("time-series-collection",
- CollectionOptions.timeSeries("time_stamp", options -> options.expireAfter(Duration.ofSeconds(10))));
+ @Test // GH-4099
+ void passOnTimeSeriesExpireOption() {
- ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
- verify(db).createCollection(any(), options.capture());
+ template.createCollection("time-series-collection",
+ CollectionOptions.timeSeries("time_stamp", options -> options.expireAfter(Duration.ofSeconds(10))));
- assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS)).isEqualTo(10);
- }
+ ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
+ verify(db).createCollection(any(), options.capture());
- @Test // GH-4099
- void doNotSetTimeSeriesExpireOptionForNegativeValue() {
+ assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS)).isEqualTo(10);
+ }
- template.createCollection("time-series-collection",
- CollectionOptions.timeSeries("time_stamp", options -> options.expireAfter(Duration.ofSeconds(-10))));
+ @Test // GH-4099
+ void doNotSetTimeSeriesExpireOptionForNegativeValue() {
- ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
- verify(db).createCollection(any(), options.capture());
+ template.createCollection("time-series-collection",
+ CollectionOptions.timeSeries("time_stamp", options -> options.expireAfter(Duration.ofSeconds(-10))));
- assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS)).isEqualTo(0L);
- }
+ ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
+ verify(db).createCollection(any(), options.capture());
+ assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS)).isEqualTo(0L);
+ }
- class AutogenerateableId {
+ class AutogenerateableId {
@Id BigInteger id;
}
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 36cf0886a..97f22378d 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
@@ -456,8 +456,10 @@ public class ReactiveMongoTemplateUnitTests {
@Test // DATAMONGO-1719
void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() {
- template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class,
- PersonSpELProjection.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe();
+ template
+ .doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class,
+ PersonSpELProjection.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER)
+ .subscribe();
verify(findPublisher, never()).projection(any());
}
@@ -638,10 +640,14 @@ public class ReactiveMongoTemplateUnitTests {
Assertions.assertThat(options.getValue().getCollation()).isNull();
}
- @Test // DATAMONGO-1854
+ @Test // DATAMONGO-1854, GH-4978
void createCollectionShouldApplyDefaultCollation() {
- template.createCollection(Sith.class).subscribe();
+ template.createCollection(Sith.class, options -> {
+
+ assertThat(options.getCollation()).contains(Collation.of("de_AT"));
+ return options;
+ }).subscribe();
ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
@@ -665,7 +671,7 @@ public class ReactiveMongoTemplateUnitTests {
@Test // DATAMONGO-1854
void createCollectionShouldUseDefaultCollationIfCollectionOptionsAreNull() {
- template.createCollection(Sith.class, null).subscribe();
+ template.createCollection(Sith.class, (CollectionOptions) null).subscribe();
ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
@@ -1751,8 +1757,7 @@ public class ReactiveMongoTemplateUnitTests {
ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
- assertThat(options.getValue().getExpireAfter(TimeUnit.MINUTES))
- .isEqualTo(10);
+ assertThat(options.getValue().getExpireAfter(TimeUnit.MINUTES)).isEqualTo(10);
}
@Test // GH-4099
@@ -1763,8 +1768,7 @@ public class ReactiveMongoTemplateUnitTests {
ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
- assertThat(options.getValue().getExpireAfter(TimeUnit.DAYS))
- .isEqualTo(1);
+ assertThat(options.getValue().getExpireAfter(TimeUnit.DAYS)).isEqualTo(1);
}
@Test // GH-4099
@@ -1775,8 +1779,7 @@ public class ReactiveMongoTemplateUnitTests {
ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
- assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS))
- .isEqualTo(11);
+ assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS)).isEqualTo(11);
}
@Test // GH-4099
@@ -1787,16 +1790,14 @@ public class ReactiveMongoTemplateUnitTests {
ArgumentCaptor options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
- assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS))
- .isEqualTo(100);
+ assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS)).isEqualTo(100);
}
@Test // GH-4099
void createCollectionShouldSetUpTimeSeriesWithInvalidTimeoutExpiration() {
- assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() ->
- template.createCollection(TimeSeriesTypeWithInvalidExpireAfter.class).subscribe()
- );
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> template.createCollection(TimeSeriesTypeWithInvalidExpireAfter.class).subscribe());
}
private void stubFindSubscribe(Document document) {