Skip to content

Commit 64271c0

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 558c982 commit 64271c0

File tree

4 files changed

+309
-12
lines changed

4 files changed

+309
-12
lines changed

Diff for: 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
}

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

+15
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.springframework.data.mongodb.core.aggregation.SetOperation;
3535
import org.springframework.data.mongodb.core.mapping.Document;
3636
import org.springframework.data.mongodb.core.mapping.Field;
37+
import org.springframework.data.mongodb.core.query.BasicUpdate;
3738
import org.springframework.data.mongodb.core.query.Criteria;
3839
import org.springframework.data.mongodb.core.query.Query;
3940
import org.springframework.data.mongodb.core.query.Update;
@@ -289,6 +290,20 @@ void nullValueShouldBePropagatedToDatabase() {
289290
null);
290291
}
291292

293+
@Test // GH-4918
294+
void updateShouldHonorVersionProvided() {
295+
296+
Versioned source = template.insert(Versioned.class).one(new Versioned("id-1", "value-0"));
297+
298+
Update update = new BasicUpdate("{ '$set' : { 'value' : 'changed' }, '$inc' : { 'version' : 10 } }");
299+
template.update(Versioned.class).matching(Query.query(Criteria.where("id").is(source.id))).apply(update).first();
300+
301+
assertThat(
302+
collection(Versioned.class).find(new org.bson.Document("_id", source.id)).limit(1).into(new ArrayList<>()))
303+
.containsExactly(new org.bson.Document("_id", source.id).append("version", 10L).append("value", "changed")
304+
.append("_class", "org.springframework.data.mongodb.core.MongoTemplateUpdateTests$Versioned"));
305+
}
306+
292307
private List<org.bson.Document> all(Class<?> type) {
293308
return collection(type).find(new org.bson.Document()).into(new ArrayList<>());
294309
}
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)