DATAMONGO-2502 - Fix nested array path mapping for updates.

Original pull request: #847.
This commit is contained in:
Christoph Strobl
2020-03-30 09:22:53 +02:00
committed by Mark Paluch
parent 5dd91d0b6d
commit 936a0d35f7
4 changed files with 93 additions and 21 deletions

View File

@@ -922,6 +922,7 @@ public class QueryMapper {
*/
protected static class MetadataBackedField extends Field {
private static final Pattern POSITIONAL_PARAMETER_PATTERN = Pattern.compile("\\.\\$(\\[.*?\\])?|\\.\\d+");
private static final String INVALID_ASSOCIATION_REFERENCE = "Invalid path reference %s! Associations can only be pointed to directly or via their id property!";
private final MongoPersistentEntity<?> entity;
@@ -963,11 +964,15 @@ public class QueryMapper {
this.entity = entity;
this.mappingContext = context;
this.path = getPath(name);
this.path = getPath(removePositionalPlaceholders(name));
this.property = path == null ? property : path.getLeafProperty();
this.association = findAssociation();
}
private static String removePositionalPlaceholders(String raw) {
return POSITIONAL_PARAMETER_PATTERN.matcher(raw).replaceAll("");
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#with(java.lang.String)
@@ -1168,7 +1173,7 @@ public class QueryMapper {
* @since 1.7
*/
protected Converter<MongoPersistentProperty, String> getAssociationConverter() {
return new AssociationConverter(getAssociation());
return new AssociationConverter(name, getAssociation());
}
protected MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> getMappingContext() {
@@ -1247,21 +1252,10 @@ public class QueryMapper {
StringBuilder mappedName = new StringBuilder(PropertyToFieldNameConverter.INSTANCE.convert(property));
boolean inspect = iterator.hasNext();
int depth = 0;
while (inspect) {
String partial = iterator.next();
if (depth > 0 && property.isCollectionLike() && property.isEntity() && property.getComponentType() != null) {
MongoPersistentEntity<?> persistentEntity = mappingContext
.getRequiredPersistentEntity(property.getComponentType());
MongoPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(partial);
if (persistentProperty != null) {
partial = mapPropertyName(persistentProperty);
}
}
boolean isPositional = (isPositionalParameter(partial) && (property.isMap() || property.isCollectionLike()));
if (isPositional) {
@@ -1269,13 +1263,12 @@ public class QueryMapper {
}
inspect = isPositional && iterator.hasNext();
depth++;
}
return mappedName.toString();
}
private static boolean isPositionalParameter(String partial) {
static boolean isPositionalParameter(String partial) {
if ("$".equals(partial)) {
return true;
@@ -1303,6 +1296,7 @@ public class QueryMapper {
*/
protected static class AssociationConverter implements Converter<MongoPersistentProperty, String> {
private final String name;
private final MongoPersistentProperty property;
private boolean associationFound;
@@ -1311,10 +1305,11 @@ public class QueryMapper {
*
* @param association must not be {@literal null}.
*/
public AssociationConverter(Association<MongoPersistentProperty> association) {
public AssociationConverter(String name, Association<MongoPersistentProperty> association) {
Assert.notNull(association, "Association must not be null!");
this.property = association.getInverse();
this.name = name;
}
/*
@@ -1332,6 +1327,12 @@ public class QueryMapper {
associationFound = true;
}
if (associationFound) {
if (name.endsWith("$") && property.isCollectionLike()) {
return source.getFieldName() + ".$";
}
}
return source.getFieldName();
}
}

View File

@@ -272,6 +272,7 @@ public class UpdateMapper extends QueryMapper {
*
* @author Thomas Darimont
* @author Oliver Gierke
* @author Christoph Strobl
*/
private static class MetadataBackedUpdateField extends MetadataBackedField {
@@ -289,7 +290,7 @@ public class UpdateMapper extends QueryMapper {
public MetadataBackedUpdateField(MongoPersistentEntity<?> entity, String key,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
super(key.replaceAll("\\.\\$(\\[.*\\])?", ""), entity, mappingContext);
super(key, entity, mappingContext);
this.key = key;
}
@@ -338,7 +339,7 @@ public class UpdateMapper extends QueryMapper {
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
Association<MongoPersistentProperty> association, String key) {
super(association);
super(key, association);
this.mapper = new KeyMapper(key, mappingContext);
}

View File

@@ -38,7 +38,6 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
@@ -233,8 +232,8 @@ class DefaultBulkOperationsUnitTests {
verify(beforeSaveCallback).onBeforeSave(personArgumentCaptor.capture(), any(), eq("collection-1"));
verify(afterSaveCallback).onAfterSave(personArgumentCaptor.capture(), any(), eq("collection-1"));
assertThat(personArgumentCaptor.getAllValues()).extracting("firstName")
.containsExactly("init", "before-convert", "before-convert");
assertThat(personArgumentCaptor.getAllValues()).extracting("firstName").containsExactly("init", "before-convert",
"before-convert");
verify(collection).bulkWrite(captor.capture(), any());
InsertOneModel<Document> updateModel = (InsertOneModel<Document>) captor.getValue().get(0);
@@ -340,6 +339,51 @@ class DefaultBulkOperationsUnitTests {
.isEqualTo(new org.bson.Document("element", new Document("$gte", 100)));
}
@Test // DATAMONGO-2502
void shouldRetainNestedArrayPathWithPlaceholdersForNoMatchingPaths() {
ops.updateOne(new BasicQuery("{}"), new Update().set("items.$.documents.0.fileId", "new-id")).execute();
verify(collection).bulkWrite(captor.capture(), any());
UpdateOneModel<Document> updateModel = (UpdateOneModel<Document>) captor.getValue().get(0);
assertThat(updateModel.getUpdate())
.isEqualTo(new Document("$set", new Document("items.$.documents.0.fileId", "new-id")));
}
@Test // DATAMONGO-2502
void shouldRetainNestedArrayPathWithPlaceholdersForMappedEntity() {
DefaultBulkOperations ops = new DefaultBulkOperations(template, "collection-1",
new BulkOperationContext(BulkMode.ORDERED, Optional.of(mappingContext.getPersistentEntity(OrderTest.class)),
new QueryMapper(converter), new UpdateMapper(converter), null, null));
ops.updateOne(new BasicQuery("{}"), Update.update("items.$.documents.0.fileId", "file-id")).execute();
verify(collection).bulkWrite(captor.capture(), any());
UpdateOneModel<Document> updateModel = (UpdateOneModel<Document>) captor.getValue().get(0);
assertThat(updateModel.getUpdate())
.isEqualTo(new Document("$set", new Document("items.$.documents.0.fileId", "file-id")));
}
static class OrderTest {
String id;
List<OrderTestItem> items;
}
static class OrderTestItem {
private String cartId;
private List<OrderTestDocument> documents;
}
static class OrderTestDocument {
private String fileId;
}
class SomeDomainType {
@Id String id;

View File

@@ -966,6 +966,32 @@ public class QueryMapperUnitTests {
assertThat(target).isEqualTo(new org.bson.Document("arrayCustomName.$[some_item].nes-ted.$[other_item]", "value"));
}
@Test // DATAMONGO-2502
void shouldAllowDeeplyNestedPlaceholders() {
org.bson.Document target = mapper.getMappedObject(
query(where("level0.$[some_item].arrayObj.$[other_item].nested").is("value")).getQueryObject(),
context.getPersistentEntity(WithDeepArrayNesting.class));
assertThat(target).isEqualTo(new org.bson.Document("level0.$[some_item].arrayObj.$[other_item].nested", "value"));
}
@Test // DATAMONGO-2502
void shouldAllowDeeplyNestedPlaceholdersWithCustomName() {
org.bson.Document target = mapper.getMappedObject(
query(where("level0.$[some_item].arrayCustomName.$[other_item].nested").is("value")).getQueryObject(),
context.getPersistentEntity(WithDeepArrayNesting.class));
assertThat(target)
.isEqualTo(new org.bson.Document("level0.$[some_item].arrayCustomName.$[other_item].nes-ted", "value"));
}
class WithDeepArrayNesting {
List<WithNestedArray> level0;
}
class WithNestedArray {
List<NestedArrayOfObj> arrayObj;