From 4e53fa792e8582e19ea7498ee353a8bc0dbaa3a2 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 19 May 2025 14:44:15 +0200 Subject: [PATCH] =?UTF-8?q?Add=20`createCollection(=E2=80=A6)`=20overload?= =?UTF-8?q?=20accepting=20a=20customizer=20function=20for=20`CollectionOpt?= =?UTF-8?q?ions`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Original Pull Request: #4979 --- .../data/mongodb/core/MongoOperations.java | 29 +++++++- .../data/mongodb/core/MongoTemplate.java | 13 +++- .../mongodb/core/ReactiveMongoOperations.java | 29 +++++++- .../mongodb/core/ReactiveMongoTemplate.java | 23 ++++-- .../mongodb/core/MongoTemplateUnitTests.java | 74 +++++++++---------- .../core/ReactiveMongoTemplateUnitTests.java | 33 +++++---- 6 files changed, 136 insertions(+), 65 deletions(-) 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 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 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 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 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 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) {