Polishing.

Extract CreateCollectionOptions conversion to EntityOperations to unify collection creation. Adopt tests.

See #3984
Original pull request: #3990.
This commit is contained in:
Mark Paluch
2022-03-11 15:02:54 +01:00
parent 1f06954952
commit 612845f59c
6 changed files with 195 additions and 177 deletions

View File

@@ -21,6 +21,7 @@ import java.util.Map;
import java.util.Optional;
import org.bson.Document;
import org.springframework.core.convert.ConversionService;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.convert.CustomConversions;
@@ -32,7 +33,9 @@ import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions;
import org.springframework.data.mongodb.core.convert.MongoConverter;
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.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
@@ -41,9 +44,11 @@ import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.timeseries.Granularity;
import org.springframework.data.mongodb.core.validation.Validator;
import org.springframework.data.projection.EntityProjection;
import org.springframework.data.projection.EntityProjectionIntrospector;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.util.Optionals;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -52,6 +57,10 @@ import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.client.model.TimeSeriesGranularity;
import com.mongodb.client.model.ValidationOptions;
/**
* Common operations performed on an entity in the context of it's mapping metadata.
*
@@ -67,20 +76,31 @@ class EntityOperations {
private static final String ID_FIELD = "_id";
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context;
private final QueryMapper queryMapper;
private final EntityProjectionIntrospector introspector;
private final MongoJsonSchemaMapper schemaMapper;
EntityOperations(MongoConverter converter) {
this(converter.getMappingContext(), converter.getCustomConversions(), converter.getProjectionFactory());
this(converter, new QueryMapper(converter));
}
EntityOperations(MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context,
CustomConversions conversions, ProjectionFactory projectionFactory) {
EntityOperations(MongoConverter converter, QueryMapper queryMapper) {
this(converter, converter.getMappingContext(), converter.getCustomConversions(), converter.getProjectionFactory(),
queryMapper);
}
EntityOperations(MongoConverter converter,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context,
CustomConversions conversions, ProjectionFactory projectionFactory, QueryMapper queryMapper) {
this.context = context;
this.queryMapper = queryMapper;
this.introspector = EntityProjectionIntrospector.create(projectionFactory,
EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy()
.and(((target, underlyingType) -> !conversions.isSimpleType(target))),
context);
this.schemaMapper = new MongoJsonSchemaMapper(converter);
}
/**
@@ -259,6 +279,75 @@ class EntityOperations {
return introspector.introspect(resultType, entityType);
}
/**
* Convert {@link CollectionOptions} to {@link CreateCollectionOptions} using {@link Class entityType} to obtain
* mapping metadata.
*
* @param collectionOptions
* @param entityType
* @return
* @since 3.4
*/
public CreateCollectionOptions convertToCreateCollectionOptions(@Nullable CollectionOptions collectionOptions,
Class<?> entityType) {
Optional<Collation> collation = Optionals.firstNonEmpty(
() -> Optional.ofNullable(collectionOptions).flatMap(CollectionOptions::getCollation),
() -> forType(entityType).getCollation());//
CreateCollectionOptions result = new CreateCollectionOptions();
collation.map(Collation::toMongoCollation).ifPresent(result::collation);
if (collectionOptions == null) {
return result;
}
collectionOptions.getCapped().ifPresent(result::capped);
collectionOptions.getSize().ifPresent(result::sizeInBytes);
collectionOptions.getMaxDocuments().ifPresent(result::maxDocuments);
collectionOptions.getCollation().map(Collation::toMongoCollation).ifPresent(result::collation);
collectionOptions.getValidationOptions().ifPresent(it -> {
ValidationOptions validationOptions = new ValidationOptions();
it.getValidationAction().ifPresent(validationOptions::validationAction);
it.getValidationLevel().ifPresent(validationOptions::validationLevel);
it.getValidator().ifPresent(val -> validationOptions.validator(getMappedValidator(val, entityType)));
result.validationOptions(validationOptions);
});
collectionOptions.getTimeSeriesOptions().map(forType(entityType)::mapTimeSeriesOptions).ifPresent(it -> {
com.mongodb.client.model.TimeSeriesOptions options = new com.mongodb.client.model.TimeSeriesOptions(
it.getTimeField());
if (StringUtils.hasText(it.getMetaField())) {
options.metaField(it.getMetaField());
}
if (!Granularity.DEFAULT.equals(it.getGranularity())) {
options.granularity(TimeSeriesGranularity.valueOf(it.getGranularity().name().toUpperCase()));
}
result.timeSeriesOptions(options);
});
return result;
}
private Document getMappedValidator(Validator validator, Class<?> domainType) {
Document validationRules = validator.toDocument();
if (validationRules.containsKey("$jsonSchema")) {
return schemaMapper.mapSchema(validationRules, domainType);
}
return queryMapper.getMappedObject(validationRules, context.getPersistentEntity(domainType));
}
/**
* A representation of information about an entity.
*

View File

@@ -224,7 +224,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
this.queryMapper = new QueryMapper(this.mongoConverter);
this.updateMapper = new UpdateMapper(this.mongoConverter);
this.schemaMapper = new MongoJsonSchemaMapper(this.mongoConverter);
this.operations = new EntityOperations(this.mongoConverter);
this.operations = new EntityOperations(this.mongoConverter, this.queryMapper);
this.propertyOperations = new PropertyOperations(this.mongoConverter.getMappingContext());
this.queryOperations = new QueryOperations(queryMapper, updateMapper, operations, propertyOperations,
mongoDbFactory);
@@ -595,13 +595,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Assert.notNull(entityClass, "EntityClass must not be null!");
CollectionOptions options = collectionOptions != null ? collectionOptions : CollectionOptions.empty();
options = Optionals
.firstNonEmpty(() -> Optional.ofNullable(collectionOptions).flatMap(CollectionOptions::getCollation),
() -> operations.forType(entityClass).getCollation()) //
.map(options::collation).orElse(options);
return doCreateCollection(getCollectionName(entityClass), convertToDocument(options, entityClass));
return doCreateCollection(getCollectionName(entityClass),
operations.convertToCreateCollectionOptions(collectionOptions, entityClass));
}
@Override
@@ -617,7 +612,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
@Nullable CollectionOptions collectionOptions) {
Assert.notNull(collectionName, "CollectionName must not be null!");
return doCreateCollection(collectionName, convertToDocument(collectionOptions, Object.class));
return doCreateCollection(collectionName,
operations.convertToCreateCollectionOptions(collectionOptions, Object.class));
}
@Override
@@ -2225,54 +2221,24 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
*/
@SuppressWarnings("ConstantConditions")
protected MongoCollection<Document> doCreateCollection(String collectionName, Document collectionOptions) {
return doCreateCollection(collectionName, getCreateCollectionOptions(collectionOptions));
}
/**
* Create the specified collection using the provided options
*
* @param collectionName
* @param collectionOptions
* @return the collection that was created
* @since 3.3.3
*/
@SuppressWarnings("ConstantConditions")
protected MongoCollection<Document> doCreateCollection(String collectionName,
CreateCollectionOptions collectionOptions) {
return execute(db -> {
CreateCollectionOptions co = new CreateCollectionOptions();
if (collectionOptions.containsKey("capped")) {
co.capped((Boolean) collectionOptions.get("capped"));
}
if (collectionOptions.containsKey("size")) {
co.sizeInBytes(((Number) collectionOptions.get("size")).longValue());
}
if (collectionOptions.containsKey("max")) {
co.maxDocuments(((Number) collectionOptions.get("max")).longValue());
}
if (collectionOptions.containsKey("collation")) {
co.collation(IndexConverters.fromDocument(collectionOptions.get("collation", Document.class)));
}
if (collectionOptions.containsKey("validator")) {
com.mongodb.client.model.ValidationOptions options = new com.mongodb.client.model.ValidationOptions();
if (collectionOptions.containsKey("validationLevel")) {
options.validationLevel(ValidationLevel.fromString(collectionOptions.getString("validationLevel")));
}
if (collectionOptions.containsKey("validationAction")) {
options.validationAction(ValidationAction.fromString(collectionOptions.getString("validationAction")));
}
options.validator(collectionOptions.get("validator", Document.class));
co.validationOptions(options);
}
if (collectionOptions.containsKey("timeseries")) {
Document timeSeries = collectionOptions.get("timeseries", Document.class);
com.mongodb.client.model.TimeSeriesOptions options = new com.mongodb.client.model.TimeSeriesOptions(
timeSeries.getString("timeField"));
if (timeSeries.containsKey("metaField")) {
options.metaField(timeSeries.getString("metaField"));
}
if (timeSeries.containsKey("granularity")) {
options.granularity(TimeSeriesGranularity.valueOf(timeSeries.getString("granularity").toUpperCase()));
}
co.timeSeriesOptions(options);
}
db.createCollection(collectionName, co);
db.createCollection(collectionName, collectionOptions);
MongoCollection<Document> coll = db.getCollection(collectionName, Document.class);
@@ -2285,6 +2251,54 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
});
}
private CreateCollectionOptions getCreateCollectionOptions(Document document) {
CreateCollectionOptions options = new CreateCollectionOptions();
if (document.containsKey("capped")) {
options.capped((Boolean) document.get("capped"));
}
if (document.containsKey("size")) {
options.sizeInBytes(((Number) document.get("size")).longValue());
}
if (document.containsKey("max")) {
options.maxDocuments(((Number) document.get("max")).longValue());
}
if (document.containsKey("collation")) {
options.collation(IndexConverters.fromDocument(document.get("collation", Document.class)));
}
if (document.containsKey("validator")) {
ValidationOptions validation = new ValidationOptions();
if (document.containsKey("validationLevel")) {
validation.validationLevel(ValidationLevel.fromString(document.getString("validationLevel")));
}
if (document.containsKey("validationAction")) {
validation.validationAction(ValidationAction.fromString(document.getString("validationAction")));
}
validation.validator(document.get("validator", Document.class));
options.validationOptions(validation);
}
if (document.containsKey("timeseries")) {
Document timeSeries = document.get("timeseries", Document.class);
TimeSeriesOptions timeseries = new TimeSeriesOptions(timeSeries.getString("timeField"));
if (timeSeries.containsKey("metaField")) {
timeseries.metaField(timeSeries.getString("metaField"));
}
if (timeSeries.containsKey("granularity")) {
timeseries.granularity(TimeSeriesGranularity.valueOf(timeSeries.getString("granularity").toUpperCase()));
}
options.timeSeriesOptions(timeseries);
}
return options;
}
/**
* Map the results of an ad-hoc query on the default MongoDB collection to an object using the template's converter.
* The query document is specified as a standard {@link Document} and so is the fields specification.

View File

@@ -46,6 +46,7 @@ import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@@ -85,11 +86,9 @@ import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggrega
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.JsonSchemaMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper;
import org.springframework.data.mongodb.core.convert.MongoWriter;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.convert.QueryMapper;
@@ -111,8 +110,6 @@ import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter;
import org.springframework.data.mongodb.core.timeseries.Granularity;
import org.springframework.data.mongodb.core.validation.Validator;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.projection.EntityProjection;
import org.springframework.data.util.Optionals;
@@ -130,7 +127,16 @@ import com.mongodb.CursorType;
import com.mongodb.MongoException;
import com.mongodb.ReadPreference;
import com.mongodb.WriteConcern;
import com.mongodb.client.model.*;
import com.mongodb.client.model.CountOptions;
import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.client.model.DeleteOptions;
import com.mongodb.client.model.EstimatedDocumentCountOptions;
import com.mongodb.client.model.FindOneAndDeleteOptions;
import com.mongodb.client.model.FindOneAndReplaceOptions;
import com.mongodb.client.model.FindOneAndUpdateOptions;
import com.mongodb.client.model.ReplaceOptions;
import com.mongodb.client.model.ReturnDocument;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.model.changestream.FullDocument;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.InsertOneResult;
@@ -175,7 +181,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private final PersistenceExceptionTranslator exceptionTranslator;
private final QueryMapper queryMapper;
private final UpdateMapper updateMapper;
private final JsonSchemaMapper schemaMapper;
private final ApplicationListener<MappingContextEvent<?, ?>> indexCreatorListener;
private final EntityOperations operations;
private final PropertyOperations propertyOperations;
@@ -243,12 +248,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
this.mongoConverter = mongoConverter == null ? getDefaultMongoConverter() : mongoConverter;
this.queryMapper = new QueryMapper(this.mongoConverter);
this.updateMapper = new UpdateMapper(this.mongoConverter);
this.schemaMapper = new MongoJsonSchemaMapper(this.mongoConverter);
this.indexCreatorListener = new IndexCreatorEventListener(subscriptionExceptionHandler);
// We always have a mapping context in the converter, whether it's a simple one or not
this.mappingContext = this.mongoConverter.getMappingContext();
this.operations = new EntityOperations(this.mongoConverter);
this.operations = new EntityOperations(this.mongoConverter, this.queryMapper);
this.propertyOperations = new PropertyOperations(this.mongoConverter.getMappingContext());
this.queryOperations = new QueryOperations(queryMapper, updateMapper, operations, propertyOperations,
mongoDatabaseFactory);
@@ -276,7 +280,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
this.mongoConverter = that.mongoConverter;
this.queryMapper = that.queryMapper;
this.updateMapper = that.updateMapper;
this.schemaMapper = that.schemaMapper;
this.indexCreator = that.indexCreator;
this.indexCreatorListener = that.indexCreatorListener;
this.mappingContext = that.mappingContext;
@@ -2201,56 +2204,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable CollectionOptions collectionOptions,
Class<?> entityType) {
CreateCollectionOptions result = new CreateCollectionOptions();
if (collectionOptions == null) {
return result;
}
collectionOptions.getCapped().ifPresent(result::capped);
collectionOptions.getSize().ifPresent(result::sizeInBytes);
collectionOptions.getMaxDocuments().ifPresent(result::maxDocuments);
collectionOptions.getCollation().map(Collation::toMongoCollation).ifPresent(result::collation);
collectionOptions.getValidationOptions().ifPresent(it -> {
ValidationOptions validationOptions = new ValidationOptions();
it.getValidationAction().ifPresent(validationOptions::validationAction);
it.getValidationLevel().ifPresent(validationOptions::validationLevel);
it.getValidator().ifPresent(val -> validationOptions.validator(getMappedValidator(val, entityType)));
result.validationOptions(validationOptions);
});
collectionOptions.getTimeSeriesOptions().map(operations.forType(entityType)::mapTimeSeriesOptions).ifPresent(it -> {
TimeSeriesOptions options = new TimeSeriesOptions(it.getTimeField());
if (StringUtils.hasText(it.getMetaField())) {
options.metaField(it.getMetaField());
}
if (!Granularity.DEFAULT.equals(it.getGranularity())) {
options.granularity(TimeSeriesGranularity.valueOf(it.getGranularity().name().toUpperCase()));
}
result.timeSeriesOptions(options);
});
return result;
}
private Document getMappedValidator(Validator validator, Class<?> domainType) {
Document validationRules = validator.toDocument();
if (validationRules.containsKey("$jsonSchema")) {
return schemaMapper.mapSchema(validationRules, domainType);
}
return queryMapper.getMappedObject(validationRules, mappingContext.getPersistentEntity(domainType));
return operations.convertToCreateCollectionOptions(collectionOptions, entityType);
}
/**

View File

@@ -1,59 +0,0 @@
/*
* Copyright 2019-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
/**
* @author Christoph Strobl
*/
public class EntityOperationUnitTests {
EntityOperations ops;
MongoMappingContext mappingContext = new MongoMappingContext();
ConversionService conversionService = new DefaultConversionService();
@BeforeEach
public void setUp() {
ops = new EntityOperations(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext));
}
@Test // DATAMONGO-2293
public void populateIdShouldReturnTargetBeanWhenIdIsNull() {
assertThat(initAdaptibleEntity(new DomainTypeWithIdProperty()).populateIdIfNecessary(null)).isNotNull();
}
<T> AdaptibleEntity<T> initAdaptibleEntity(T source) {
return ops.forEntity(source, conversionService);
}
private static class DomainTypeWithIdProperty {
@Id String id;
String value;
}
}

