DATAMONGO-1992 - Extract common entity operations API from (Reactive)MongoTemplate.

Introduced EntityOperations and MappedDocument to allow to share common operations from MongoTemplate and ReactiveMongoTemplate.
This commit is contained in:
Oliver Gierke
2018-06-19 15:59:39 +02:00
parent d1b1dfbae9
commit 323b0a8479
17 changed files with 1142 additions and 649 deletions

View File

@@ -0,0 +1,629 @@
/*
* Copyright 2018 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
*
* http://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 lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.Collection;
import java.util.Map;
import org.bson.Document;
import org.springframework.core.convert.ConversionService;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.mapping.IdentifierAccessor;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mongodb.core.convert.MongoWriter;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import com.mongodb.util.JSONParseException;
/**
* Common operations performed on an entity in the context of it's mapping metadata.
*
* @author Oliver Gierke
* @since 2.1
* @see MongoTemplate
* @see ReactiveMongoTemplate
*/
@RequiredArgsConstructor
class EntityOperations {
private static final String ID_FIELD = "_id";
private final @NonNull MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context;
/**
* Creates a new {@link Entity} for the given bean.
*
* @param entity must not be {@literal null}.
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> Entity<T> forEntity(T entity) {
Assert.notNull(entity, "Bean must not be null!");
if (entity instanceof String) {
return new SimpleEntity(parse(entity.toString()));
}
if (entity instanceof Map) {
return new SimpleEntity((Map<String, Object>) entity);
}
return MappedEntity.of(entity, context);
}
/**
* Creates a new {@link AdaptibleEntity} for the given bean and {@link ConversionService}.
*
* @param entity must not be {@literal null}.
* @param conversionService must not be {@literal null}.
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> AdaptibleEntity<T> forEntity(T entity, ConversionService conversionService) {
Assert.notNull(entity, "Bean must not be null!");
Assert.notNull(conversionService, "ConversionService must not be null!");
if (entity instanceof String) {
return new SimpleEntity(parse(entity.toString()));
}
if (entity instanceof Map) {
return new SimpleEntity((Map<String, Object>) entity);
}
return AdaptibleMappedEntity.of(entity, context, conversionService);
}
public String determineCollectionName(@Nullable Class<?> entityClass) {
if (entityClass == null) {
throw new InvalidDataAccessApiUsageException(
"No class parameter provided, entity collection can't be determined!");
}
return context.getRequiredPersistentEntity(entityClass).getCollection();
}
/**
* Returns the collection name to be used for the given entity.
*
* @param obj can be {@literal null}.
* @return
*/
@Nullable
public String determineEntityCollectionName(@Nullable Object obj) {
return null == obj ? null : determineCollectionName(obj.getClass());
}
public Query getByIdInQuery(Collection<?> entities) {
MultiValueMap<String, Object> byIds = new LinkedMultiValueMap<>();
entities.stream() //
.map(this::forEntity) //
.forEach(it -> byIds.add(it.getIdFieldName(), it.getId()));
Criteria[] criterias = byIds.entrySet().stream() //
.map(it -> Criteria.where(it.getKey()).in(it.getValue())) //
.toArray(Criteria[]::new);
return new Query(criterias.length == 1 ? criterias[0] : new Criteria().orOperator(criterias));
}
/**
* Returns the name of the identifier property. Considers mapping information but falls back to the MongoDB default of
* {@code _id} if no identifier property can be found.
*
* @param type must not be {@literal null}.
* @return
*/
public String getIdPropertyName(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
MongoPersistentEntity<?> persistentEntity = context.getPersistentEntity(type);
if (persistentEntity != null && persistentEntity.getIdProperty() != null) {
return persistentEntity.getRequiredIdProperty().getName();
}
return ID_FIELD;
}
private static Document parse(String source) {
try {
return Document.parse(source);
} catch (JSONParseException | org.bson.json.JsonParseException o_O) {
throw new MappingException("Could not parse given String to save into a JSON document!", o_O);
}
}
/**
* A representation of information about an entity.
*
* @author Oliver Gierke
* @since 2.1
*/
interface Entity<T> {
/**
* Returns the field name of the identifier of the entity.
*
* @return
*/
String getIdFieldName();
/**
* Returns the identifier of the entity.
*
* @return
*/
Object getId();
/**
* Returns the {@link Query} to find the entity by its identifier.
*
* @return
*/
Query getByIdQuery();
/**
* Returns the {@link Query} to find the entity in its current version.
*
* @return
*/
Query getQueryForVersion();
/**
* Maps the backing entity into a {@link MappedDocument} using the given {@link MongoWriter}.
*
* @param writer must not be {@literal null}.
* @return
*/
MappedDocument toMappedDocument(MongoWriter<? super T> writer);
/**
* Asserts that the identifier type is updatable in case its not already set.
*/
default void assertUpdateableIdIfNotSet() {}
/**
* Returns whether the entity is versioned, i.e. if it contains a version property.
*
* @return
*/
default boolean isVersionedEntity() {
return false;
}
/**
* Returns the value of the version if the entity has a version property, {@literal null} otherwise.
*
* @return
*/
@Nullable
Object getVersion();
/**
* Returns the underlying bean.
*
* @return
*/
T getBean();
}
/**
* Information and commands on an entity.
*
* @author Oliver Gierke
* @since 2.1
*/
interface AdaptibleEntity<T> extends Entity<T> {
/**
* Populates the identifier of the backing entity if it has an identifier property and there's no identifier
* currently present.
*
* @param id must not be {@literal null}.
* @return
*/
@Nullable
T populateIdIfNecessary(@Nullable Object id);
/**
* Initializes the version property of the of the current entity if available.
*
* @return the entity with the version property updated if available.
*/
T initializeVersionProperty();
/**
* Increments the value of the version property if available.
*
* @return the entity with the version property incremented if available.
*/
T incrementVersion();
/**
* Returns the current version value if the entity has a version property.
*
* @return the current version or {@literal null} in case it's uninitialized or the entity doesn't expose a version
* property.
*/
@Nullable
Number getVersion();
}
@RequiredArgsConstructor
private static class SimpleEntity<T extends Map<String, Object>> implements AdaptibleEntity<T> {
private final T map;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getIdPropertyName()
*/
@Override
public String getIdFieldName() {
return ID_FIELD;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getId()
*/
@Override
public Object getId() {
return map.get(ID_FIELD);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getByIdQuery()
*/
@Override
public Query getByIdQuery() {
return Query.query(Criteria.where(ID_FIELD).is(map.get(ID_FIELD)));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.MutablePersistableSource#populateIdIfNecessary(java.lang.Object)
*/
@Nullable
@Override
public T populateIdIfNecessary(@Nullable Object id) {
map.put(ID_FIELD, id);
return map;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getQueryForVersion()
*/
@Override
public Query getQueryForVersion() {
throw new MappingException("Cannot query for version on plain Documents!");
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#toMappedDocument(org.springframework.data.mongodb.core.convert.MongoWriter)
*/
@Override
public MappedDocument toMappedDocument(MongoWriter<? super T> writer) {
return MappedDocument.of(map instanceof Document //
? (Document) map //
: new Document(map));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.MutablePersistableSource#initializeVersionProperty()
*/
@Override
public T initializeVersionProperty() {
return map;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.MutablePersistableSource#getVersion()
*/
@Override
@Nullable
public Number getVersion() {
return null;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.MutablePersistableSource#incrementVersion()
*/
@Override
public T incrementVersion() {
return map;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getBean()
*/
@Override
public T getBean() {
return map;
}
}
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
private static class MappedEntity<T> implements Entity<T> {
private final @NonNull MongoPersistentEntity<?> entity;
private final @NonNull IdentifierAccessor idAccessor;
private final @NonNull PersistentPropertyAccessor<T> propertyAccessor;
private static <T> MappedEntity<T> of(T bean,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context) {
MongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(bean.getClass());
IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(bean);
PersistentPropertyAccessor<T> propertyAccessor = entity.getPropertyAccessor(bean);
return new MappedEntity<>(entity, identifierAccessor, propertyAccessor);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getIdPropertyName()
*/
@Override
public String getIdFieldName() {
return entity.getRequiredIdProperty().getFieldName();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getId()
*/
@Override
public Object getId() {
return idAccessor.getRequiredIdentifier();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getByIdQuery()
*/
@Override
public Query getByIdQuery() {
if (!entity.hasIdProperty()) {
throw new MappingException("No id property found for object of type " + entity.getType() + "!");
}
MongoPersistentProperty idProperty = entity.getRequiredIdProperty();
return Query.query(Criteria.where(idProperty.getName()).is(getId()));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getQueryForVersion(java.lang.Object)
*/
@Override
public Query getQueryForVersion() {
MongoPersistentProperty idProperty = entity.getRequiredIdProperty();
MongoPersistentProperty property = entity.getRequiredVersionProperty();
return new Query(Criteria.where(idProperty.getName()).is(getId())//
.and(property.getName()).is(getVersion()));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#toMappedDocument(org.springframework.data.mongodb.core.convert.MongoWriter)
*/
@Override
public MappedDocument toMappedDocument(MongoWriter<? super T> writer) {
T bean = propertyAccessor.getBean();
Document document = new Document();
writer.write(bean, document);
if (document.containsKey(ID_FIELD) && document.get(ID_FIELD) == null) {
document.remove(ID_FIELD);
}
return MappedDocument.of(document);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.Entity#assertUpdateableIdIfNotSet()
*/
public void assertUpdateableIdIfNotSet() {
if (!entity.hasIdProperty()) {
return;
}
MongoPersistentProperty property = entity.getRequiredIdProperty();
Object propertyValue = idAccessor.getIdentifier();
if (propertyValue != null) {
return;
}
if (!MongoSimpleTypes.AUTOGENERATED_ID_TYPES.contains(property.getType())) {
throw new InvalidDataAccessApiUsageException(
String.format("Cannot autogenerate id of type %s for entity of type %s!", property.getType().getName(),
entity.getType().getName()));
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#isVersionedEntity()
*/
@Override
public boolean isVersionedEntity() {
return entity.hasVersionProperty();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getVersion()
*/
@Override
@Nullable
public Object getVersion() {
return propertyAccessor.getProperty(entity.getRequiredVersionProperty());
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getBean()
*/
@Override
public T getBean() {
return propertyAccessor.getBean();
}
}
private static class AdaptibleMappedEntity<T> extends MappedEntity<T> implements AdaptibleEntity<T> {
private final MongoPersistentEntity<?> entity;
private final ConvertingPropertyAccessor<T> propertyAccessor;
private final IdentifierAccessor identifierAccessor;
private AdaptibleMappedEntity(MongoPersistentEntity<?> entity, IdentifierAccessor identifierAccessor,
ConvertingPropertyAccessor<T> propertyAccessor) {
super(entity, identifierAccessor, propertyAccessor);
this.entity = entity;
this.propertyAccessor = propertyAccessor;
this.identifierAccessor = identifierAccessor;
}
private static <T> AdaptibleEntity<T> of(T bean,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context,
ConversionService conversionService) {
MongoPersistentEntity<?> entity = context.getRequiredPersistentEntity(bean.getClass());
IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(bean);
PersistentPropertyAccessor<T> propertyAccessor = entity.getPropertyAccessor(bean);
return new AdaptibleMappedEntity<>(entity, identifierAccessor,
new ConvertingPropertyAccessor<>(propertyAccessor, conversionService));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity#populateIdIfNecessary(java.lang.Object)
*/
@Nullable
@Override
public T populateIdIfNecessary(@Nullable Object id) {
if (id == null) {
return null;
}
T bean = propertyAccessor.getBean();
MongoPersistentProperty idProperty = entity.getIdProperty();
if (idProperty == null) {
return bean;
}
if (identifierAccessor.getIdentifier() != null) {
return bean;
}
propertyAccessor.setProperty(idProperty, id);
return propertyAccessor.getBean();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.MappedEntity#getVersion()
*/
@Override
@Nullable
public Number getVersion() {
MongoPersistentProperty versionProperty = entity.getRequiredVersionProperty();
return propertyAccessor.getProperty(versionProperty, Number.class);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity#initializeVersionProperty()
*/
@Override
public T initializeVersionProperty() {
if (!entity.hasVersionProperty()) {
return propertyAccessor.getBean();
}
propertyAccessor.setProperty(entity.getRequiredVersionProperty(), 0);
return propertyAccessor.getBean();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity#incrementVersion()
*/
@Override
public T incrementVersion() {
MongoPersistentProperty versionProperty = entity.getRequiredVersionProperty();
Number version = getVersion();
Number nextVersion = version == null ? 0 : version.longValue() + 1;
propertyAccessor.setProperty(versionProperty, nextVersion);
return propertyAccessor.getBean();
}
}
}

View File

@@ -119,11 +119,11 @@ class ExecutableAggregationOperationSupport implements ExecutableAggregationOper
TypedAggregation<?> typedAggregation = (TypedAggregation<?>) aggregation;
if (typedAggregation.getInputType() != null) {
return template.determineCollectionName(typedAggregation.getInputType());
return template.getCollectionName(typedAggregation.getInputType());
}
}
return template.determineCollectionName(domainType);
return template.getCollectionName(domainType);
}
}
}

View File

@@ -230,7 +230,7 @@ class ExecutableFindOperationSupport implements ExecutableFindOperation {
}
private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType);
return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType);
}
private String asString() {

View File

@@ -129,7 +129,7 @@ class ExecutableInsertOperationSupport implements ExecutableInsertOperation {
}
private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType);
return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType);
}
}
}

View File

@@ -15,10 +15,11 @@
*/
package org.springframework.data.mongodb.core;
import java.util.List;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.List;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.lang.Nullable;
@@ -67,8 +68,9 @@ class ExecutableMapReduceOperationSupport implements ExecutableMapReduceOperatio
private final @Nullable String reduceFunction;
private final @Nullable MapReduceOptions options;
ExecutableMapReduceSupport(MongoTemplate template, Class<?> domainType, Class<T> returnType, @Nullable String collection,
Query query, @Nullable String mapFunction, @Nullable String reduceFunction, @Nullable MapReduceOptions options) {
ExecutableMapReduceSupport(MongoTemplate template, Class<?> domainType, Class<T> returnType,
@Nullable String collection, Query query, @Nullable String mapFunction, @Nullable String reduceFunction,
@Nullable MapReduceOptions options) {
this.template = template;
this.domainType = domainType;
@@ -169,7 +171,7 @@ class ExecutableMapReduceOperationSupport implements ExecutableMapReduceOperatio
}
private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType);
return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType);
}
}
}

View File

@@ -123,7 +123,7 @@ class ExecutableRemoveOperationSupport implements ExecutableRemoveOperation {
}
private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType);
return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType);
}
}
}

View File

@@ -221,7 +221,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation {
}
private String getCollectionName() {
return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType);
return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType);
}
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2018 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
*
* http://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 lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Collection;
import java.util.List;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.util.StreamUtils;
import com.mongodb.client.model.Filters;
/**
* A MongoDB document in its mapped state. I.e. after a source document has been mapped using mapping information of the
* entity the source document was supposed to represent.
*
* @author Oliver Gierke
* @since 2.1
*/
@RequiredArgsConstructor(staticName = "of")
public class MappedDocument {
private static final String ID_FIELD = "_id";
private static final Document ID_ONLY_PROJECTION = new Document(ID_FIELD, 1);
private final @Getter Document document;
public static Document getIdOnlyProjection() {
return ID_ONLY_PROJECTION;
}
public static Document getIdIn(Collection<?> ids) {
return new Document(ID_FIELD, new Document("$in", ids));
}
public static List<Object> toIds(Collection<Document> documents) {
return documents.stream()//
.map(it -> it.get(ID_FIELD))//
.collect(StreamUtils.toUnmodifiableList());
}
public boolean hasId() {
return document.containsKey(ID_FIELD);
}
public boolean hasNonNullId() {
return hasId() && document.get(ID_FIELD) != null;
}
public Object getId() {
return document.get(ID_FIELD);
}
public <T> T getId(Class<T> type) {
return document.get(ID_FIELD, type);
}
public boolean isIdPresent(Class<?> type) {
return type.isInstance(getId());
}
public Bson getIdFilter() {
return Filters.eq(ID_FIELD, document.get(ID_FIELD));
}
public Update updateWithoutId() {
return Update.fromDocument(document, ID_FIELD);
}
}

View File

@@ -30,5 +30,4 @@ import com.mongodb.reactivestreams.client.MongoCollection;
public interface ReactiveCollectionCallback<T> {
Publisher<T> doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException;
}

View File

@@ -15,7 +15,6 @@
*/
package org.springframework.data.mongodb.core;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.SerializationUtils.*;
import lombok.AccessLevel;
@@ -24,9 +23,20 @@ import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -49,32 +59,39 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.convert.ConversionService;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.annotation.Id;
import org.springframework.data.convert.EntityReader;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.Metric;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.MappingContextEvent;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.PrefixingDelegatingAggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.DbRefProxyHandler;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DbRefResolverCallback;
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.QueryMapper;
import org.springframework.data.mongodb.core.convert.UpdateMapper;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.JsonSchemaMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
@@ -112,7 +129,6 @@ import org.springframework.data.mongodb.util.MongoClientVersion;
import org.springframework.data.projection.ProjectionInformation;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.Pair;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -129,6 +145,15 @@ import com.mongodb.Mongo;
import com.mongodb.MongoException;
import com.mongodb.ReadPreference;
import com.mongodb.WriteConcern;
import com.mongodb.client.model.CountOptions;
import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.client.model.DeleteOptions;
import com.mongodb.client.model.FindOneAndDeleteOptions;
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.ValidationOptions;
import com.mongodb.client.model.*;
import com.mongodb.client.model.changestream.FullDocument;
import com.mongodb.client.result.DeleteResult;
@@ -143,8 +168,6 @@ import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.reactivestreams.client.MongoDatabase;
import com.mongodb.reactivestreams.client.Success;
import com.mongodb.util.JSONParseException;
import reactor.util.function.Tuples;
/**
* Primary implementation of {@link ReactiveMongoOperations}. It simplifies the use of Reactive MongoDB usage and helps
@@ -165,7 +188,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
public static final DbRefResolver NO_OP_REF_RESOLVER = NoOpDbRefResolver.INSTANCE;
private static final Logger LOGGER = LoggerFactory.getLogger(ReactiveMongoTemplate.class);
private static final String ID_FIELD = "_id";
private static final WriteResultChecking DEFAULT_WRITE_RESULT_CHECKING = WriteResultChecking.NONE;
private static final Collection<Class<?>> ITERABLE_CLASSES;
@@ -189,6 +211,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private final JsonSchemaMapper schemaMapper;
private final SpelAwareProxyProjectionFactory projectionFactory;
private final ApplicationListener<MappingContextEvent<?, ?>> indexCreatorListener;
private final EntityOperations operations;
private @Nullable WriteConcern writeConcern;
private WriteConcernResolver writeConcernResolver = DefaultWriteConcernResolver.INSTANCE;
@@ -253,6 +276,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
// 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.mappingContext);
// We create indexes based on mapping events
if (this.mappingContext instanceof MongoMappingContext) {
@@ -279,6 +303,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
this.indexCreator = that.indexCreator;
this.indexCreatorListener = that.indexCreatorListener;
this.mappingContext = that.mappingContext;
this.operations = that.operations;
}
private void onCheckForIndexes(MongoPersistentEntity<?> entity, Consumer<Throwable> subscriptionExceptionHandler) {
@@ -820,10 +845,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
*/
public <T> Mono<T> findById(Object id, Class<T> entityClass, String collectionName) {
MongoPersistentEntity<?> persistentEntity = mappingContext.getPersistentEntity(entityClass);
MongoPersistentProperty idProperty = persistentEntity != null ? persistentEntity.getIdProperty() : null;
String idKey = idProperty == null ? ID_FIELD : idProperty.getName();
String idKey = operations.getIdPropertyName(entityClass);
return doFindOne(collectionName, new Document(idKey, id), null, entityClass, null);
}
@@ -855,8 +877,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), entity);
String mappedFieldName = queryMapper.getMappedFields(new Document(field, 1), entity).keySet().iterator().next();
Class<T> mongoDriverCompatibleType = mongoDatabaseFactory.getCodecFor(resultClass).map(Codec::getEncoderClass)
.orElse((Class) BsonValue.class);
Class<T> mongoDriverCompatibleType = mongoDatabaseFactory.getCodecFor(resultClass) //
.map(Codec::getEncoderClass) //
.orElse((Class<T>) BsonValue.class);
Flux<?> result = execute(collectionName, collection -> {
@@ -1006,11 +1029,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#geoNear(org.springframework.data.mongodb.core.query.NearQuery, java.lang.Class, java.lang.String)
*/
@Override
@SuppressWarnings("unchecked")
public <T> Flux<GeoResult<T>> geoNear(NearQuery near, Class<T> entityClass, String collectionName) {
return geoNear(near, entityClass, collectionName, entityClass);
}
@SuppressWarnings("unchecked")
protected <T> Flux<GeoResult<T>> geoNear(NearQuery near, Class<?> entityClass, String collectionName,
Class<T> returnType) {
@@ -1045,11 +1068,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return executeCommand(command, this.readPreference).flatMapMany(document -> {
List<Document> l = document.get("results", List.class);
if (l == null) {
return Flux.empty();
}
return Flux.fromIterable(l);
List<Document> results = document.get("results", List.class);
return results == null ? Flux.empty() : Flux.fromIterable(results);
}).skip(near.getSkip() != null ? near.getSkip() : 0).map(callback::doWith);
});
}
@@ -1122,7 +1144,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Document mappedFields = queryMapper.getMappedFields(query.getFieldsObject(), entity);
Document mappedSort = queryMapper.getMappedSort(query.getSortObject(), entity);
Document mappedReplacement = toDocument(replacement, this.mongoConverter);
Document mappedReplacement = operations.forEntity(replacement).toMappedDocument(this.mongoConverter).getDocument();
return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort,
query.getCollation().map(Collation::toMongoCollation).orElse(null), entityType, mappedReplacement, options,
@@ -1253,16 +1275,18 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return Mono.defer(() -> {
T toSave = (T) initializeVersionProperty(objectToSave);
AdaptibleEntity<T> entity = operations.forEntity(objectToSave, mongoConverter.getConversionService());
T toSave = entity.initializeVersionProperty();
maybeEmitEvent(new BeforeConvertEvent<>(toSave, collectionName));
Document dbDoc = toDocument(toSave, writer);
Document dbDoc = entity.toMappedDocument(writer).getDocument();
maybeEmitEvent(new BeforeSaveEvent<>(toSave, dbDoc, collectionName));
Mono<T> afterInsert = insertDBObject(collectionName, dbDoc, toSave.getClass()).map(id -> {
T saved = (T) populateIdIfNecessary(toSave, id);
T saved = entity.populateIdIfNecessary(id);
maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName));
return saved;
});
@@ -1327,19 +1351,21 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(writer, "MongoWriter must not be null!");
Mono<List<Tuple2<T, Document>>> prepareDocuments = Flux.fromIterable(batchToSave)
.map(o -> {
Mono<List<Tuple2<AdaptibleEntity<T>, Document>>> prepareDocuments = Flux.fromIterable(batchToSave).map(o -> {
T toSave = (T) initializeVersionProperty(o);
maybeEmitEvent(new BeforeConvertEvent<>(toSave, collectionName));
AdaptibleEntity<T> entity = operations.forEntity(o, mongoConverter.getConversionService());
T toSave = entity.initializeVersionProperty();
Document dbDoc = toDocument(toSave, writer);
BeforeConvertEvent<T> event = new BeforeConvertEvent<>(toSave, collectionName);
toSave = maybeEmitEvent(event).getSource();
maybeEmitEvent(new BeforeSaveEvent<>(toSave, dbDoc, collectionName));
return Tuples.of(toSave, dbDoc);
}).collectList();
Document dbDoc = entity.toMappedDocument(writer).getDocument();
Flux<Tuple2<T, Document>> insertDocuments = prepareDocuments.flatMapMany(tuples -> {
maybeEmitEvent(new BeforeSaveEvent<>(toSave, dbDoc, collectionName));
return Tuples.of(entity, dbDoc);
}).collectList();
Flux<Tuple2<AdaptibleEntity<T>, Document>> insertDocuments = prepareDocuments.flatMapMany(tuples -> {
List<Document> dbObjects = tuples.stream().map(Tuple2::getT2).collect(Collectors.toList());
@@ -1348,7 +1374,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return insertDocuments.map(tuple -> {
T saved = (T) populateIdIfNecessary(tuple.getT1(), tuple.getT2().get(ID_FIELD));
Object id = MappedDocument.of(tuple.getT2()).getId();
T saved = tuple.getT1().populateIdIfNecessary(id);
maybeEmitEvent(new AfterSaveEvent<>(saved, tuple.getT2(), collectionName));
return saved;
});
@@ -1409,48 +1437,33 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private <T> Mono<T> doSaveVersioned(T objectToSave, MongoPersistentEntity<?> entity, String collectionName) {
AdaptibleEntity<T> forEntity = operations.forEntity(objectToSave, mongoConverter.getConversionService());
return createMono(collectionName, collection -> {
ConvertingPropertyAccessor convertingAccessor = new ConvertingPropertyAccessor(
entity.getPropertyAccessor(objectToSave), mongoConverter.getConversionService());
MongoPersistentProperty idProperty = entity.getRequiredIdProperty();
MongoPersistentProperty versionProperty = entity.getRequiredVersionProperty();
Object version = convertingAccessor.getProperty(versionProperty);
Number versionNumber = convertingAccessor.getProperty(versionProperty, Number.class);
Number versionNumber = forEntity.getVersion();
// Fresh instance -> initialize version property
if (version == null) {
if (versionNumber == null) {
return doInsert(collectionName, objectToSave, mongoConverter);
}
assertUpdateableIdIfNotSet(objectToSave);
forEntity.assertUpdateableIdIfNotSet();
// Create query for entity with the id and old version
Object id = convertingAccessor.getProperty(idProperty);
Query query = new Query(Criteria.where(idProperty.getName()).is(id).and(versionProperty.getName()).is(version));
Query query = forEntity.getQueryForVersion();
if (versionNumber == null) {
versionNumber = 0;
}
// Bump version number
convertingAccessor.setProperty(versionProperty, versionNumber.longValue() + 1);
T toSave = forEntity.incrementVersion();
T toSave = (T) convertingAccessor.getBean();
BeforeConvertEvent<T> event = new BeforeConvertEvent<>(toSave, collectionName);
T afterEvent = ReactiveMongoTemplate.this.maybeEmitEvent(event).getSource();
ReactiveMongoTemplate.this.maybeEmitEvent(new BeforeConvertEvent<T>(toSave, collectionName));
MappedDocument mapped = operations.forEntity(toSave).toMappedDocument(mongoConverter);
Document document = mapped.getDocument();
Document document = ReactiveMongoTemplate.this.toDocument(toSave, mongoConverter);
ReactiveMongoTemplate.this.maybeEmitEvent(new BeforeSaveEvent<>(afterEvent, document, collectionName));
ReactiveMongoTemplate.this.maybeEmitEvent(new BeforeSaveEvent<>(toSave, document, collectionName));
Update update = Update.fromDocument(document, ID_FIELD);
return doUpdate(collectionName, query, update, toSave.getClass(), false, false).map(updateResult -> {
maybeEmitEvent(new AfterSaveEvent<>(toSave, document, collectionName));
return toSave;
});
return doUpdate(collectionName, query, mapped.updateWithoutId(), afterEvent.getClass(), false, false)
.map(updateResult -> maybeEmitEvent(new AfterSaveEvent<T>(afterEvent, document, collectionName)).getSource());
});
}
@@ -1460,15 +1473,16 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return createMono(collectionName, collection -> {
maybeEmitEvent(new BeforeConvertEvent<>(objectToSave, collectionName));
Document dbDoc = toDocument(objectToSave, writer);
maybeEmitEvent(new BeforeSaveEvent<>(objectToSave, dbDoc, collectionName));
T toSave = maybeEmitEvent(new BeforeConvertEvent<T>(objectToSave, collectionName)).getSource();
return saveDocument(collectionName, dbDoc, objectToSave.getClass()).map(id -> {
AdaptibleEntity<T> entity = operations.forEntity(toSave, mongoConverter.getConversionService());
Document dbDoc = entity.toMappedDocument(writer).getDocument();
maybeEmitEvent(new BeforeSaveEvent<T>(toSave, dbDoc, collectionName));
T saved = (T) populateIdIfNecessary(objectToSave, id);
maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName));
return saved;
return saveDocument(collectionName, dbDoc, toSave.getClass()).map(id -> {
T saved = entity.populateIdIfNecessary(id);
return maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName)).getSource();
});
});
}
@@ -1479,7 +1493,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
LOGGER.debug("Inserting Document containing fields: " + dbDoc.keySet() + " in collection: " + collectionName);
}
final Document document = new Document(dbDoc);
Document document = new Document(dbDoc);
Flux<Success> execute = execute(collectionName, collection -> {
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT, collectionName, entityClass,
@@ -1491,7 +1506,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return collectionToUse.insertOne(document);
});
return Flux.from(execute).last().map(success -> document.get(ID_FIELD));
return Flux.from(execute).last().map(success -> MappedDocument.of(document).getId());
}
protected Flux<ObjectId> insertDocumentList(final String collectionName, final List<Document> dbDocList) {
@@ -1516,12 +1531,14 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
documents.addAll(toDocuments(dbDocList));
return collectionToUse.insertMany(documents);
}).flatMap(s -> {
List<Document> documentsWithIds = documents.stream()
.filter(document -> document.get(ID_FIELD) instanceof ObjectId).collect(Collectors.toList());
return Flux.fromIterable(documentsWithIds);
}).map(document -> document.get(ID_FIELD, ObjectId.class));
return Flux.fromStream(documents.stream() //
.map(MappedDocument::of) //
.filter(it -> it.isIdPresent(ObjectId.class)) //
.map(it -> it.getId(ObjectId.class)));
});
}
private MongoCollection<Document> prepareCollection(MongoCollection<Document> collection,
@@ -1546,24 +1563,19 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.SAVE, collectionName, entityClass,
document, null);
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
MappedDocument mapped = MappedDocument.of(document);
Publisher<?> publisher;
if (!document.containsKey(ID_FIELD)) {
if (writeConcernToUse == null) {
publisher = collection.insertOne(document);
} else {
publisher = collection.withWriteConcern(writeConcernToUse).insertOne(document);
}
} else if (writeConcernToUse == null) {
publisher = collection.replaceOne(Filters.eq(ID_FIELD, document.get(ID_FIELD)), document,
new ReplaceOptions().upsert(true));
} else {
publisher = collection.withWriteConcern(writeConcernToUse)
.replaceOne(Filters.eq(ID_FIELD, document.get(ID_FIELD)), document, new ReplaceOptions().upsert(true));
}
MongoCollection<Document> collectionToUse = writeConcernToUse == null //
? collection //
: collection.withWriteConcern(writeConcernToUse);
return Mono.from(publisher).map(o -> document.get(ID_FIELD));
Publisher<?> publisher = !mapped.hasId() //
? collectionToUse.insertOne(document) //
: collectionToUse.replaceOne(mapped.getIdFilter(), document, new ReplaceOptions().upsert(true));
return Mono.from(publisher).map(o -> mapped.getId());
});
}
/*
@@ -1639,7 +1651,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return doUpdate(collectionName, query, update, entityClass, false, true);
}
protected Mono<UpdateResult> doUpdate(final String collectionName, @Nullable Query query, @Nullable Update update,
protected Mono<UpdateResult> doUpdate(final String collectionName, Query query, @Nullable Update update,
@Nullable Class<?> entityClass, final boolean upsert, final boolean multi) {
MongoPersistentEntity<?> entity = entityClass == null ? null : getPersistentEntity(entityClass);
@@ -1648,7 +1660,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
increaseVersionForUpdateIfNecessary(entity, update);
Document queryObj = query == null ? new Document() : queryMapper.getMappedObject(query.getQueryObject(), entity);
Document queryObj = queryMapper.getMappedObject(query.getQueryObject(), entity);
Document updateObj = update == null ? new Document()
: updateMapper.getMappedObject(update.getUpdateObject(), entity);
@@ -1742,7 +1754,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(object, "Object must not be null!");
return remove(getIdQueryFor(object), object.getClass());
return remove(operations.forEntity(object).getByIdQuery(), object.getClass());
}
/*
@@ -1754,72 +1766,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(object, "Object must not be null!");
Assert.hasText(collectionName, "Collection name must not be null or empty!");
return doRemove(collectionName, getIdQueryFor(object), object.getClass());
}
/**
* Returns {@link Entry} containing the field name of the id property as {@link Entry#getKey()} and the {@link Id}s
* property value as its {@link Entry#getValue()}.
*
* @param object
* @return
*/
private Pair<String, Object> extractIdPropertyAndValue(Object object) {
Assert.notNull(object, "Id cannot be extracted from 'null'.");
Assert.notNull(object, "Id cannot be extracted from 'null'.");
Class<?> objectType = object.getClass();
if (object instanceof Document) {
return Pair.of(ID_FIELD, ((Document) object).get(ID_FIELD));
}
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(objectType);
if (entity != null && entity.hasIdProperty()) {
MongoPersistentProperty idProperty = entity.getIdProperty();
return Pair.of(idProperty.getFieldName(), entity.getPropertyAccessor(object).getProperty(idProperty));
}
throw new MappingException("No id property found for object of type " + objectType);
}
/**
* Returns a {@link Query} for the given entity by its id.
*
* @param object must not be {@literal null}.
* @return
*/
private Query getIdQueryFor(Object object) {
Pair<String, Object> id = extractIdPropertyAndValue(object);
return new Query(where(id.getFirst()).is(id.getSecond()));
}
/**
* Returns a {@link Query} for the given entities by their ids.
*
* @param objects must not be {@literal null} or {@literal empty}.
* @return
*/
private Query getIdInQueryFor(Collection<?> objects) {
Assert.notEmpty(objects, "Cannot create Query for empty collection.");
Iterator<?> it = objects.iterator();
Pair<String, Object> firstEntry = extractIdPropertyAndValue(it.next());
ArrayList<Object> ids = new ArrayList<>(objects.size());
ids.add(firstEntry.getSecond());
while (it.hasNext()) {
ids.add(extractIdPropertyAndValue(it.next()).getSecond());
}
return new Query(where(firstEntry.getFirst()).in(ids));
return doRemove(collectionName, operations.forEntity(object).getByIdQuery(), object.getClass());
}
private void assertUpdateableIdIfNotSet(Object value) {
@@ -1902,13 +1849,14 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
FindPublisher<Document> cursor = new QueryFindPublisherPreparer(query, entityClass)
.prepare(collection.find(removeQuey)) //
.projection(new Document(ID_FIELD, 1));
.projection(MappedDocument.getIdOnlyProjection());
return Flux.from(cursor) //
.map(doc -> doc.get(ID_FIELD)) //
.map(MappedDocument::of) //
.map(MappedDocument::getId) //
.collectList() //
.flatMapMany(val -> {
return collectionToUse.deleteMany(new Document(ID_FIELD, new Document("$in", val)), deleteOptions);
return collectionToUse.deleteMany(MappedDocument.getIdIn(val), deleteOptions);
});
} else {
return collectionToUse.deleteMany(removeQuey, deleteOptions);
@@ -2019,8 +1967,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
.map(publisher::startAtOperationTime).orElse(publisher);
publisher = publisher.fullDocument(options.getFullDocumentLookup().orElse(fullDocument));
return Flux.from(publisher).map(document -> new ChangeStreamEvent<>(document, targetType, getConverter()));
}
return Flux.from(
publisher ).map(document -> new ChangeStreamEvent<>(document, targetType, getConverter()));
}
List<Document> prepareFilter(ChangeStreamOptions options) {
@@ -2223,7 +2172,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Flux<T> flux = find(query, entityClass, collectionName);
return Flux.from(flux).collectList()
.flatMapMany(list -> Flux.from(remove(getIdInQueryFor(list), entityClass, collectionName))
.flatMapMany(list -> Flux.from(remove(operations.getByIdInQuery(list), entityClass, collectionName))
.flatMap(deleteResult -> Flux.fromIterable(list)));
}
@@ -2509,50 +2458,13 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
});
}
protected <T> void maybeEmitEvent(MongoMappingEvent<T> event) {
protected <E extends MongoMappingEvent<T>, T> E maybeEmitEvent(E event) {
if (null != eventPublisher) {
eventPublisher.publishEvent(event);
}
}
/**
* Populates the id property of the saved object, if it's not set already.
*
* @param savedObject
* @param id
*/
@SuppressWarnings("unchecked")
private Object populateIdIfNecessary(Object savedObject, @Nullable Object id) {
if (id == null) {
return null;
}
if (savedObject instanceof Map) {
Map<String, Object> map = (Map<String, Object>) savedObject;
map.put(ID_FIELD, id);
return map;
}
MongoPersistentProperty idProp = getIdPropertyFor(savedObject.getClass());
if (idProp == null) {
return savedObject;
}
ConversionService conversionService = mongoConverter.getConversionService();
MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(savedObject.getClass());
PersistentPropertyAccessor accessor = entity.getPropertyAccessor(savedObject);
if (accessor.getProperty(idProp) != null) {
return accessor.getBean();
}
new ConvertingPropertyAccessor(accessor, conversionService).setProperty(idProp, id);
return accessor.getBean();
return event;
}
private MongoCollection<Document> getAndPrepareCollection(MongoDatabase db, String collectionName) {
@@ -2583,10 +2495,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
* @param collection
*/
protected MongoCollection<Document> prepareCollection(MongoCollection<Document> collection) {
if (this.readPreference != null) {
return collection.withReadPreference(readPreference);
}
return collection;
return this.readPreference != null ? collection.withReadPreference(readPreference) : collection;
}
/**
@@ -2782,49 +2691,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return queryMapper.getMappedSort(query.getSortObject(), mappingContext.getPersistentEntity(type));
}
/**
* @param objectToSave
* @param writer
* @return
*/
private <T> Document toDocument(T objectToSave, MongoWriter<T> writer) {
if (objectToSave instanceof Document) {
return (Document) objectToSave;
}
if (!(objectToSave instanceof String)) {
Document dbDoc = new Document();
writer.write(objectToSave, dbDoc);
if (dbDoc.containsKey(ID_FIELD) && dbDoc.get(ID_FIELD) == null) {
dbDoc.remove(ID_FIELD);
}
return dbDoc;
} else {
try {
return Document.parse((String) objectToSave);
} catch (JSONParseException | org.bson.json.JsonParseException e) {
throw new MappingException("Could not parse given String to save into a JSON document!", e);
}
}
}
private Object initializeVersionProperty(Object entity) {
MongoPersistentEntity<?> mongoPersistentEntity = getPersistentEntity(entity.getClass());
if (mongoPersistentEntity != null && mongoPersistentEntity.hasVersionProperty()) {
ConvertingPropertyAccessor accessor = new ConvertingPropertyAccessor(
mongoPersistentEntity.getPropertyAccessor(entity), mongoConverter.getConversionService());
accessor.setProperty(mongoPersistentEntity.getRequiredVersionProperty(), 0);
return accessor.getBean();
}
return entity;
}
// Callback implementations
/**
@@ -3135,13 +3001,15 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private final @NonNull String collectionName;
@Nullable
@SuppressWarnings("unchecked")
public T doWith(@Nullable Document object) {
if (object == null) {
return null;
}
Class<?> typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) ? entityType
Class<?> typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) //
? entityType //
: targetType;
if (null != object) {
@@ -3208,6 +3076,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
this.type = type;
}
@SuppressWarnings("deprecation")
public <T> FindPublisher<T> prepare(FindPublisher<T> findPublisher) {
if (query == null) {
@@ -3226,12 +3095,15 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}
try {
if (query.getSkip() > 0) {
findPublisherToUse = findPublisherToUse.skip((int) query.getSkip());
}
if (query.getLimit() > 0) {
findPublisherToUse = findPublisherToUse.limit(query.getLimit());
}
if (!ObjectUtils.isEmpty(query.getSortObject())) {
Document sort = type != null ? getMappedSortObject(query, type) : query.getSortObject();
findPublisherToUse = findPublisherToUse.sort(sort);

View File

@@ -147,7 +147,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
*/
public void setTypeMapper(@Nullable MongoTypeMapper typeMapper) {
this.typeMapper = typeMapper == null
? new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, mappingContext) : typeMapper;
? new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, mappingContext)
: typeMapper;
}
/*
@@ -272,7 +273,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity);
S instance = instantiator.createInstance(entity, provider);
PersistentPropertyAccessor accessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(instance),
PersistentPropertyAccessor<S> accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance),
conversionService);
MongoPersistentProperty idProperty = entity.getIdProperty();
@@ -296,7 +297,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
MappingMongoConverter.this);
readProperties(entity, accessor, idProperty, documentAccessor, valueProvider, callback);
return (S) accessor.getBean();
return accessor.getBean();
}
private Object readIdValue(ObjectPath path, DefaultSpELExpressionEvaluator evaluator,
@@ -557,7 +558,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
MongoPersistentEntity<?> entity = isSubtype(prop.getType(), obj.getClass())
? mappingContext.getRequiredPersistentEntity(obj.getClass()) : mappingContext.getRequiredPersistentEntity(type);
? mappingContext.getRequiredPersistentEntity(obj.getClass())
: mappingContext.getRequiredPersistentEntity(type);
Object existingValue = accessor.get(prop);
Document document = existingValue instanceof Document ? (Document) existingValue : new Document();
@@ -779,7 +781,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
return conversions.hasCustomWriteTarget(key.getClass(), String.class)
? (String) getPotentiallyConvertedSimpleWrite(key) : key.toString();
? (String) getPotentiallyConvertedSimpleWrite(key)
: key.toString();
}
/**
@@ -1481,7 +1484,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
List<Document> referencedRawDocuments = dbrefs.size() == 1
? Collections.singletonList(readRef(dbrefs.iterator().next())) : bulkReadRefs(dbrefs);
? Collections.singletonList(readRef(dbrefs.iterator().next()))
: bulkReadRefs(dbrefs);
String collectionName = dbrefs.iterator().next().getCollectionName();
List<T> targeList = new ArrayList<>(dbrefs.size());

View File

@@ -16,8 +16,7 @@
package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
@@ -78,13 +77,13 @@ public class ExecutableAggregationOperationSupportUnitTests {
@Test // DATAMONGO-1563
public void aggregateWithUntypedAggregation() {
when(template.determineCollectionName(any(Class.class))).thenReturn("person");
when(template.getCollectionName(any(Class.class))).thenReturn("person");
opSupport.aggregateAndReturn(Person.class).by(newAggregation(project("foo"))).all();
ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
verify(template).determineCollectionName(captor.capture());
verify(template).getCollectionName(captor.capture());
verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture());
assertThat(captor.getAllValues()).containsExactly(Person.class, Person.class);
@@ -93,13 +92,13 @@ public class ExecutableAggregationOperationSupportUnitTests {
@Test // DATAMONGO-1563
public void aggregateWithTypeAggregation() {
when(template.determineCollectionName(any(Class.class))).thenReturn("person");
when(template.getCollectionName(any(Class.class))).thenReturn("person");
opSupport.aggregateAndReturn(Jedi.class).by(newAggregation(Person.class, project("foo"))).all();
ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
verify(template).determineCollectionName(captor.capture());
verify(template).getCollectionName(captor.capture());
verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture());
assertThat(captor.getAllValues()).containsExactly(Person.class, Jedi.class);
@@ -118,13 +117,13 @@ public class ExecutableAggregationOperationSupportUnitTests {
@Test // DATAMONGO-1563
public void aggregateStreamWithUntypedAggregation() {
when(template.determineCollectionName(any(Class.class))).thenReturn("person");
when(template.getCollectionName(any(Class.class))).thenReturn("person");
opSupport.aggregateAndReturn(Person.class).by(newAggregation(project("foo"))).stream();
ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
verify(template).determineCollectionName(captor.capture());
verify(template).getCollectionName(captor.capture());
verify(template).aggregateStream(any(Aggregation.class), eq("person"), captor.capture());
assertThat(captor.getAllValues()).containsExactly(Person.class, Person.class);
@@ -133,13 +132,13 @@ public class ExecutableAggregationOperationSupportUnitTests {
@Test // DATAMONGO-1563
public void aggregateStreamWithTypeAggregation() {
when(template.determineCollectionName(any(Class.class))).thenReturn("person");
when(template.getCollectionName(any(Class.class))).thenReturn("person");
opSupport.aggregateAndReturn(Jedi.class).by(newAggregation(Person.class, project("foo"))).stream();
ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
verify(template).determineCollectionName(captor.capture());
verify(template).getCollectionName(captor.capture());
verify(template).aggregateStream(any(Aggregation.class), eq("person"), captor.capture());
assertThat(captor.getAllValues()).containsExactly(Person.class, Jedi.class);

View File

@@ -16,10 +16,8 @@
package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.anyList;
import lombok.Data;
@@ -56,7 +54,7 @@ public class ExecutableInsertOperationSupportUnitTests {
public void setUp() {
when(template.bulkOps(any(), any(), any())).thenReturn(bulkOperations);
when(template.determineCollectionName(any(Class.class))).thenReturn(STAR_WARS);
when(template.getCollectionName(any(Class.class))).thenReturn(STAR_WARS);
when(bulkOperations.insert(anyList())).thenReturn(bulkOperations);
ops = new ExecutableInsertOperationSupport(template);
@@ -88,7 +86,7 @@ public class ExecutableInsertOperationSupportUnitTests {
ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
verify(template).determineCollectionName(captor.capture());
verify(template).getCollectionName(captor.capture());
verify(template).insert(eq(luke), eq(STAR_WARS));
assertThat(captor.getAllValues()).containsExactly(Person.class);
@@ -99,7 +97,7 @@ public class ExecutableInsertOperationSupportUnitTests {
ops.insert(Person.class).inCollection(STAR_WARS).one(luke);
verify(template, never()).determineCollectionName(any(Class.class));
verify(template, never()).getCollectionName(any(Class.class));
verify(template).insert(eq(luke), eq(STAR_WARS));
}
@@ -108,7 +106,7 @@ public class ExecutableInsertOperationSupportUnitTests {
ops.insert(Person.class).all(Arrays.asList(luke, han));
verify(template).determineCollectionName(any(Class.class));
verify(template).getCollectionName(any(Class.class));
verify(template).insert(anyList(), eq(STAR_WARS));
}
@@ -119,7 +117,7 @@ public class ExecutableInsertOperationSupportUnitTests {
ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
verify(template).determineCollectionName(any(Class.class));
verify(template).getCollectionName(any(Class.class));
verify(template).bulkOps(eq(BulkMode.ORDERED), captor.capture(), eq(STAR_WARS));
verify(bulkOperations).insert(anyList());
verify(bulkOperations).execute();
@@ -132,7 +130,7 @@ public class ExecutableInsertOperationSupportUnitTests {
ArgumentCaptor<Class> captor = ArgumentCaptor.forClass(Class.class);
verify(template).determineCollectionName(any(Class.class));
verify(template).getCollectionName(any(Class.class));
verify(template).bulkOps(eq(BulkMode.UNORDERED), captor.capture(), eq(STAR_WARS));
verify(bulkOperations).insert(anyList());
verify(bulkOperations).execute();

View File

@@ -15,9 +15,7 @@
*/
package org.springframework.data.mongodb.core;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import lombok.AllArgsConstructor;
@@ -55,7 +53,7 @@ public class ExecutableMapReduceOperationSupportUnitTests {
@Before
public void setUp() {
when(template.determineCollectionName(eq(Person.class))).thenReturn(STAR_WARS);
when(template.getCollectionName(eq(Person.class))).thenReturn(STAR_WARS);
mapReduceOpsSupport = new ExecutableMapReduceOperationSupport(template);
}

View File

@@ -30,16 +30,28 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.Value;
import lombok.experimental.Wither;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Instant;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import lombok.experimental.Wither;
import org.bson.types.ObjectId;
import org.hamcrest.collection.IsMapContaining;
import org.joda.time.DateTime;
@@ -58,8 +70,10 @@ import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.annotation.Version;
import org.springframework.data.auditing.IsNewAwareAuditingHandler;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
@@ -79,6 +93,7 @@ import org.springframework.data.mongodb.core.index.IndexInfo;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener;
import org.springframework.data.mongodb.core.mapping.event.AuditingEventListener;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent;
import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent;
import org.springframework.data.mongodb.core.query.BasicQuery;
@@ -140,6 +155,8 @@ public class MongoTemplateTests {
this.context = context;
context.addApplicationListener(new PersonWithIdPropertyOfTypeUUIDListener());
context.addApplicationListener(
new AuditingEventListener(() -> new IsNewAwareAuditingHandler(template.getConverter().getMappingContext())));
}
@Autowired
@@ -219,6 +236,7 @@ public class MongoTemplateTests {
template.dropCollection(DocumentWithCollectionOfSamples.class);
template.dropCollection(WithGeoJson.class);
template.dropCollection(DocumentWithNestedTypeHavingStringIdProperty.class);
template.dropCollection(ImmutableAudited.class);
}
@Test
@@ -353,7 +371,6 @@ public class MongoTemplateTests {
}
@Test
@SuppressWarnings("deprecation")
public void testEnsureIndex() throws Exception {
Person p1 = new Person("Oliver");
@@ -762,7 +779,7 @@ public class MongoTemplateTests {
assertThat(template.findDistinct(new BasicQuery("{'address.state' : 'PA'}"), "name", MyPerson.class, String.class))
.containsExactlyInAnyOrder(person1.getName(), person2.getName());
assertThat(template.findDistinct(new BasicQuery("{'address.state' : 'PA'}"), "name",
template.determineCollectionName(MyPerson.class), MyPerson.class, String.class))
template.getCollectionName(MyPerson.class), MyPerson.class, String.class))
.containsExactlyInAnyOrder(person1.getName(), person2.getName());
}
@@ -1259,14 +1276,14 @@ public class MongoTemplateTests {
template.setWriteConcern(WriteConcern.UNACKNOWLEDGED);
template.save(person);
UpdateResult result = template.updateFirst(query(where("id").is(person.getId())), update("firstName", "Carter"),
template.updateFirst(query(where("id").is(person.getId())), update("firstName", "Carter"),
PersonWithIdPropertyOfTypeObjectId.class);
FsyncSafeWriteConcernResolver resolver = new FsyncSafeWriteConcernResolver();
template.setWriteConcernResolver(resolver);
Query q = query(where("_id").is(person.getId()));
Update u = update("firstName", "Carter");
result = template.updateFirst(q, u, PersonWithIdPropertyOfTypeObjectId.class);
template.updateFirst(q, u, PersonWithIdPropertyOfTypeObjectId.class);
MongoAction lastMongoAction = resolver.getMongoAction();
assertThat(lastMongoAction.getCollectionName(), is("personWithIdPropertyOfTypeObjectId"));
@@ -1283,7 +1300,7 @@ public class MongoTemplateTests {
public WriteConcern resolve(MongoAction action) {
this.mongoAction = action;
return WriteConcern.FSYNC_SAFE;
return WriteConcern.JOURNALED;
}
public MongoAction getMongoAction() {
@@ -1530,7 +1547,7 @@ public class MongoTemplateTests {
org.bson.Document document = new org.bson.Document();
document.put("firstName", "Oliver");
template.insert(document, template.determineCollectionName(PersonWithVersionPropertyOfTypeInteger.class));
template.insert(document, template.getCollectionName(PersonWithVersionPropertyOfTypeInteger.class));
}
@Test // DATAMONGO-1617
@@ -1685,7 +1702,7 @@ public class MongoTemplateTests {
@Test(expected = DuplicateKeyException.class) // DATAMONGO-622
public void preventsDuplicateInsert() {
template.setWriteConcern(WriteConcern.SAFE);
template.setWriteConcern(WriteConcern.ACKNOWLEDGED);
PersonWithVersionPropertyOfTypeInteger person = new PersonWithVersionPropertyOfTypeInteger();
person.firstName = "Dave";
@@ -3527,11 +3544,11 @@ public class MongoTemplateTests {
template.save(rickon);
List<Sample> result = template.findAllAndRemove(query(where("field").regex(".*stark$")),
template.determineCollectionName(Sample.class));
template.getCollectionName(Sample.class));
assertThat(result, hasSize(2));
assertThat(result, containsInAnyOrder(bran, rickon));
assertThat(template.count(new BasicQuery("{}"), template.determineCollectionName(Sample.class)), is(equalTo(1L)));
assertThat(template.count(new BasicQuery("{}"), template.getCollectionName(Sample.class)), is(equalTo(1L)));
}
@Test // DATAMONGO-1779
@@ -3594,6 +3611,20 @@ public class MongoTemplateTests {
assertThat(target).isEqualTo(source);
}
@Test // DATAMONGO-1992
public void writesAuditingMetadataForImmutableTypes() {
ImmutableAudited source = new ImmutableAudited(null, null);
ImmutableAudited result = template.save(source);
assertThat(result).isNotSameAs(source).describedAs("Expected a different instances to be returned!");
assertThat(result.modified).isNotNull().describedAs("Auditing field must not be null!");
ImmutableAudited read = template.findOne(query(where("id").is(result.getId())), ImmutableAudited.class);
assertThat(read.modified).isEqualTo(result.modified).describedAs("Expected auditing information to be read!");
}
static class TypeWithNumbers {
@Id String id;
@@ -4074,6 +4105,8 @@ public class MongoTemplateTests {
}
// DATAMONGO-1992
@AllArgsConstructor
@Wither
static class ImmutableVersioned {
@@ -4086,4 +4119,11 @@ public class MongoTemplateTests {
version = null;
}
}
@Value
@Wither
static class ImmutableAudited {
@Id String id;
@LastModifiedDate Instant modified;
}
}

View File

@@ -15,17 +15,25 @@
*/
package org.springframework.data.mongodb.core.mapping.event;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.Value;
import lombok.experimental.Wither;
import java.util.Arrays;
import java.util.Date;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.AdditionalAnswers;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.core.Ordered;
import org.springframework.data.annotation.CreatedDate;
@@ -54,8 +62,9 @@ public class AuditingEventListenerUnitTests {
mappingContext.getPersistentEntity(Sample.class);
handler = spy(new IsNewAwareAuditingHandler(new PersistentEntities(Arrays.asList(mappingContext))));
doNothing().when(handler).markCreated(any());
doNothing().when(handler).markModified(any());
doAnswer(AdditionalAnswers.returnsArgAt(0)).when(handler).markCreated(any());
doAnswer(AdditionalAnswers.returnsArgAt(0)).when(handler).markModified(any());
listener = new AuditingEventListener(() -> handler);
}
@@ -93,10 +102,37 @@ public class AuditingEventListenerUnitTests {
assertThat(listener.getOrder(), is(100));
}
@Test // DATAMONGO-1992
public void propagatesChangedInstanceToEvent() {
ImmutableSample sample = new ImmutableSample();
BeforeConvertEvent<Object> event = new BeforeConvertEvent<>(sample, "collection");
ImmutableSample newSample = new ImmutableSample();
IsNewAwareAuditingHandler handler = mock(IsNewAwareAuditingHandler.class);
doReturn(newSample).when(handler).markAudited(eq(sample));
AuditingEventListener listener = new AuditingEventListener(() -> handler);
listener.onApplicationEvent(event);
assertThat(event.getSource()).isSameAs(newSample);
}
static class Sample {
@Id String id;
@CreatedDate Date created;
@LastModifiedDate Date modified;
}
@Value
@Wither
@AllArgsConstructor
@NoArgsConstructor(force = true)
static class ImmutableSample {
@Id String id;
@CreatedDate Date created;
@LastModifiedDate Date modified;
}
}