Skip to content

Commit f7549f7

Browse files
jakubwladyslawchristophstrobl
authored andcommitted
Add support for replaceOne operation.
Add replace methods to MongoOperations and MongoTemplate that allow to replace the first matching document with a given value. Closes: #4462 Original Pull Request: #4463
1 parent 66c4a3b commit f7549f7

File tree

6 files changed

+353
-25
lines changed

6 files changed

+353
-25
lines changed

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

+116
Original file line numberDiff line numberDiff line change
@@ -1753,6 +1753,122 @@ default long exactCount(Query query, String collectionName) {
17531753
*/
17541754
<T> List<T> findAllAndRemove(Query query, Class<T> entityClass, String collectionName);
17551755

1756+
/**
1757+
* Triggers <a href="https://docs.mongodb.com/manual/reference/method/db.collection.replaceOne/">replaceOne</a> to
1758+
* replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document.
1759+
* <br />
1760+
* The collection name is derived from the {@literal replacement} type. <br />
1761+
* Options are defaulted to {@link ReplaceOptions#empty()}. <br />
1762+
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
1763+
*
1764+
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
1765+
* fields specification. Must not be {@literal null}.
1766+
* @param replacement the replacement document. Must not be {@literal null}.
1767+
* @return the {@link UpdateResult} which lets you access the results of the previous replacement.
1768+
* @throws org.springframework.data.mapping.MappingException if the collection name cannot be
1769+
* {@link #getCollectionName(Class) derived} from the given replacement value.
1770+
*/
1771+
default <T> UpdateResult replace(Query query, T replacement) {
1772+
return replace(query, replacement, ReplaceOptions.empty());
1773+
}
1774+
1775+
/**
1776+
* Triggers <a href="https://docs.mongodb.com/manual/reference/method/db.collection.replaceOne/">replaceOne</a> to
1777+
* replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement}
1778+
* document.<br />
1779+
* Options are defaulted to {@link ReplaceOptions#empty()}. <br />
1780+
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
1781+
*
1782+
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
1783+
* fields specification. Must not be {@literal null}.
1784+
* @param replacement the replacement document. Must not be {@literal null}.
1785+
* @param collectionName the collection to query. Must not be {@literal null}.
1786+
* @return the {@link UpdateResult} which lets you access the results of the previous replacement.
1787+
* @throws org.springframework.data.mapping.MappingException if the collection name cannot be
1788+
* {@link #getCollectionName(Class) derived} from the given replacement value.
1789+
*/
1790+
default <T> UpdateResult replace(Query query, T replacement, String collectionName) {
1791+
return replace(query, replacement, ReplaceOptions.empty(), collectionName);
1792+
}
1793+
1794+
/**
1795+
* Triggers <a href="https://docs.mongodb.com/manual/reference/method/db.collection.replaceOne/">replaceOne</a> to
1796+
* replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
1797+
* taking {@link ReplaceOptions} into account.<br />
1798+
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
1799+
*
1800+
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
1801+
* fields specification. Must not be {@literal null}.
1802+
* @param replacement the replacement document. Must not be {@literal null}.
1803+
* @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
1804+
* @return the {@link UpdateResult} which lets you access the results of the previous replacement.
1805+
* @throws org.springframework.data.mapping.MappingException if the collection name cannot be
1806+
* {@link #getCollectionName(Class) derived} from the given replacement value.
1807+
*/
1808+
default <T> UpdateResult replace(Query query, T replacement, ReplaceOptions options) {
1809+
return replace(query, replacement, options, getCollectionName(ClassUtils.getUserClass(replacement)));
1810+
}
1811+
1812+
/**
1813+
* Triggers <a href="https://docs.mongodb.com/manual/reference/method/db.collection.replaceOne/">replaceOne</a> to
1814+
* replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
1815+
* taking {@link ReplaceOptions} into account.<br />
1816+
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
1817+
*
1818+
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
1819+
* fields specification. Must not be {@literal null}.
1820+
* @param replacement the replacement document. Must not be {@literal null}.
1821+
* @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
1822+
* @return the {@link UpdateResult} which lets you access the results of the previous replacement.
1823+
* @throws org.springframework.data.mapping.MappingException if the collection name cannot be
1824+
* {@link #getCollectionName(Class) derived} from the given replacement value.
1825+
*/
1826+
default <T> UpdateResult replace(Query query, T replacement, ReplaceOptions options, String collectionName) {
1827+
1828+
Assert.notNull(replacement, "Replacement must not be null");
1829+
return replace(query, replacement, options, (Class<T>) ClassUtils.getUserClass(replacement), collectionName);
1830+
}
1831+
1832+
/**
1833+
* Triggers <a href="https://docs.mongodb.com/manual/reference/method/db.collection.replaceOne/">replaceOne</a> to
1834+
* replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
1835+
* taking {@link ReplaceOptions} into account.<br />
1836+
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
1837+
*
1838+
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
1839+
* fields specification. Must not be {@literal null}.
1840+
* @param replacement the replacement document. Must not be {@literal null}.
1841+
* @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
1842+
* @param entityType the type used for mapping the {@link Query} to domain type fields and deriving the collection
1843+
* from. Must not be {@literal null}.
1844+
* @return the {@link UpdateResult} which lets you access the results of the previous replacement.
1845+
* @throws org.springframework.data.mapping.MappingException if the collection name cannot be
1846+
* {@link #getCollectionName(Class) derived} from the given replacement value.
1847+
*/
1848+
default <S> UpdateResult replace(Query query, S replacement, ReplaceOptions options, Class<S> entityType) {
1849+
1850+
return replace(query, replacement, options, entityType, getCollectionName(ClassUtils.getUserClass(entityType)));
1851+
}
1852+
1853+
/**
1854+
* Triggers <a href="https://docs.mongodb.com/manual/reference/method/db.collection.replaceOne/">replaceOne</a> to
1855+
* replace a single document matching {@link Criteria} of given {@link Query} with the {@code replacement} document
1856+
* taking {@link ReplaceOptions} into account.<br />
1857+
* <strong>NOTE:</strong> The replacement entity must not hold an {@literal id}.
1858+
*
1859+
* @param query the {@link Query} class that specifies the {@link Criteria} used to find a record and also an optional
1860+
* fields specification. Must not be {@literal null}.
1861+
* @param replacement the replacement document. Must not be {@literal null}.
1862+
* @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
1863+
* @param entityType the type used for mapping the {@link Query} to domain type fields. Must not be {@literal null}.
1864+
* @param collectionName the collection to query. Must not be {@literal null}.
1865+
* @return the {@link UpdateResult} which lets you access the results of the previous replacement.
1866+
* @throws org.springframework.data.mapping.MappingException if the collection name cannot be
1867+
* {@link #getCollectionName(Class) derived} from the given replacement value.
1868+
*/
1869+
<S> UpdateResult replace(Query query, S replacement, ReplaceOptions options, Class<S> entityType,
1870+
String collectionName);
1871+
17561872
/**
17571873
* Returns the underlying {@link MongoConverter}.
17581874
*

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

+82-2
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@
178178
* @author Anton Barkan
179179
* @author Bartłomiej Mazur
180180
* @author Michael Krog
181+
* @author Jakub Zurawa
181182
*/
182183
public class MongoTemplate
183184
implements MongoOperations, ApplicationContextAware, IndexOperationsProvider, ReadPreferenceAware {
@@ -1618,7 +1619,7 @@ protected Object saveDocument(String collectionName, Document dbDoc, Class<?> en
16181619
}
16191620
}
16201621

1621-
collectionToUse.replaceOne(filter, replacement, new ReplaceOptions().upsert(true));
1622+
collectionToUse.replaceOne(filter, replacement, new com.mongodb.client.model.ReplaceOptions().upsert(true));
16221623
}
16231624
return mapped.getId();
16241625
});
@@ -1749,7 +1750,7 @@ protected UpdateResult doUpdate(String collectionName, Query query, UpdateDefini
17491750
}
17501751
}
17511752

