Browse Source

DATAMONGO-2215 - Add support for array filters to Update.

We now support filtered positional $[<identifier>] operator via Updates. This allows to specify a filter criteria chain for the elements in an array.

new Update()
    .set("grades.$[element]", 100)
    .filterArray(Criteria.where("element").gte(100));

Array filters can be used for MongoOperations#update* & MongoOperations#findAndModify

Original pull request: #656.
pull/658/head
Christoph Strobl 7 years ago committed by Mark Paluch
parent
commit
1d910da697
  1. 9
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java
  2. 18
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
  3. 16
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
  4. 37
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java
  5. 34
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UpdateDefinition.java
  6. 33
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java
  7. 39
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java
  8. 36
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java

9
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java

@ -137,5 +137,14 @@ public class MappedDocument { @@ -137,5 +137,14 @@ public class MappedDocument {
public Boolean isIsolated() {
return delegate.isIsolated();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.UpdateDefinition#getArrayFilters()
*/
@Override
public List<ArrayFilter> getArrayFilters() {
return delegate.getArrayFilters();
}
}
}

18
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java

@ -104,6 +104,7 @@ import org.springframework.data.mongodb.core.query.NearQuery; @@ -104,6 +104,7 @@ import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter;
import org.springframework.data.mongodb.core.validation.Validator;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.util.CloseableIterator;
@ -1587,6 +1588,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -1587,6 +1588,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
UpdateOptions opts = new UpdateOptions();
opts.upsert(upsert);
if (update.hasArrayFilters()) {
opts.arrayFilters(
update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList()));
}
Document queryObj = new Document();
if (query != null) {
@ -2551,7 +2557,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -2551,7 +2557,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
collectionName);
}
return executeFindOneInternal(new FindAndModifyCallback(mappedQuery, fields, sort, mappedUpdate, options),
return executeFindOneInternal(
new FindAndModifyCallback(mappedQuery, fields, sort, mappedUpdate,
update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList()), options),
new ReadDocumentCallback<>(readerToUse, entityClass, collectionName), collectionName);
}
@ -2908,14 +2916,16 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -2908,14 +2916,16 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
private final Document fields;
private final Document sort;
private final Document update;
private final List<Document> arrayFilters;
private final FindAndModifyOptions options;
public FindAndModifyCallback(Document query, Document fields, Document sort, Document update,
FindAndModifyOptions options) {
List<Document> arrayFilters, FindAndModifyOptions options) {
this.query = query;
this.fields = fields;
this.sort = sort;
this.update = update;
this.arrayFilters = arrayFilters;
this.options = options;
}
@ -2933,6 +2943,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @@ -2933,6 +2943,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
options.getCollation().map(Collation::toMongoCollation).ifPresent(opts::collation);
if (!arrayFilters.isEmpty()) {
opts.arrayFilters(arrayFilters);
}
return collection.findOneAndUpdate(query, update, opts);
}
}

16
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java

