Skip to content

Commit 4ecf20c

Browse files
christophstroblmp911de
authored andcommitted
DATAMONGO-2195 - Consider version when removing an entity.
We now consider a potential @Version when removing an entity. MongoOperations#remove(Object) and MongoOperations#remove(Object, String) include the version of the object to remove if the entity is versioned. Opposed to save(Object), remove(Object) does not throw OptimisticLockingFailureException if a versioned entity could not be removed. This behavior is subject to be changed in a future release. Throwing OptimisticLockingFailureException on failed delete on Template API level was not introduced to not break existing application code. MongoRepository now also considers the entities version, following the very same logic as MongoOperations. To remove an entity without version check use MongoOperations#remove(Query,…) or MongoRepository#deleteById(…). Original pull request: #641.
1 parent e8a3b69 commit 4ecf20c

File tree

13 files changed

+584
-22
lines changed

13 files changed

+584
-22
lines changed

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,16 @@ interface Entity<T> {
199199
*/
200200
Query getByIdQuery();
201201

202+
/**
203+
* Returns the {@link Query} to remove an entity by its {@literal id} and if applicable {@literal version}.
204+
*
205+
* @return the {@link Query} to use for removing the entity. Never {@literal null}.
206+
* @since 2.2
207+
*/
208+
default Query getRemoveByQuery() {
209+
return isVersionedEntity() ? getQueryForVersion() : getByIdQuery();
210+
}
211+
202212
/**
203213
* Returns the {@link Query} to find the entity in its current version.
204214
*
@@ -490,10 +500,10 @@ public Query getByIdQuery() {
490500
public Query getQueryForVersion() {
491501

492502
MongoPersistentProperty idProperty = entity.getRequiredIdProperty();
493-
MongoPersistentProperty property = entity.getRequiredVersionProperty();
503+
MongoPersistentProperty versionProperty = entity.getRequiredVersionProperty();
494504

495505
return new Query(Criteria.where(idProperty.getName()).is(getId())//
496-
.and(property.getName()).is(getVersion()));
506+
.and(versionProperty.getName()).is(getVersion()));
497507
}
498508

499509
/*

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,19 +1350,29 @@ <S, T> T findAndReplace(Query query, S replacement, FindAndReplaceOptions option
13501350
UpdateResult updateMulti(Query query, Update update, Class<?> entityClass, String collectionName);
13511351

13521352
/**
1353-
* Remove the given object from the collection by id.
1353+
* Remove the given object from the collection by {@literal id} and (if applicable) its
1354+
* {@link org.springframework.data.annotation.Version}.
13541355
*
13551356
* @param object must not be {@literal null}.
13561357
* @return the {@link DeleteResult} which lets you access the results of the previous delete.
1358+
* @throws org.springframework.dao.OptimisticLockingFailureException if the given {@literal object} is
1359+
* {@link org.springframework.data.annotation.Version versioned} and the current version does not match the
1360+
* one within the store. If no document with matching {@literal _id} exists in the collection the operation
1361+
* completes without error.
13571362
*/
13581363
DeleteResult remove(Object object);
13591364

13601365
/**
1361-
* Removes the given object from the given collection.
1366+
* Removes the given object from the given collection by {@literal id} and (if applicable) its
1367+
* {@link org.springframework.data.annotation.Version}.
13621368
*
13631369
* @param object must not be {@literal null}.
13641370
* @param collectionName name of the collection where the objects will removed, must not be {@literal null} or empty.
13651371
* @return the {@link DeleteResult} which lets you access the results of the previous delete.
1372+
* @throws org.springframework.dao.OptimisticLockingFailureException if the given {@literal object} is
1373+
* {@link org.springframework.data.annotation.Version versioned} and the current version does not match the
1374+
* one within the store. If no document with matching {@literal _id} exists in the collection the operation
1375+
* completes without error.
13661376
*/
13671377
DeleteResult remove(Object object, String collectionName);
13681378

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

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,16 @@
6868
import org.springframework.data.mongodb.core.aggregation.Fields;
6969
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
7070
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
71-
import org.springframework.data.mongodb.core.convert.*;
71+
import org.springframework.data.mongodb.core.convert.DbRefResolver;
72+
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
73+
import org.springframework.data.mongodb.core.convert.JsonSchemaMapper;
74+
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
75+
import org.springframework.data.mongodb.core.convert.MongoConverter;
76+
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
77+
import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper;
78+
import org.springframework.data.mongodb.core.convert.MongoWriter;
79+
import org.springframework.data.mongodb.core.convert.QueryMapper;
80+
import org.springframework.data.mongodb.core.convert.UpdateMapper;
7281
import org.springframework.data.mongodb.core.index.IndexOperations;
7382
import org.springframework.data.mongodb.core.index.IndexOperationsProvider;
7483
import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher;
@@ -1638,9 +1647,7 @@ public DeleteResult remove(Object object) {
16381647

16391648
Assert.notNull(object, "Object must not be null!");
16401649

1641-
Query query = operations.forEntity(object).getByIdQuery();
1642-
1643-
return remove(query, object.getClass());
1650+
return remove(object, operations.determineCollectionName(object.getClass()));
16441651
}
16451652

16461653
@Override
@@ -1649,7 +1656,7 @@ public DeleteResult remove(Object object, String collectionName) {
16491656
Assert.notNull(object, "Object must not be null!");
16501657
Assert.hasText(collectionName, "Collection name must not be null or empty!");
16511658

1652-
Query query = operations.forEntity(object).getByIdQuery();
1659+
Query query = operations.forEntity(object).getRemoveByQuery();
16531660

16541661
return doRemove(collectionName, query, object.getClass(), false);
16551662
}
@@ -1723,10 +1730,36 @@ public DeleteResult doInCollection(MongoCollection<Document> collection)
17231730
DeleteResult result = multi ? collectionToUse.deleteMany(removeQuery, options)
17241731
: collectionToUse.deleteOne(removeQuery, options);
17251732

1733+
checkForOptimisticLockingFailures(result, removeQuery, collectionToUse);
1734+
17261735
maybeEmitEvent(new AfterDeleteEvent<>(queryObject, entityClass, collectionName));
17271736

17281737
return result;
17291738
}
1739+
1740+
private void checkForOptimisticLockingFailures(DeleteResult result, Document removeQuery,
1741+
MongoCollection<Document> collectionToUse) {
1742+
1743+
if (multi || !ResultOperations.isUndecidedDeleteResult(result, removeQuery, entity)) {
1744+
return;
1745+
}
1746+
1747+
String versionFieldName = entity.getVersionProperty().getFieldName();
1748+
Document idQuery = new Document(removeQuery);
1749+
idQuery.remove(versionFieldName);
1750+
1751+
Iterator<Document> it = collectionToUse.find(idQuery).projection(Projections.include("_id", versionFieldName))
1752+
.limit(1).iterator();
1753+
1754+
if (it.hasNext()) {
1755+
1756+
Document source = it.next();
1757+
throw ResultOperations.newDeleteVersionedOptimisticLockingException(source.get("_id"), collectionName,
1758+
removeQuery.get(versionFieldName), source.get(versionFieldName));
1759+
1760+
}
1761+
1762+
}
17301763
});
17311764
}
17321765

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

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1688,7 +1688,7 @@ private boolean containsVersionProperty(Document document, @Nullable MongoPersis
16881688
return false;
16891689
}
16901690