1752-
ReplaceOptions replaceOptions = updateContext.getReplaceOptions(entityClass);
1753+
com.mongodb.client.model.ReplaceOptions replaceOptions = updateContext.getReplaceOptions(entityClass);
17531754
return collection.replaceOne(filter, updateObj, replaceOptions);
17541755
} else {
17551756
return multi ? collection.updateMany(queryObj, updateObj, opts)
@@ -3421,6 +3422,56 @@ public MongoDatabaseFactory getMongoDatabaseFactory() {
34213422
return mongoDbFactory;
34223423
}
34233424

3425+
@Override
3426+
public <S> UpdateResult replace(Query query, S replacement, ReplaceOptions options, Class<S> entityType,
3427+
String collectionName) {
3428+
Assert.notNull(query, "Query must not be null");
3429+
Assert.notNull(replacement, "Replacement must not be null");
3430+
Assert.notNull(options, "Options must not be null Use ReplaceOptions#empty() instead");
3431+
Assert.notNull(entityType, "EntityType must not be null");
3432+
Assert.notNull(collectionName, "CollectionName must not be null");
3433+
3434+
Assert.isTrue(query.getLimit() <= 1, "Query must not define a limit other than 1 ore none");
3435+
Assert.isTrue(query.getSkip() <= 0, "Query must not define skip");
3436+
3437+
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityType);
3438+
QueryContext queryContext = queryOperations.createQueryContext(query);
3439+
3440+
CollectionPreparerDelegate collectionPreparer = createDelegate(query);
3441+
Document mappedQuery = queryContext.getMappedQuery(entity);
3442+
3443+
replacement = maybeCallBeforeConvert(replacement, collectionName);
3444+
Document mappedReplacement = operations.forEntity(replacement).toMappedDocument(this.mongoConverter).getDocument();
3445+
maybeCallBeforeSave(replacement, mappedReplacement, collectionName);
3446+
3447+
maybeEmitEvent(new BeforeSaveEvent<>(replacement, mappedReplacement, collectionName));
3448+
maybeCallBeforeSave(replacement, mappedReplacement, collectionName);
3449+
3450+
UpdateResult result = doReplace(options, entityType, collectionName, queryContext, collectionPreparer, mappedQuery,
3451+
mappedReplacement);
3452+
3453+
if (result.wasAcknowledged()) {
3454+
maybeEmitEvent(new AfterSaveEvent<>(replacement, mappedReplacement, collectionName));
3455+
maybeCallAfterSave(replacement, mappedReplacement, collectionName);
3456+
}
3457+
3458+
return result;
3459+
}
3460+
3461+
private <S> UpdateResult doReplace(ReplaceOptions options, Class<S> entityType, String collectionName,
3462+
QueryContext queryContext, CollectionPreparerDelegate collectionPreparer, Document mappedQuery,
3463+
Document replacement) {
3464+
ReplaceCallback replaceCallback = new ReplaceCallback(collectionPreparer, mappedQuery, replacement,
3465+
queryContext.getCollation(entityType).orElse(null), options);
3466+
if (LOGGER.isDebugEnabled()) {
3467+
LOGGER.debug(
3468+
String.format("findAndReplace using query: %s for class: %s and replacement: %s " + "in collection: %s",
3469+
serializeToJsonSafely(mappedQuery), entityType, serializeToJsonSafely(replacement), collectionName));
3470+
}
3471+
3472+
return execute(collectionName, replaceCallback);
3473+
}
3474+
34243475
/**
34253476
* A {@link CloseableIterator} that is backed by a MongoDB {@link MongoCollection}.
34263477
*
@@ -3555,4 +3606,33 @@ interface CountExecution {
35553606
long countDocuments(CollectionPreparer collectionPreparer, String collection, Document filter,
35563607
CountOptions options);
35573608
}
3609+
3610+
private static class ReplaceCallback implements CollectionCallback<UpdateResult> {
3611+
3612+
private final CollectionPreparer<MongoCollection<Document>> collectionPreparer;
3613+
private final Document query;
3614+
private final Document update;
3615+
private final @Nullable com.mongodb.client.model.Collation collation;
3616+
private final ReplaceOptions options;
3617+
3618+
ReplaceCallback(CollectionPreparer<MongoCollection<Document>> collectionPreparer, Document query, Document update,
3619+
@Nullable com.mongodb.client.model.Collation collation, ReplaceOptions options) {
3620+
this.collectionPreparer = collectionPreparer;
3621+
this.query = query;
3622+
this.update = update;
3623+
this.options = options;
3624+
this.collation = collation;
3625+
}
3626+
3627+
@Override
3628+
public UpdateResult doInCollection(MongoCollection<Document> collection)
3629+
throws MongoException, DataAccessException {
3630+
com.mongodb.client.model.ReplaceOptions opts = new com.mongodb.client.model.ReplaceOptions();
3631+
opts.collation(collation);
3632+
3633+
opts.upsert(options.isUpsert());
3634+
3635+
return collectionPreparer.prepare(collection).replaceOne(query, update, opts);
3636+
}
3637+
}
35583638
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -1782,7 +1782,7 @@ protected Mono<UpdateResult> doUpdate(String collectionName, Query query, @Nulla
17821782
deferredFilter = Mono.just(filter);
17831783
}
17841784

1785-
ReplaceOptions replaceOptions = updateContext.getReplaceOptions(entityClass);
1785+
com.mongodb.client.model.ReplaceOptions replaceOptions = updateContext.getReplaceOptions(entityClass);
17861786
return deferredFilter.flatMap(it -> Mono.from(collectionToUse.replaceOne(it, updateObj, replaceOptions)));
17871787
}
17881788

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Options for
3+
* <a href="https://docs.mongodb.com/manual/reference/method/db.collection.replaceOne/">replaceOne</a>.
4+
* <br />
5+
* Defaults to
6+
* <dl>
7+
* <dt>upsert</dt>
8+
* <dd>false</dd>
9+
* </dl>
10+
*
11+
* @author Jakub Zurawa
12+
*/
13+
package org.springframework.data.mongodb.core;
14+
15+
public class ReplaceOptions {
16+
private boolean upsert;
17+
18+
private static final ReplaceOptions NONE = new ReplaceOptions() {
19+
20+
private static final String ERROR_MSG = "ReplaceOptions.none() cannot be changed; Please use ReplaceOptions.options() instead";
21+
22+
@Override
23+
public ReplaceOptions upsert() {
24+
throw new UnsupportedOperationException(ERROR_MSG);
25+
}
26+
};
27+
28+
/**
29+
* Static factory method to create a {@link ReplaceOptions} instance.
30+
* <dl>
31+
* <dt>upsert</dt>
32+
* <dd>false</dd>
33+
* </dl>
34+
*
35+
* @return new instance of {@link ReplaceOptions}.
36+
*/
37+
public static ReplaceOptions options() {
38+
return new ReplaceOptions();
39+
}
40+
41+
/**
42+
* Static factory method returning an unmodifiable {@link ReplaceOptions} instance.
43+
*
44+
* @return unmodifiable {@link ReplaceOptions} instance.
45+
* @since 2.2
46+
*/
47+
public static ReplaceOptions none() {
48+
return NONE;
49+
}
50+
51+
/**
52+
* Static factory method to create a {@link ReplaceOptions} instance with
53+
* <dl>
54+
* <dt>upsert</dt>
55+
* <dd>false</dd>
56+
* </dl>
57+
*
58+
* @return new instance of {@link ReplaceOptions}.
59+
*/
60+
public static ReplaceOptions empty() {
61+
return new ReplaceOptions();
62+
}
63+
64+
/**
65+
* Insert a new document if not exists.
66+
*
67+
* @return this.
68+
*/
69+
public ReplaceOptions upsert() {
70+
71+
this.upsert = true;
72+
return this;
73+
}
74+
75+
/**
76+
* Get the bit indicating if to create a new document if not exists.
77+
*
78+
* @return {@literal true} if set.
79+
*/
80+
public boolean isUpsert() {
81+
return upsert;
82+
}
83+
84+
}

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

