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 2bb8643d1b
commit fda07d9999
6 changed files with 193 additions and 167 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 given {@link CollectionOptions} to a document and take the domain type information into account when
* creating a mapped schema for validation.
*
* @param collectionOptions can be {@literal null}.
* @param entityType must not be {@literal null}. Use {@link Object} type instead.
* @return the converted {@link CreateCollectionOptions}.
* @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

@@ -648,13 +648,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));
}
/*
@@ -676,7 +671,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));
}
/*
@@ -2475,56 +2471,23 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
* @param collectionOptions
* @return the collection that was created
*/
@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);
@@ -2537,6 +2500,54 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
});
}
private CreateCollectionOptions getCreateCollectionOptions(Document collectionOptions) {
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")) {
ValidationOptions options = new 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);
TimeSeriesOptions options = new 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);
}
return co;
}
/**
* 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

@@ -112,7 +112,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;
@@ -131,7 +130,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;
@@ -718,13 +726,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
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), convertToCreateCollectionOptions(options, entityClass));
return doCreateCollection(getCollectionName(entityClass),
operations.convertToCreateCollectionOptions(collectionOptions, entityClass));
}
/*
@@ -2542,45 +2545,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;
return operations.convertToCreateCollectionOptions(collectionOptions, entityType);
}
private Document getMappedValidator(Validator validator, Class<?> domainType) {

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,