@ -92,6 +92,7 @@ import org.springframework.data.mongodb.core.query.NearQuery; @@ -92,6 +92,7 @@ import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter;
import org.springframework.data.mongodb.core.validation.Validator;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.util.Optionals;
@ -1640,6 +1641,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -1640,6 +1641,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
UpdateOptions updateOptions = new UpdateOptions().upsert(upsert);
query.getCollation().map(Collation::toMongoCollation).ifPresent(updateOptions::collation);
if (update.hasArrayFilters()) {
updateOptions.arrayFilters(update.getArrayFilters().stream().map(ArrayFilter::asDocument)
.map(it -> queryMapper.getMappedObject(it, entity)).collect(Collectors.toList()));
}
if (!UpdateMapper.isUpdateObject(updateObj)) {
ReplaceOptions replaceOptions = new ReplaceOptions();
@ -2367,7 +2373,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -2367,7 +2373,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
collectionName));
}
return executeFindOneInternal(new FindAndModifyCallback(mappedQuery, fields, sort, mappedUpdate, options),
return executeFindOneInternal(new FindAndModifyCallback(mappedQuery, fields, sort, mappedUpdate, update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList()), options),
new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName);
});
}
@ -2751,6 +2757,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -2751,6 +2757,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private final Document fields;
private final Document sort;
private final Document update;
private final List<Document> arrayFilters;
private final FindAndModifyOptions options;
@Override
@ -2766,12 +2773,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -2766,12 +2773,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return collection.findOneAndDelete(query, findOneAndDeleteOptions);
}
FindOneAndUpdateOptions findOneAndUpdateOptions = convertToFindOneAndUpdateOptions(options, fields, sort);
FindOneAndUpdateOptions findOneAndUpdateOptions = convertToFindOneAndUpdateOptions(options, fields, sort, arrayFilters);
return collection.findOneAndUpdate(query, update, findOneAndUpdateOptions);
}
private FindOneAndUpdateOptions convertToFindOneAndUpdateOptions(FindAndModifyOptions options, Document fields,
Document sort) {
private static FindOneAndUpdateOptions convertToFindOneAndUpdateOptions(FindAndModifyOptions options, Document fields,
Document sort, List<Document> arrayFilters) {
FindOneAndUpdateOptions result = new FindOneAndUpdateOptions();
@ -2784,6 +2791,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -2784,6 +2791,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}
result = options.getCollation().map(Collation::toMongoCollation).map(result::collation).orElse(result);
result.arrayFilters(arrayFilters);
return result;
}

37
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java

