Skip to content

Commit e7286a1

Browse files
Add support for replaceOne operation
1 parent a44e240 commit e7286a1

File tree

5 files changed

+363
-24
lines changed

5 files changed

+363
-24
lines changed

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

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

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

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
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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>returnNew</dt>
32+
* <dd>false</dd>
33+
* <dt>upsert</dt>
34+
* <dd>false</dd>
35+
* </dl>
36+
*
37+
* @return new instance of {@link ReplaceOptions}.
38+
*/
39+
public static ReplaceOptions options() {
40+
return new ReplaceOptions();
41+
}
42+
43+
/**
44+
* Static factory method returning an unmodifiable {@link ReplaceOptions} instance.
45+
*
46+
* @return unmodifiable {@link ReplaceOptions} instance.
47+
* @since 2.2
48+
*/
49+
public static ReplaceOptions none() {
50+
return NONE;
51+
}
52+
53+
/**
54+
* Static factory method to create a {@link ReplaceOptions} instance with
55+
* <dl>
56+
* <dt>returnNew</dt>
57+
* <dd>false</dd>
58+
* <dt>upsert</dt>
59+
* <dd>false</dd>
60+
* </dl>
61+
*
62+
* @return new instance of {@link ReplaceOptions}.
63+
*/
64+
public static ReplaceOptions empty() {
65+
return new ReplaceOptions();
66+
}
67+
68+
/**
69+
* Insert a new document if not exists.
70+
*
71+
* @return this.
72+
*/
73+
public ReplaceOptions upsert() {
74+
75+
this.upsert = true;
76+
return this;
77+
}
78+
79+
/**
80+
* Get the bit indicating if to create a new document if not exists.
81+
*
82+
* @return {@literal true} if set.
83+
*/
84+
public boolean isUpsert() {
85+
return upsert;
86+
}
87+
88+
}

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)