DATAMONGO-2502 - Fix nested array path mapping for updates.
Original pull request: #847.
This commit is contained in:
committed by
Mark Paluch
parent
5dd91d0b6d
commit
936a0d35f7
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user