1691-
return document.containsKey(persistentEntity.getRequiredIdProperty().getFieldName());
1691+
return document.containsKey(persistentEntity.getRequiredVersionProperty().getFieldName());
16921692
}
16931693

16941694
/*
@@ -1717,7 +1717,7 @@ public Mono<DeleteResult> remove(Object object) {
17171717

17181718
Assert.notNull(object, "Object must not be null!");
17191719

1720-
return remove(operations.forEntity(object).getByIdQuery(), object.getClass());
1720+
return remove(operations.forEntity(object).getRemoveByQuery(), object.getClass());
17211721
}
17221722

17231723
/*
@@ -1729,7 +1729,7 @@ public Mono<DeleteResult> remove(Object object, String collectionName) {
17291729
Assert.notNull(object, "Object must not be null!");
17301730
Assert.hasText(collectionName, "Collection name must not be null or empty!");
17311731

1732-
return doRemove(collectionName, operations.forEntity(object).getByIdQuery(), object.getClass());
1732+
return doRemove(collectionName, operations.forEntity(object).getRemoveByQuery(), object.getClass());
17331733
}
17341734

17351735
private void assertUpdateableIdIfNotSet(Object value) {
@@ -1787,15 +1787,14 @@ protected <T> Mono<DeleteResult> doRemove(String collectionName, Query query, @N
17871787

17881788
Document queryObject = query.getQueryObject();
17891789
MongoPersistentEntity<?> entity = getPersistentEntity(entityClass);
1790+
Document removeQuery = queryMapper.getMappedObject(queryObject, entity);
17901791

17911792
return execute(collectionName, collection -> {
17921793

1793-
Document removeQuey = queryMapper.getMappedObject(queryObject, entity);
1794-
1795-
maybeEmitEvent(new BeforeDeleteEvent<>(removeQuey, entityClass, collectionName));
1794+
maybeEmitEvent(new BeforeDeleteEvent<>(removeQuery, entityClass, collectionName));
17961795

17971796
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.REMOVE, collectionName, entityClass,
1798-
null, removeQuey);
1797+
null, removeQuery);
17991798

18001799
DeleteOptions deleteOptions = new DeleteOptions();
18011800
query.getCollation().map(Collation::toMongoCollation).ifPresent(deleteOptions::collation);
@@ -1805,13 +1804,13 @@ protected <T> Mono<DeleteResult> doRemove(String collectionName, Query query, @N
18051804

18061805
if (LOGGER.isDebugEnabled()) {
18071806
LOGGER.debug("Remove using query: {} in collection: {}.",
1808-
new Object[] { serializeToJsonSafely(removeQuey), collectionName });
1807+
new Object[] { serializeToJsonSafely(removeQuery), collectionName });
18091808
}
18101809

18111810
if (query.getLimit() > 0 || query.getSkip() > 0) {
18121811

18131812
FindPublisher<Document> cursor = new QueryFindPublisherPreparer(query, entityClass)
1814-
.prepare(collection.find(removeQuey)) //
1813+
.prepare(collection.find(removeQuery)) //
18151814
.projection(MappedDocument.getIdOnlyProjection());
18161815

18171816
return Flux.from(cursor) //
@@ -1822,10 +1821,39 @@ protected <T> Mono<DeleteResult> doRemove(String collectionName, Query query, @N
18221821
return collectionToUse.deleteMany(MappedDocument.getIdIn(val), deleteOptions);
18231822
});
18241823
} else {
1825-
return collectionToUse.deleteMany(removeQuey, deleteOptions);
1824+
return collectionToUse.deleteMany(removeQuery, deleteOptions);
1825+
}
1826+
1827+
}).flatMap(deleteResult -> {
1828+
1829+
if (!ResultOperations.isUndecidedDeleteResult(deleteResult, removeQuery, entity)) {
1830+
return Mono.just(deleteResult);
18261831
}
18271832

1828-
}).doOnNext(deleteResult -> maybeEmitEvent(new AfterDeleteEvent<>(queryObject, entityClass, collectionName)))
1833+
return execute(collectionName, (coll -> {
1834+
1835+
String versionFieldName = entity.getVersionProperty().getFieldName();
1836+
1837+
Document queryWithoutVersion = new Document(removeQuery);
1838+
queryWithoutVersion.remove(versionFieldName);
1839+
1840+
Publisher<Document> publisher = coll.find(queryWithoutVersion)
1841+
.projection(Projections.include("_id", entity.getVersionProperty().getFieldName())).limit(1).first();
1842+
1843+
return Mono.from(publisher) //
1844+
.defaultIfEmpty(new Document()) //
1845+
.flatMap(it -> {
1846+
1847+
if (it.isEmpty()) {
1848+
return Mono.just(deleteResult);
1849+
}
1850+
1851+
return Mono.error(ResultOperations.newDeleteVersionedOptimisticLockingException(it.get("_id"),
1852+
collectionName, removeQuery.get(versionFieldName), it.get(versionFieldName)));
1853+
});
1854+
})).next();
1855+
1856+
}).doOnNext(it -> maybeEmitEvent(new AfterDeleteEvent<>(queryObject, entityClass, collectionName))) //
18291857
.next();
18301858
}
18311859

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2019 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;
17+
18+
import org.bson.Document;
19+
import org.springframework.dao.OptimisticLockingFailureException;
20+
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
21+
import org.springframework.lang.Nullable;
22+
23+
import com.mongodb.client.result.DeleteResult;
24+
import com.mongodb.client.result.UpdateResult;
25+
26+
/**
27+
* {@link ResultOperations} is intended to share logic depending on {@link DeleteResult} and
28+
* {@link com.mongodb.client.result.UpdateResult} between reactive and imperative implementations.
29+
*
30+
* @author Christoph Strobl
31+
* @since 2.2
32+
*/
33+
class ResultOperations {
34+
35+
/**
36+
* Decide if a given {@link DeleteResult} needs further inspection. Returns {@literal true} if the delete of a
37+
* {@link org.springframework.data.annotation.Version versioned} entity did not remove any documents although the
38+
* operation has been {@link DeleteResult#wasAcknowledged() acknowledged}.
39+
*
40+
* @param deleteResult must not be {@literal null}.
41+
* @param query the actual query used for the delete operation.
42+
* @param entity can be {@literal null}.
43+
* @return {@literal true} if it cannot be decided if nothing got deleted because of a potential
44+
* {@link org.springframework.data.annotation.Version version} mismatch, or if the document did not exist in
45+
* first place.
46+
*/
47+
static boolean isUndecidedDeleteResult(DeleteResult deleteResult, org.bson.Document query,
48+
@Nullable MongoPersistentEntity<?> entity) {
49+
50+
if (!deleteResult.wasAcknowledged() || deleteResult.getDeletedCount() > 0) {
51+
return false;
52+
}
53+
54+
if (entity == null) {
55+
return false;
56+
}
57+
58+
if (!entity.hasVersionProperty()) {
59+
return false;
60+
}
61+
62+
return containsVersionProperty(query, entity);
63+
}
64+
65+
static OptimisticLockingFailureException newDeleteVersionedOptimisticLockingException(Object id,
66+
String collectionName, Object expectedVersion, Object actualVersion) {
67+
68+
throw new OptimisticLockingFailureException(
69+
String.format("The entity with id %s in %s has changed and cannot be deleted! " + System.lineSeparator() + //
70+
"Expected version %s but was %s.", id, collectionName, expectedVersion, actualVersion));
71+
}
72+
73+
private static boolean containsVersionProperty(Document document, MongoPersistentEntity<?> entity) {
74+
return document.containsKey(entity.getRequiredVersionProperty().getFieldName());
75+
}
76+
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ public void delete(T entity) {
161161

162162
Assert.notNull(entity, "The given entity must not be null!");
163163

164-
deleteById(entityInformation.getRequiredId(entity));
164+
mongoOperations.remove(entity, entityInformation.getCollectionName());
165165
}
166166

167167
/*

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ public Mono<Void> delete(T entity) {
355355

356356
Assert.notNull(entity, "The given entity must not be null!");
357357

358-
return deleteById(entityInformation.getRequiredId(entity));
358+
return mongoOperations.remove(entity, entityInformation.getCollectionName()).then();
359359
}
360360

361361
/*

0 commit comments

Comments
 (0)