@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
*/
package org.springframework.data.mongodb.core.query;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@ -58,6 +59,7 @@ public class Update implements UpdateDefinition { @@ -58,6 +59,7 @@ public class Update implements UpdateDefinition {
private Set<String> keysToUpdate = new HashSet<>();
private Map<String, Object> modifierOps = new LinkedHashMap<>();
private Map<String, PushOperatorBuilder> pushCommandBuilders = new LinkedHashMap<>(1);
private List<ArrayFilter> arrayFilters = new ArrayList<>();
/**
* Static factory method to create an Update using the provided key
@ -399,6 +401,33 @@ public class Update implements UpdateDefinition { @@ -399,6 +401,33 @@ public class Update implements UpdateDefinition {
return this;
}
/**
* Filter elements in an array that match the given criteria for update.
*
* @param criteria must not be {@literal null}.
* @return this.
* @since 2.2
*/
public Update filterArray(CriteriaDefinition criteria) {
this.arrayFilters.add(() -> criteria.getCriteriaObject());
return this;
}
/**
* Filter elements in an array that match the given criteria for update.
*
* @param identifier the positional operator identifier filter criteria name.
* @param expression the positional operator filter expression.
* @return this.
* @since 2.2
*/
public Update filterArray(String identifier, Object expression) {
this.arrayFilters.add(() -> new Document(identifier, expression));
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.UpdateDefinition#isIsolated()
@ -415,6 +444,14 @@ public class Update implements UpdateDefinition { @@ -415,6 +444,14 @@ public class Update implements UpdateDefinition {
return new Document(modifierOps);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.query.UpdateDefinition#getArrayFilters()
*/
public List<ArrayFilter> getArrayFilters() {
return Collections.unmodifiableList(this.arrayFilters);
}
/**
* This method is not called anymore rather override {@link #addMultiFieldOperation(String, String, Object)}.
*

34
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UpdateDefinition.java

@ -15,6 +15,8 @@ @@ -15,6 +15,8 @@
*/
package org.springframework.data.mongodb.core.query;
import java.util.List;
import org.bson.Document;
/**
@ -53,4 +55,36 @@ public interface UpdateDefinition { @@ -53,4 +55,36 @@ public interface UpdateDefinition {
* @param key must not be {@literal null}.
*/
void inc(String key);
/**
* Get the specification which elements to modify in an array field.
*
* @return never {@literal null}.
* @since 2.2
*/
List<ArrayFilter> getArrayFilters();
/**
* @return {@literal true} if {@link UpdateDefinition} contains {@link #getArrayFilters() array filters}.
* @since 2.2
*/
default boolean hasArrayFilters() {
return !getArrayFilters().isEmpty();
}
/**
* A filter to specify which elements to modify in an array field.
*
* @since 2.2
*/
interface ArrayFilter {
/**
* Get the {@link Document} representation of the filter to apply. The returned Document is subject to mapping
* domain type filed names.
*
* @return never {@literal null}.
*/
Document asDocument();
}
}

33
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java

@ -32,6 +32,7 @@ import java.util.Map; @@ -32,6 +32,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
import org.assertj.core.api.Assertions;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
@ -1034,6 +1035,34 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @@ -1034,6 +1035,34 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
is(equalTo(new Document("version", 11).append("_class", VersionedEntity.class.getName()))));
}
@Test // DATAMONGO-2215
public void updateShouldApplyArrayFilters() {
template.updateFirst(new BasicQuery("{}"),
new Update().set("grades.$[element]", 100).filterArray(Criteria.where("element").gte(100)),
EntityWithListOfSimple.class);
ArgumentCaptor<UpdateOptions> options = ArgumentCaptor.forClass(UpdateOptions.class);
verify(collection).updateOne(any(), any(), options.capture());
Assertions.assertThat((List<Bson>) options.getValue().getArrayFilters())
.contains(new org.bson.Document("element", new Document("$gte", 100)));
}
@Test // DATAMONGO-2215
public void findAndModifyShouldApplyArrayFilters() {
template.findAndModify(new BasicQuery("{}"),
new Update().set("grades.$[element]", 100).filterArray(Criteria.where("element").gte(100)),
EntityWithListOfSimple.class);
ArgumentCaptor<FindOneAndUpdateOptions> options = ArgumentCaptor.forClass(FindOneAndUpdateOptions.class);
verify(collection).findOneAndUpdate(any(), any(), options.capture());
Assertions.assertThat((List<Bson>) options.getValue().getArrayFilters())
.contains(new org.bson.Document("element", new Document("$gte", 100)));
}
class AutogenerateableId {
@Id BigInteger id;
@ -1102,6 +1131,10 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @@ -1102,6 +1131,10 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
AutogenerateableId foo;
}
static class EntityWithListOfSimple {
List<Integer> grades;
}
/**
* Mocks out the {@link MongoTemplate#getDb()} method to return the {@link DB} mock instead of executing the actual
* behaviour.

39
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java

@ -25,8 +25,10 @@ import reactor.core.publisher.Mono; @@ -25,8 +25,10 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.assertj.core.api.Assertions;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
@ -47,6 +49,7 @@ import org.springframework.data.mongodb.core.mapping.Field; @@ -47,6 +49,7 @@ import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
@ -81,6 +84,8 @@ public class ReactiveMongoTemplateUnitTests { @@ -81,6 +84,8 @@ public class ReactiveMongoTemplateUnitTests {
@Mock FindPublisher findPublisher;
@Mock AggregatePublisher aggregatePublisher;
@Mock Publisher runCommandPublisher;
@Mock Publisher updatePublisher;
@Mock Publisher findAndUpdatePublisher;
MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator();
MappingMongoConverter converter;
@ -98,6 +103,8 @@ public class ReactiveMongoTemplateUnitTests { @@ -98,6 +103,8 @@ public class ReactiveMongoTemplateUnitTests {
when(collection.find(any(Document.class), any(Class.class))).thenReturn(findPublisher);
when(collection.aggregate(anyList())).thenReturn(aggregatePublisher);
when(collection.aggregate(anyList(), any(Class.class))).thenReturn(aggregatePublisher);
when(collection.updateOne(any(), any(), any(UpdateOptions.class))).thenReturn(updatePublisher);
when(collection.findOneAndUpdate(any(), any(), any(FindOneAndUpdateOptions.class))).thenReturn(findAndUpdatePublisher);
when(findPublisher.projection(any())).thenReturn(findPublisher);
when(findPublisher.limit(anyInt())).thenReturn(findPublisher);
when(findPublisher.collation(any())).thenReturn(findPublisher);
@ -343,6 +350,34 @@ public class ReactiveMongoTemplateUnitTests { @@ -343,6 +350,34 @@ public class ReactiveMongoTemplateUnitTests {
verify(findPublisher, never()).projection(any());
}
@Test // DATAMONGO-2215
public void updateShouldApplyArrayFilters() {
template.updateFirst(new BasicQuery("{}"),
new Update().set("grades.$[element]", 100).filterArray(Criteria.where("element").gte(100)),
EntityWithListOfSimple.class).subscribe();
ArgumentCaptor<UpdateOptions> options = ArgumentCaptor.forClass(UpdateOptions.class);
verify(collection).updateOne(any(), any(), options.capture());
Assertions.assertThat((List<Bson>) options.getValue().getArrayFilters())
.contains(new org.bson.Document("element", new Document("$gte", 100)));
}
@Test // DATAMONGO-2215
public void findAndModifyShouldApplyArrayFilters() {
template.findAndModify(new BasicQuery("{}"),
new Update().set("grades.$[element]", 100).filterArray(Criteria.where("element").gte(100)),
EntityWithListOfSimple.class).subscribe();
ArgumentCaptor<FindOneAndUpdateOptions> options = ArgumentCaptor.forClass(FindOneAndUpdateOptions.class);
verify(collection).findOneAndUpdate(any(), any(), options.capture());
Assertions.assertThat((List<Bson>) options.getValue().getArrayFilters())
.contains(new org.bson.Document("element", new Document("$gte", 100)));
}
@Data
@org.springframework.data.mongodb.core.mapping.Document(collection = "star-wars")
static class Person {
@ -371,4 +406,8 @@ public class ReactiveMongoTemplateUnitTests { @@ -371,4 +406,8 @@ public class ReactiveMongoTemplateUnitTests {
@Field("firstname") String name;
}
static class EntityWithListOfSimple {
List<Integer> grades;
}
}

36
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java

@ -1051,6 +1051,42 @@ public class UpdateMapperUnitTests { @@ -1051,6 +1051,42 @@ public class UpdateMapperUnitTests {
assertThat(mappedUpdate).isEqualTo(new Document("$inc", new Document("aliased.$[]", 10)));
}
@Test // DATAMONGO-2215
public void mappingShouldAllowPositionParameterWithIdentifier() {
Update update = new Update().set("grades.$[element]", 10) //
.filterArray(Criteria.where("element").gte(100));
Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
context.getPersistentEntity(EntityWithListOfSimple.class));
assertThat(mappedUpdate).isEqualTo(new Document("$set", new Document("grades.$[element]", 10)));
}
@Test // DATAMONGO-2215
public void mappingShouldAllowPositionParameterWithIdentifierWhenFieldHasExplicitFieldName() {
Update update = new Update().set("list.$[element]", 10) //
.filterArray(Criteria.where("element").gte(100));
Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
context.getPersistentEntity(ParentClass.class));
assertThat(mappedUpdate).isEqualTo(new Document("$set", new Document("aliased.$[element]", 10)));
}
@Test // DATAMONGO-2215
public void mappingShouldAllowNestedPositionParameterWithIdentifierWhenFieldHasExplicitFieldName() {
Update update = new Update().set("list.$[element].value", 10) //
.filterArray(Criteria.where("element").gte(100));
Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
context.getPersistentEntity(ParentClass.class));
assertThat(mappedUpdate).isEqualTo(new Document("$set", new Document("aliased.$[element].value", 10)));
}
static class DomainTypeWrappingConcreteyTypeHavingListOfInterfaceTypeAttributes {
ListModelWrapper concreteTypeWithListAttributeOfInterfaceType;
}

Loading…
Cancel
Save