+16
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
* @author Mark Paluch
117117
* @author Laszlo Csontos
118118
* @author duozhilin
119+
* @author Jakub Zurawa
119120
*/
120121
@ExtendWith(MongoClientExtension.class)
121122
public class MongoTemplateTests {
@@ -3872,6 +3873,21 @@ void shouldExecuteQueryWithExpression() {
38723873
assertThat(loaded).isEqualTo(source2);
38733874
}
38743875

3876+
@Test // GH-4300
3877+
public void replaceShouldReplaceDocument() {
3878+
3879+
org.bson.Document doc = new org.bson.Document("foo", "bar");
3880+
String collectionName = "replace";
3881+
template.save(doc, collectionName);
3882+
3883+
org.bson.Document replacement = new org.bson.Document("foo", "baz");
3884+
UpdateResult updateResult = template.replace(query(where("foo").is("bar")), replacement, ReplaceOptions.options(),
3885+
collectionName);
3886+
3887+
assertThat(updateResult.wasAcknowledged()).isTrue();
3888+
assertThat(template.findOne(query(where("foo").is("baz")), org.bson.Document.class, collectionName)).isNotNull();
3889+
}
3890+
38753891
private AtomicReference<ImmutableVersioned> createAfterSaveReference() {
38763892

38773893
AtomicReference<ImmutableVersioned> saved = new AtomicReference<>();

0 commit comments

Comments
 (0)