Skip to content

Commit 8e9728c

Browse files
christophstroblmp911de
authored andcommitted
Fix basic update overriding values.
This change makes sure basic update appends values to operations instead of overriding them. This change aligns the behaviour with Update and fixes issues where using the Update annotation with versioned entities can lead to loss of update information. Closes: #4918 Original pull request: #4921
1 parent d08f0bc commit 8e9728c

File tree

4 files changed

+309
-12
lines changed

4 files changed

+309
-12
lines changed

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

+37-12
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@
1515
*/
1616
package org.springframework.data.mongodb.core.query;
1717

18-
import java.util.Arrays;
1918
import java.util.Collections;
19+
import java.util.LinkedHashMap;
20+
import java.util.List;
21+
import java.util.Map;
2022

2123
import org.bson.Document;
2224
import org.springframework.lang.Nullable;
25+
import org.springframework.util.ClassUtils;
2326

2427
/**
2528
* @author Thomas Risberg
@@ -44,63 +47,85 @@ public BasicUpdate(Document updateObject) {
4447

4548
@Override
4649
public Update set(String key, @Nullable Object value) {
47-
updateObject.put("$set", Collections.singletonMap(key, value));
50+
setOperationValue("$set", key, value);
4851
return this;
4952
}
5053

5154
@Override
5255
public Update unset(String key) {
53-
updateObject.put("$unset", Collections.singletonMap(key, 1));
56+
setOperationValue("$unset", key, 1);
5457
return this;
5558
}
5659

5760
@Override
5861
public Update inc(String key, Number inc) {
59-
updateObject.put("$inc", Collections.singletonMap(key, inc));
62+
setOperationValue("$inc", key, inc);
6063
return this;
6164
}
6265

6366
@Override
6467
public Update push(String key, @Nullable Object value) {
65-
updateObject.put("$push", Collections.singletonMap(key, value));
68+
setOperationValue("$push", key, value);
6669
return this;
6770
}
6871

6972
@Override
7073
public Update addToSet(String key, @Nullable Object value) {
71-
updateObject.put("$addToSet", Collections.singletonMap(key, value));
74+
setOperationValue("$addToSet", key, value);
7275
return this;
7376
}
7477

7578
@Override
7679
public Update pop(String key, Position pos) {
77-
updateObject.put("$pop", Collections.singletonMap(key, (pos == Position.FIRST ? -1 : 1)));
80+
setOperationValue("$pop", key, (pos == Position.FIRST ? -1 : 1));
7881
return this;
7982
}
8083

8184
@Override
8285
public Update pull(String key, @Nullable Object value) {
83-
updateObject.put("$pull", Collections.singletonMap(key, value));
86+
setOperationValue("$pull", key, value);
8487
return this;
8588
}
8689

8790
@Override
8891
public Update pullAll(String key, Object[] values) {
89-
Document keyValue = new Document();
90-
keyValue.put(key, Arrays.copyOf(values, values.length));
91-
updateObject.put("$pullAll", keyValue);
92+
setOperationValue("$pullAll", key, List.of(values));
9293
return this;
9394
}
9495

9596
@Override
9697
public Update rename(String oldName, String newName) {
97-
updateObject.put("$rename", Collections.singletonMap(oldName, newName));
98+
setOperationValue("$rename", oldName, newName);
9899
return this;
99100
}
100101

102+
@Override
103+
public boolean modifies(String key) {
104+
return super.modifies(key) || Update.fromDocument(getUpdateObject()).modifies(key);
105+
}
106+
101107
@Override
102108
public Document getUpdateObject() {
103109
return updateObject;
104110
}
105111

112+
void setOperationValue(String operator, String key, Object value) {
113+
114+
if (!updateObject.containsKey(operator)) {
115+
updateObject.put(operator, Collections.singletonMap(key, value));
116+
} else {
117+
Object existingValue = updateObject.get(operator);
118+
if (existingValue instanceof Map<?, ?> existing) {
119+
Map<Object, Object> target = new LinkedHashMap<>(existing);
120+
target.put(key, value);
121+
updateObject.put(operator, target);
122+
} else {
123+
throw new IllegalStateException(
124+
"Cannot add ['%s' : { '%s' : ... }]. Operator already exists with value of type [%s] which is not suitable for appending"
125+
.formatted(operator, key,
126+
existingValue != null ? ClassUtils.getShortName(existingValue.getClass()) : "null"));
127+
}
128+
}
129+
}
130+
106131
}

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

+15
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.springframework.data.mongodb.core.aggregation.SetOperation;
4242
import org.springframework.data.mongodb.core.mapping.Document;
4343
import org.springframework.data.mongodb.core.mapping.Field;
44+
import org.springframework.data.mongodb.core.query.BasicUpdate;
4445
import org.springframework.data.mongodb.core.query.Criteria;
4546
import org.springframework.data.mongodb.core.query.Query;
4647
import org.springframework.data.mongodb.core.query.Update;
@@ -326,6 +327,20 @@ void updateFirstWithSort(Class<?> domainType, Sort sort, UpdateDefinition update
326327
"Science is real!");
327328
}
328329

330+
@Test // GH-4918
331+
void updateShouldHonorVersionProvided() {
332+
333+
Versioned source = template.insert(Versioned.class).one(new Versioned("id-1", "value-0"));
334+
335+
Update update = new BasicUpdate("{ '$set' : { 'value' : 'changed' }, '$inc' : { 'version' : 10 } }");
336+
template.update(Versioned.class).matching(Query.query(Criteria.where("id").is(source.id))).apply(update).first();
337+
338+
assertThat(
339+
collection(Versioned.class).find(new org.bson.Document("_id", source.id)).limit(1).into(new ArrayList<>()))
340+
.containsExactly(new org.bson.Document("_id", source.id).append("version", 10L).append("value", "changed")
341+
.append("_class", "org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Versioned"));
342+
}
343+
329344
private List<org.bson.Document> all(Class<?> type) {
330345
return collection(type).find(new org.bson.Document()).into(new ArrayList<>());
331346
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.query;
17+
18+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
19+
import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
20+
21+
import java.util.function.Function;
22+
import java.util.stream.Stream;
23+
24+
import org.bson.Document;
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.params.ParameterizedTest;
27+
import org.junit.jupiter.params.provider.Arguments;
28+
import org.junit.jupiter.params.provider.CsvSource;
29+
import org.junit.jupiter.params.provider.MethodSource;
30+
import org.springframework.data.mongodb.core.query.Update.Position;
31+
32+
/**
33+
* @author Christoph Strobl
34+
*/
35+
public class BasicUpdateUnitTests {
36+
37+
@Test // GH-4918
38+
void setOperationValueShouldAppendsOpsCorrectly() {
39+
40+
BasicUpdate basicUpdate = new BasicUpdate("{}");
41+
basicUpdate.setOperationValue("$set", "key1", "alt");
42+
basicUpdate.setOperationValue("$set", "key2", "nps");
43+
basicUpdate.setOperationValue("$unset", "key3", "x");
44+
45+
assertThat(basicUpdate.getUpdateObject())
46+
.isEqualTo("{ '$set' : { 'key1' : 'alt', 'key2' : 'nps' }, '$unset' : { 'key3' : 'x' } }");
47+
}
48+
49+
@Test // GH-4918
50+
void setOperationErrorsOnNonMapType() {
51+
52+
BasicUpdate basicUpdate = new BasicUpdate("{ '$set' : 1 }");
53+
assertThatExceptionOfType(IllegalStateException.class)
54+
.isThrownBy(() -> basicUpdate.setOperationValue("$set", "k", "v"));
55+
}
56+
57+
@ParameterizedTest // GH-4918
58+
@CsvSource({ //
59+
"{ }, k1, false", //
60+
"{ '$set' : { 'k1' : 'v1' } }, k1, true", //
61+
"{ '$set' : { 'k1' : 'v1' } }, k2, false", //
62+
"{ '$set' : { 'k1.k2' : 'v1' } }, k1, false", //
63+
"{ '$set' : { 'k1.k2' : 'v1' } }, k1.k2, true", //
64+
"{ '$set' : { 'k1' : 'v1' } }, '', false", //
65+
"{ '$inc' : { 'k1' : 1 } }, k1, true" })
66+
void modifiesLooksUpKeyCorrectly(String source, String key, boolean modified) {
67+
68+
BasicUpdate basicUpdate = new BasicUpdate(source);
69+
assertThat(basicUpdate.modifies(key)).isEqualTo(modified);
70+
}
71+
72+
@ParameterizedTest // GH-4918
73+
@MethodSource("updateOpArgs")
74+
void updateOpsShouldNotOverrideExistingValues(String operator, Function<BasicUpdate, Update> updateFunction) {
75+
76+
Document source = Document.parse("{ '%s' : { 'key-1' : 'value-1' } }".formatted(operator));
77+
Update update = updateFunction.apply(new BasicUpdate(source));
78+
79+
assertThat(update.getUpdateObject()).containsEntry("%s.key-1".formatted(operator), "value-1")
80+
.containsKey("%s.key-2".formatted(operator));
81+
}
82+
83+
static Stream<Arguments> updateOpArgs() {
84+
85+
return Stream.of( //
86+
Arguments.of("$set", (Function<BasicUpdate, Update>) update -> update.set("key-2", "value-2")),
87+
Arguments.of("$unset", (Function<BasicUpdate, Update>) update -> update.unset("key-2")),
88+
Arguments.of("$inc", (Function<BasicUpdate, Update>) update -> update.inc("key-2", 1)),
89+
Arguments.of("$push", (Function<BasicUpdate, Update>) update -> update.push("key-2", "value-2")),
90+
Arguments.of("$addToSet", (Function<BasicUpdate, Update>) update -> update.addToSet("key-2", "value-2")),
91+
Arguments.of("$pop", (Function<BasicUpdate, Update>) update -> update.pop("key-2", Position.FIRST)),
92+
Arguments.of("$pull", (Function<BasicUpdate, Update>) update -> update.pull("key-2", "value-2")),
93+
Arguments.of("$pullAll",
94+
(Function<BasicUpdate, Update>) update -> update.pullAll("key-2", new String[] { "value-2" })),
95+
Arguments.of("$rename", (Function<BasicUpdate, Update>) update -> update.rename("key-2", "value-2")));
96+
};
97+
}

0 commit comments

Comments
 (0)