Skip to content

Commit d68636f

Browse files
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) new throw an OptimisticLockingFailureException when the object to remove is versioned and the version of the given entity does not match the one within the store. If no document with matching _id can be found the remove operation passes without an error. 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(…
1 parent 21f7e99 commit d68636f

File tree

12 files changed

+512
-22
lines changed

12 files changed

+512
-22
lines changed

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,21 @@ 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+
210+
if (isVersionedEntity()) {
211+
return getQueryForVersion();
212+
}
213+
214+
return getByIdQuery();
215+
}
216+
202217
/**
203218
* Returns the {@link Query} to find the entity in its current version.
204219
*
@@ -490,10 +505,10 @@ public Query getByIdQuery() {
490505
public Query getQueryForVersion() {
491506

492507
MongoPersistentProperty idProperty = entity.getRequiredIdProperty();
493-
MongoPersistentProperty property = entity.getRequiredVersionProperty();
508+
MongoPersistentProperty versionProperty = entity.getRequiredVersionProperty();
494509

495510
return new Query(Criteria.where(idProperty.getName()).is(getId())//
496-
.and(property.getName()).is(getVersion()));
511+
.and(versionProperty.getName()).is(getVersion()));
497512
}
498513

499514
/*

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: 42 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,40 @@ 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 && entity != null && entity.hasVersionProperty() && result.wasAcknowledged()) {
1744+
1745+
if (result.getDeletedCount() == 0) {
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)
1752+
.projection(Projections.include("_id", versionFieldName)).limit(1).iterator();
1753+
1754+
if (it.hasNext()) {
1755+
1756+
Document source = it.next();
1757+
1758+
throw new OptimisticLockingFailureException(String
1759+
.format("The entity with id %s in %s has changed and cannot be deleted! " + System.lineSeparator() + //
1760+
"Expected version %s but was %s.", source.get("_id"), collectionName, removeQuery.get(versionFieldName),
1761+
source.get(versionFieldName)));
1762+
}
1763+
1764+
}
1765+
}
1766+
}
17301767
});
17311768
}
17321769

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

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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,47 @@ 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 (entity != null && entity.hasVersionProperty()) {
1830+
1831+
if (deleteResult.wasAcknowledged() && deleteResult.getDeletedCount() == 0) {
1832+
1833+
if (query.getQueryObject().containsKey(entity.getVersionProperty().getFieldName())) {
1834+
1835+
return execute(collectionName, (coll -> {
1836+
1837+
String versionFieldName = entity.getVersionProperty().getFieldName();
1838+
1839+
Document daQuery = new Document(removeQuery);
1840+
daQuery.remove(versionFieldName);
1841+
1842+
Publisher<Document> publisher = coll.find(daQuery)
1843+
.projection(Projections.include("_id", entity.getVersionProperty().getFieldName())).limit(1).first();
1844+
1845+
return Mono.from(publisher) //
1846+
.defaultIfEmpty(new Document()) //
1847+
.flatMap(it -> {
1848+
1849+
if (it.isEmpty()) {
1850+
return Mono.just(deleteResult);
1851+
}
1852+
return Mono.error(new OptimisticLockingFailureException(String.format(
1853+
"The entity with id %s in %s has changed and cannot be deleted! " + System.lineSeparator() + //
1854+
"Expected version %s but was %s.", it.get("_id"), collectionName, removeQuery.get(versionFieldName),
1855+
it.get(versionFieldName))));
1856+
1857+
});
1858+
})).next();
1859+
}
1860+
}
18261861
}
18271862

1828-
}).doOnNext(deleteResult -> maybeEmitEvent(new AfterDeleteEvent<>(queryObject, entityClass, collectionName)))
1863+
return Mono.just(deleteResult);
1864+
}).doOnNext(it -> maybeEmitEvent(new AfterDeleteEvent<>(queryObject, entityClass, collectionName))) //
18291865
.next();
18301866
}
18311867

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
/*

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

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@
4343
import java.util.stream.Collectors;
4444
import java.util.stream.IntStream;
4545

46+
import org.assertj.core.api.Assertions;
4647
import org.bson.types.ObjectId;
4748
import org.hamcrest.collection.IsMapContaining;
48-
import org.hamcrest.core.IsSame;
4949
import org.joda.time.DateTime;
5050
import org.junit.After;
5151
import org.junit.Before;
@@ -1646,6 +1646,62 @@ public void initializesVersionOnInsert() {
16461646
assertThat(person.version, is(0));
16471647
}
16481648

1649+
@Test // DATAMONGO-2195
1650+
public void removeEntityWithMatchingVersion() {
1651+
1652+
PersonWithVersionPropertyOfTypeInteger person = new PersonWithVersionPropertyOfTypeInteger();
1653+
person.firstName = "Dave";
1654+
1655+
template.insert(person);
1656+
assertThat(person.version, is(0));
1657+
1658+
template.remove(person);
1659+
assertThat(template.count(new Query(), Person.class)).isZero();
1660+
}
1661+
1662+
@Test // DATAMONGO-2195
1663+
public void removeEntityWithoutMatchingVersionThrowsOptimisticLockingFailureException() {
1664+
1665+
PersonWithVersionPropertyOfTypeInteger person = new PersonWithVersionPropertyOfTypeInteger();
1666+
person.firstName = "Dave";
1667+
1668+
template.insert(person);
1669+
assertThat(person.version, is(0));
1670+
template.update(PersonWithVersionPropertyOfTypeInteger.class).matching(query(where("id").is(person.id)))
1671+
.apply(new Update().set("firstName", "Walter")).first();
1672+
1673+
Assertions.assertThatThrownBy(() -> template.remove(person)).isInstanceOf(OptimisticLockingFailureException.class)
1674+
.hasMessageContaining("Expected version 0 but was 1");
1675+
assertThat(template.count(new Query(), PersonWithVersionPropertyOfTypeInteger.class)).isOne();
1676+
}
1677+
1678+
@Test // DATAMONGO-2195
1679+
public void removeNonExistingVersionedEntityJustPasses() {
1680+
1681+
PersonWithVersionPropertyOfTypeInteger person = new PersonWithVersionPropertyOfTypeInteger();
1682+
person.id = "id-1";
1683+
person.firstName = "Dave";
1684+
person.version = 10;
1685+
1686+
assertThat(template.remove(person).getDeletedCount()).isZero();
1687+
}
1688+
1689+
@Test // DATAMONGO-2195
1690+
@DirtiesContext
1691+
public void removeEntityWithoutMatchingVersionWhenUsingUnaknowledgedWriteDoesNotThrowsOptimisticLockingFailureException() {
1692+
1693+
PersonWithVersionPropertyOfTypeInteger person = new PersonWithVersionPropertyOfTypeInteger();
1694+
person.firstName = "Dave";
1695+
1696+
template.insert(person);
1697+
assertThat(person.version, is(0));
1698+
template.update(PersonWithVersionPropertyOfTypeInteger.class).matching(query(where("id").is(person.id)))
1699+
.apply(new Update().set("firstName", "Walter")).first();
1700+
1701+
template.setWriteConcern(WriteConcern.UNACKNOWLEDGED);
1702+
assertThat(template.remove(person).wasAcknowledged()).isFalse();
1703+
}
1704+
16491705
@Test // DATAMONGO-588
16501706
public void initializesVersionOnBatchInsert() {
16511707

0 commit comments

Comments
 (0)