Browse Source

Polishing.

Merge List values in Update. Add tests, reformat code.

See: #4918
Original pull request: #4921
4.3.x
Mark Paluch 9 months ago
parent
commit
9dce90a048
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 41
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicUpdate.java
  2. 10
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java
  3. 25
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/BasicUpdateUnitTests.java
  4. 28
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPersonRepositoryIntegrationTests.java

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

@ -15,16 +15,21 @@
*/ */
package org.springframework.data.mongodb.core.query; package org.springframework.data.mongodb.core.query;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.BiFunction;
import org.bson.Document; import org.bson.Document;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
/** /**
* {@link Document}-based {@link Update} variant.
*
* @author Thomas Risberg * @author Thomas Risberg
* @author John Brisbin * @author John Brisbin
* @author Oliver Gierke * @author Oliver Gierke
@ -36,12 +41,10 @@ public class BasicUpdate extends Update {
private final Document updateObject; private final Document updateObject;
public BasicUpdate(String updateString) { public BasicUpdate(String updateString) {
super(); this(Document.parse(updateString));
this.updateObject = Document.parse(updateString);
} }
public BasicUpdate(Document updateObject) { public BasicUpdate(Document updateObject) {
super();
this.updateObject = updateObject; this.updateObject = updateObject;
} }
@ -89,7 +92,17 @@ public class BasicUpdate extends Update {
@Override @Override
public Update pullAll(String key, Object[] values) { public Update pullAll(String key, Object[] values) {
setOperationValue("$pullAll", key, List.of(values)); setOperationValue("$pullAll", key, List.of(values), (o, o2) -> {
if (o instanceof List<?> prev && o2 instanceof List<?> currentValue) {
List<Object> merged = new ArrayList<>(prev.size() + currentValue.size());
merged.addAll(prev);
merged.addAll(currentValue);
return merged;
}
return o2;
});
return this; return this;
} }
@ -109,21 +122,31 @@ public class BasicUpdate extends Update {
return updateObject; return updateObject;
} }
void setOperationValue(String operator, String key, Object value) { void setOperationValue(String operator, String key, @Nullable Object value) {
setOperationValue(operator, key, value, (o, o2) -> o2);
}
void setOperationValue(String operator, String key, @Nullable Object value,
BiFunction<Object, Object, Object> mergeFunction) {
if (!updateObject.containsKey(operator)) { if (!updateObject.containsKey(operator)) {
updateObject.put(operator, Collections.singletonMap(key, value)); updateObject.put(operator, Collections.singletonMap(key, value));
} else { } else {
Object existingValue = updateObject.get(operator); Object o = updateObject.get(operator);
if (existingValue instanceof Map<?, ?> existing) { if (o instanceof Map<?, ?> existing) {
Map<Object, Object> target = new LinkedHashMap<>(existing); Map<Object, Object> target = new LinkedHashMap<>(existing);
target.put(key, value);
if (target.containsKey(key)) {
target.put(key, mergeFunction.apply(target.get(key), value));
} else {
target.put(key, value);
}
updateObject.put(operator, target); updateObject.put(operator, target);
} else { } else {
throw new IllegalStateException( throw new IllegalStateException(
"Cannot add ['%s' : { '%s' : ... }]. Operator already exists with value of type [%s] which is not suitable for appending" "Cannot add ['%s' : { '%s' : ... }]. Operator already exists with value of type [%s] which is not suitable for appending"
.formatted(operator, key, .formatted(operator, key,
existingValue != null ? ClassUtils.getShortName(existingValue.getClass()) : "null")); o != null ? ClassUtils.getShortName(o.getClass()) : "null"));
} }
} }
} }

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

@ -447,13 +447,11 @@ public class Update implements UpdateDefinition {
if (existingValue == null) { if (existingValue == null) {
keyValueMap = new Document(); keyValueMap = new Document();
this.modifierOps.put(operator, keyValueMap); this.modifierOps.put(operator, keyValueMap);
} else if (existingValue instanceof Document document) {
keyValueMap = document;
} else { } else {
if (existingValue instanceof Document document) { throw new InvalidDataAccessApiUsageException(
keyValueMap = document; "Modifier Operations should be a LinkedHashMap but was " + existingValue.getClass());
} else {
throw new InvalidDataAccessApiUsageException(
"Modifier Operations should be a LinkedHashMap but was " + existingValue.getClass());
}
} }
keyValueMap.put(key, value); keyValueMap.put(key, value);

25
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/BasicUpdateUnitTests.java

@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -15,9 +15,12 @@
*/ */
package org.springframework.data.mongodb.core.query; package org.springframework.data.mongodb.core.query;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.mongodb.test.util.Assertions.*;
import static org.springframework.data.mongodb.test.util.Assertions.assertThat; import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -27,12 +30,16 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.data.mongodb.core.query.Update.Position; import org.springframework.data.mongodb.core.query.Update.Position;
/** /**
* Unit tests for {@link BasicUpdate}.
*
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch
*/ */
public class BasicUpdateUnitTests { class BasicUpdateUnitTests {
@Test // GH-4918 @Test // GH-4918
void setOperationValueShouldAppendsOpsCorrectly() { void setOperationValueShouldAppendsOpsCorrectly() {
@ -80,8 +87,18 @@ public class BasicUpdateUnitTests {
.containsKey("%s.key-2".formatted(operator)); .containsKey("%s.key-2".formatted(operator));
} }
static Stream<Arguments> updateOpArgs() { @Test // GH-4918
void shouldNotOverridePullAll() {
Document source = Document.parse("{ '$pullAll' : { 'key-1' : ['value-1'] } }");
Update update = new BasicUpdate(source).pullAll("key-1", new String[] { "value-2" }).pullAll("key-2",
new String[] { "value-3" });
assertThat(update.getUpdateObject()).containsEntry("$pullAll.key-1", Arrays.asList("value-1", "value-2"))
.containsEntry("$pullAll.key-2", List.of("value-3"));
}
static Stream<Arguments> updateOpArgs() {
return Stream.of( // return Stream.of( //
Arguments.of("$set", (Function<BasicUpdate, Update>) update -> update.set("key-2", "value-2")), Arguments.of("$set", (Function<BasicUpdate, Update>) update -> update.set("key-2", "value-2")),
Arguments.of("$unset", (Function<BasicUpdate, Update>) update -> update.unset("key-2")), Arguments.of("$unset", (Function<BasicUpdate, Update>) update -> update.unset("key-2")),

28
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPersonRepositoryIntegrationTests.java

@ -15,13 +15,14 @@
*/ */
package org.springframework.data.mongodb.repository; package org.springframework.data.mongodb.repository;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.*;
import org.bson.Document; import org.bson.Document;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -40,12 +41,13 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClient;
/** /**
* Integration tests for Repositories using optimistic locking.
*
* @author Christoph Strobl * @author Christoph Strobl
* @since 2025/03
*/ */
@ExtendWith({ MongoClientExtension.class, SpringExtension.class }) @ExtendWith({ MongoClientExtension.class, SpringExtension.class })
@ContextConfiguration @ContextConfiguration
public class VersionedPersonRepositoryIntegrationTests { class VersionedPersonRepositoryIntegrationTests {
static @Client MongoClient mongoClient; static @Client MongoClient mongoClient;
@ -70,14 +72,15 @@ public class VersionedPersonRepositoryIntegrationTests {
@BeforeEach @BeforeEach
void beforeEach() { void beforeEach() {
MongoTestUtils.flushCollection("versioned-person-tests", template.getCollectionName(VersionedPersonWithCounter.class), MongoTestUtils.flushCollection("versioned-person-tests",
mongoClient); template.getCollectionName(VersionedPersonWithCounter.class), mongoClient);
} }
@Test // GH-4918 @Test // GH-4918
void updatesVersionedTypeCorrectly() { void updatesVersionedTypeCorrectly() {
VersionedPerson person = template.insert(VersionedPersonWithCounter.class).one(new VersionedPersonWithCounter("Donald", "Duckling")); VersionedPerson person = template.insert(VersionedPersonWithCounter.class)
.one(new VersionedPersonWithCounter("Donald", "Duckling"));
int updateCount = versionedPersonRepository.findAndSetFirstnameToLastnameByLastname(person.getLastname()); int updateCount = versionedPersonRepository.findAndSetFirstnameToLastnameByLastname(person.getLastname());
@ -93,7 +96,8 @@ public class VersionedPersonRepositoryIntegrationTests {
@Test // GH-4918 @Test // GH-4918
void updatesVersionedTypeCorrectlyWhenUpdateIsUsingInc() { void updatesVersionedTypeCorrectlyWhenUpdateIsUsingInc() {
VersionedPerson person = template.insert(VersionedPersonWithCounter.class).one(new VersionedPersonWithCounter("Donald", "Duckling")); VersionedPerson person = template.insert(VersionedPersonWithCounter.class)
.one(new VersionedPersonWithCounter("Donald", "Duckling"));
int updateCount = versionedPersonRepository.findAndIncCounterByLastname(person.getLastname()); int updateCount = versionedPersonRepository.findAndIncCounterByLastname(person.getLastname());
@ -103,13 +107,15 @@ public class VersionedPersonRepositoryIntegrationTests {
return collection.find(new Document("_id", new ObjectId(person.getId()))).first(); return collection.find(new Document("_id", new ObjectId(person.getId()))).first();
}); });
assertThat(document).containsEntry("lastname", "Duckling").containsEntry("version", 1L).containsEntry("counter", 42); assertThat(document).containsEntry("lastname", "Duckling").containsEntry("version", 1L).containsEntry("counter",
42);
} }
@Test // GH-4918 @Test // GH-4918
void updatesVersionedTypeCorrectlyWhenUpdateCoversVersionBump() { void updatesVersionedTypeCorrectlyWhenUpdateCoversVersionBump() {
VersionedPerson person = template.insert(VersionedPersonWithCounter.class).one(new VersionedPersonWithCounter("Donald", "Duckling")); VersionedPerson person = template.insert(VersionedPersonWithCounter.class)
.one(new VersionedPersonWithCounter("Donald", "Duckling"));
int updateCount = versionedPersonRepository.findAndSetFirstnameToLastnameIncVersionByLastname(person.getLastname(), int updateCount = versionedPersonRepository.findAndSetFirstnameToLastnameIncVersionByLastname(person.getLastname(),
10); 10);
@ -123,7 +129,7 @@ public class VersionedPersonRepositoryIntegrationTests {
assertThat(document).containsEntry("firstname", "Duckling").containsEntry("version", 10L); assertThat(document).containsEntry("firstname", "Duckling").containsEntry("version", 10L);
} }
public interface VersionedPersonRepository extends CrudRepository<VersionedPersonWithCounter, String> { interface VersionedPersonRepository extends CrudRepository<VersionedPersonWithCounter, String> {
@Update("{ '$set': { 'firstname' : ?0 } }") @Update("{ '$set': { 'firstname' : ?0 } }")
int findAndSetFirstnameToLastnameByLastname(String lastname); int findAndSetFirstnameToLastnameByLastname(String lastname);
@ -156,5 +162,7 @@ public class VersionedPersonRepositoryIntegrationTests {
public void setCounter(int counter) { public void setCounter(int counter) {
this.counter = counter; this.counter = counter;
} }
} }
} }

Loading…
Cancel
Save