View File

@@ -21,6 +21,9 @@ import java.time.Instant;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
@@ -34,6 +37,8 @@ import org.springframework.data.mongodb.test.util.MongoTestMappingContext;
*/
class EntityOperationsUnitTests {
ConversionService conversionService = new DefaultConversionService();
EntityOperations operations = new EntityOperations(
new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, MongoTestMappingContext.newTestContext()));
@@ -51,6 +56,21 @@ class EntityOperationsUnitTests {
.withMessageContaining("Meta field 'foo' does not exist");
}
@Test // DATAMONGO-2293
void populateIdShouldReturnTargetBeanWhenIdIsNull() {
assertThat(initAdaptibleEntity(new DomainTypeWithIdProperty()).populateIdIfNecessary(null)).isNotNull();
}
<T> EntityOperations.AdaptibleEntity<T> initAdaptibleEntity(T source) {
return operations.forEntity(source, conversionService);
}
private static class DomainTypeWithIdProperty {
@Id String id;
String value;
}
@TimeSeries(timeField = "foo")
static class InvalidTimeField {

View File

@@ -48,7 +48,7 @@ class UpdateOperationsUnitTests {
MongoConverter mongoConverter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext);
QueryMapper queryMapper = new QueryMapper(mongoConverter);
UpdateMapper updateMapper = new UpdateMapper(mongoConverter);
EntityOperations entityOperations = new EntityOperations(mongoConverter);
EntityOperations entityOperations = new EntityOperations(mongoConverter, this.queryMapper);
PropertyOperations propertyOperations = new PropertyOperations(mongoConverter.getMappingContext());
ExtendedQueryOperations queryOperations = new ExtendedQueryOperations(queryMapper, updateMapper, entityOperations, propertyOperations,