Add createCollection(…) overload accepting a customizer function for CollectionOptions.

Original Pull Request: #4979
This commit is contained in:
Mark Paluch
2025-05-19 14:44:15 +02:00
committed by Christoph Strobl
parent 172a7d1940
commit 4e53fa792e
6 changed files with 136 additions and 65 deletions

View File

@@ -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 {
* <li>TimeSeries time and meta fields, granularity and {@code expireAfter}</li>
* </ul>
* 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)
*/
<T> MongoCollection<Document> createCollection(Class<T> entityClass);
/**
* Create an uncapped collection with a name based on the provided entity class allowing to customize derived
* {@link CollectionOptions}.
* <p>
* 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:
* <ul>
* <li>Collation</li>
* <li>TimeSeries time and meta fields, granularity and {@code expireAfter}</li>
* </ul>
* 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
*/
<T> MongoCollection<Document> createCollection(Class<T> entityClass,
Function<? super CollectionOptions, ? extends CollectionOptions> collectionOptionsCustomizer);
/**
* Create a collection with a name based on the provided entity class using the options.
*

View File

@@ -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 <T> MongoCollection<Document> createCollection(Class<T> entityClass) {
return createCollection(entityClass, operations.forType(entityClass).getCollectionOptions());
return createCollection(entityClass, Function.identity());
}
@Override
public <T> MongoCollection<Document> createCollection(Class<T> 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

View File

@@ -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 {
* <li>TimeSeries time and meta fields, granularity and {@code expireAfter}</li>
* </ul>
* 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)
*/
<T> Mono<MongoCollection<Document>> createCollection(Class<T> entityClass);
/**
* Create an uncapped collection with a name based on the provided entity class allowing to customize derived
* {@link CollectionOptions}.
* <p>
* 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:
* <ul>
* <li>Collation</li>
* <li>TimeSeries time and meta fields, granularity and {@code expireAfter}</li>
* </ul>
* 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
*/
<T> Mono<MongoCollection<Document>> createCollection(Class<T> entityClass,
Function<? super CollectionOptions, ? extends CollectionOptions> collectionOptionsCustomizer);
/**
* Create a collection with a name based on the provided entity class using the options.
*

View File

@@ -659,7 +659,17 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
@Override
public <T> Mono<MongoCollection<Document>> createCollection(Class<T> entityClass) {
return createCollection(entityClass, operations.forType(entityClass).getCollectionOptions());
return createCollection(entityClass, Function.identity());
}
@Override
public <T> Mono<MongoCollection<Document>> createCollection(Class<T> 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<Boolean> 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" })
<S, T> Flux<T> doFindAndDelete(String collectionName, Query query, Class<S> entityClass,
QueryResultConverter<? super S, ? extends T> resultConverter) {

View File

@@ -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<CreateCollectionOptions> 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<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
@@ -2399,8 +2406,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
ArgumentCaptor<CreateCollectionOptions> 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<CreateCollectionOptions> 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<CreateCollectionOptions> 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<CreateCollectionOptions> 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<CreateCollectionOptions> 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() {
@Test // GH-4099
void passOnTimeSeriesExpireOption() {
template.createCollection("time-series-collection",
CollectionOptions.timeSeries("time_stamp", options -> options.expireAfter(Duration.ofSeconds(10))));
template.createCollection("time-series-collection",
CollectionOptions.timeSeries("time_stamp", options -> options.expireAfter(Duration.ofSeconds(10))));
ArgumentCaptor<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
ArgumentCaptor<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS)).isEqualTo(10);
}
assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS)).isEqualTo(10);
}
@Test // GH-4099
void doNotSetTimeSeriesExpireOptionForNegativeValue() {
@Test // GH-4099
void doNotSetTimeSeriesExpireOptionForNegativeValue() {
template.createCollection("time-series-collection",
CollectionOptions.timeSeries("time_stamp", options -> options.expireAfter(Duration.ofSeconds(-10))));
template.createCollection("time-series-collection",
CollectionOptions.timeSeries("time_stamp", options -> options.expireAfter(Duration.ofSeconds(-10))));
ArgumentCaptor<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
ArgumentCaptor<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS)).isEqualTo(0L);
}
assertThat(options.getValue().getExpireAfter(TimeUnit.SECONDS)).isEqualTo(0L);
}
class AutogenerateableId {
class AutogenerateableId {
@Id BigInteger id;
}

View File

@@ -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<CreateCollectionOptions> 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<CreateCollectionOptions> options = ArgumentCaptor.forClass(CreateCollectionOptions.class);
verify(db).createCollection(any(), options.capture());
@@ -1751,8 +1757,7 @@ public class ReactiveMongoTemplateUnitTests {
ArgumentCaptor<CreateCollectionOptions> 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<CreateCollectionOptions> 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<CreateCollectionOptions> 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<CreateCollectionOptions> 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) {