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.
This commit is contained in:
Christoph Strobl
2019-02-04 12:48:02 +01:00
committed by Mark Paluch
parent e8a3b6935e
commit 4ecf20ce4c
13 changed files with 584 additions and 22 deletions

View File

@@ -199,6 +199,16 @@ class EntityOperations {
*/
Query getByIdQuery();
/**
* Returns the {@link Query} to remove an entity by its {@literal id} and if applicable {@literal version}.
*
* @return the {@link Query} to use for removing the entity. Never {@literal null}.
* @since 2.2
*/
default Query getRemoveByQuery() {
return isVersionedEntity() ? getQueryForVersion() : getByIdQuery();
}
/**
* Returns the {@link Query} to find the entity in its current version.
*
@@ -490,10 +500,10 @@ class EntityOperations {
public Query getQueryForVersion() {
MongoPersistentProperty idProperty = entity.getRequiredIdProperty();
MongoPersistentProperty property = entity.getRequiredVersionProperty();
MongoPersistentProperty versionProperty = entity.getRequiredVersionProperty();
return new Query(Criteria.where(idProperty.getName()).is(getId())//
.and(property.getName()).is(getVersion()));
.and(versionProperty.getName()).is(getVersion()));
}
/*

View File

@@ -1350,19 +1350,29 @@ public interface MongoOperations extends FluentMongoOperations {
UpdateResult updateMulti(Query query, Update update, Class<?> entityClass, String collectionName);
/**
* Remove the given object from the collection by id.
* Remove the given object from the collection by {@literal id} and (if applicable) its
* {@link org.springframework.data.annotation.Version}.
*
* @param object must not be {@literal null}.
* @return the {@link DeleteResult} which lets you access the results of the previous delete.
* @throws org.springframework.dao.OptimisticLockingFailureException if the given {@literal object} is
* {@link org.springframework.data.annotation.Version versioned} and the current version does not match the
* one within the store. If no document with matching {@literal _id} exists in the collection the operation
* completes without error.
*/
DeleteResult remove(Object object);
/**
* Removes the given object from the given collection.
* Removes the given object from the given collection by {@literal id} and (if applicable) its
* {@link org.springframework.data.annotation.Version}.
*
* @param object must not be {@literal null}.
* @param collectionName name of the collection where the objects will removed, must not be {@literal null} or empty.
* @return the {@link DeleteResult} which lets you access the results of the previous delete.
* @throws org.springframework.dao.OptimisticLockingFailureException if the given {@literal object} is
* {@link org.springframework.data.annotation.Version versioned} and the current version does not match the
* one within the store. If no document with matching {@literal _id} exists in the collection the operation
* completes without error.
*/
DeleteResult remove(Object object, String collectionName);

View File

@@ -68,7 +68,16 @@ import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.aggregation.Fields;
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.*;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.JsonSchemaMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper;
import org.springframework.data.mongodb.core.convert.MongoWriter;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.convert.UpdateMapper;
import org.springframework.data.mongodb.core.index.IndexOperations;
import org.springframework.data.mongodb.core.index.IndexOperationsProvider;
import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher;
@@ -1638,9 +1647,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Assert.notNull(object, "Object must not be null!");
Query query = operations.forEntity(object).getByIdQuery();
return remove(query, object.getClass());
return remove(object, operations.determineCollectionName(object.getClass()));
}
@Override
@@ -1649,7 +1656,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Assert.notNull(object, "Object must not be null!");
Assert.hasText(collectionName, "Collection name must not be null or empty!");
Query query = operations.forEntity(object).getByIdQuery();
Query query = operations.forEntity(object).getRemoveByQuery();
return doRemove(collectionName, query, object.getClass(), false);
}
@@ -1723,10 +1730,36 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
DeleteResult result = multi ? collectionToUse.deleteMany(removeQuery, options)
: collectionToUse.deleteOne(removeQuery, options);
checkForOptimisticLockingFailures(result, removeQuery, collectionToUse);
maybeEmitEvent(new AfterDeleteEvent<>(queryObject, entityClass, collectionName));
return result;
}
private void checkForOptimisticLockingFailures(DeleteResult result, Document removeQuery,
MongoCollection<Document> collectionToUse) {
if (multi || !ResultOperations.isUndecidedDeleteResult(result, removeQuery, entity)) {
return;
}
String versionFieldName = entity.getVersionProperty().getFieldName();
Document idQuery = new Document(removeQuery);
idQuery.remove(versionFieldName);
Iterator<Document> it = collectionToUse.find(idQuery).projection(Projections.include("_id", versionFieldName))
.limit(1).iterator();
if (it.hasNext()) {
Document source = it.next();
throw ResultOperations.newDeleteVersionedOptimisticLockingException(source.get("_id"), collectionName,
removeQuery.get(versionFieldName), source.get(versionFieldName));
}
}
});
}

View File

@@ -1688,7 +1688,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return false;
}
return document.containsKey(persistentEntity.getRequiredIdProperty().getFieldName());
return document.containsKey(persistentEntity.getRequiredVersionProperty().getFieldName());
}
/*
@@ -1717,7 +1717,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(object, "Object must not be null!");
return remove(operations.forEntity(object).getByIdQuery(), object.getClass());
return remove(operations.forEntity(object).getRemoveByQuery(), object.getClass());
}
/*
@@ -1729,7 +1729,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(object, "Object must not be null!");
Assert.hasText(collectionName, "Collection name must not be null or empty!");
return doRemove(collectionName, operations.forEntity(object).getByIdQuery(), object.getClass());
return doRemove(collectionName, operations.forEntity(object).getRemoveByQuery(), object.getClass());
}
private void assertUpdateableIdIfNotSet(Object value) {
@@ -1787,15 +1787,14 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Document queryObject = query.getQueryObject();
MongoPersistentEntity<?> entity = getPersistentEntity(entityClass);
Document removeQuery = queryMapper.getMappedObject(queryObject, entity);
return execute(collectionName, collection -> {
Document removeQuey = queryMapper.getMappedObject(queryObject, entity);
maybeEmitEvent(new BeforeDeleteEvent<>(removeQuey, entityClass, collectionName));
maybeEmitEvent(new BeforeDeleteEvent<>(removeQuery, entityClass, collectionName));
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.REMOVE, collectionName, entityClass,
null, removeQuey);
null, removeQuery);
DeleteOptions deleteOptions = new DeleteOptions();
query.getCollation().map(Collation::toMongoCollation).ifPresent(deleteOptions::collation);
@@ -1805,13 +1804,13 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Remove using query: {} in collection: {}.",
new Object[] { serializeToJsonSafely(removeQuey), collectionName });
new Object[] { serializeToJsonSafely(removeQuery), collectionName });
}
if (query.getLimit() > 0 || query.getSkip() > 0) {
FindPublisher<Document> cursor = new QueryFindPublisherPreparer(query, entityClass)
.prepare(collection.find(removeQuey)) //
.prepare(collection.find(removeQuery)) //
.projection(MappedDocument.getIdOnlyProjection());
return Flux.from(cursor) //
@@ -1822,10 +1821,39 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return collectionToUse.deleteMany(MappedDocument.getIdIn(val), deleteOptions);
});
} else {
return collectionToUse.deleteMany(removeQuey, deleteOptions);
return collectionToUse.deleteMany(removeQuery, deleteOptions);
}
}).doOnNext(deleteResult -> maybeEmitEvent(new AfterDeleteEvent<>(queryObject, entityClass, collectionName)))
}).flatMap(deleteResult -> {
if (!ResultOperations.isUndecidedDeleteResult(deleteResult, removeQuery, entity)) {
return Mono.just(deleteResult);
}
return execute(collectionName, (coll -> {
String versionFieldName = entity.getVersionProperty().getFieldName();
Document queryWithoutVersion = new Document(removeQuery);
queryWithoutVersion.remove(versionFieldName);
Publisher<Document> publisher = coll.find(queryWithoutVersion)
.projection(Projections.include("_id", entity.getVersionProperty().getFieldName())).limit(1).first();
return Mono.from(publisher) //
.defaultIfEmpty(new Document()) //
.flatMap(it -> {
if (it.isEmpty()) {
return Mono.just(deleteResult);
}
return Mono.error(ResultOperations.newDeleteVersionedOptimisticLockingException(it.get("_id"),
collectionName, removeQuery.get(versionFieldName), it.get(versionFieldName)));
});
})).next();
}).doOnNext(it -> maybeEmitEvent(new AfterDeleteEvent<>(queryObject, entityClass, collectionName))) //
.next();
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import org.bson.Document;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.lang.Nullable;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
/**
* {@link ResultOperations} is intended to share logic depending on {@link DeleteResult} and
* {@link com.mongodb.client.result.UpdateResult} between reactive and imperative implementations.
*
* @author Christoph Strobl
* @since 2.2
*/
class ResultOperations {
/**
* Decide if a given {@link DeleteResult} needs further inspection. Returns {@literal true} if the delete of a
* {@link org.springframework.data.annotation.Version versioned} entity did not remove any documents although the
* operation has been {@link DeleteResult#wasAcknowledged() acknowledged}.
*
* @param deleteResult must not be {@literal null}.
* @param query the actual query used for the delete operation.
* @param entity can be {@literal null}.
* @return {@literal true} if it cannot be decided if nothing got deleted because of a potential
* {@link org.springframework.data.annotation.Version version} mismatch, or if the document did not exist in
* first place.
*/
static boolean isUndecidedDeleteResult(DeleteResult deleteResult, org.bson.Document query,
@Nullable MongoPersistentEntity<?> entity) {
if (!deleteResult.wasAcknowledged() || deleteResult.getDeletedCount() > 0) {
return false;
}
if (entity == null) {
return false;
}
if (!entity.hasVersionProperty()) {
return false;
}
return containsVersionProperty(query, entity);
}
static OptimisticLockingFailureException newDeleteVersionedOptimisticLockingException(Object id,
String collectionName, Object expectedVersion, Object actualVersion) {
throw new OptimisticLockingFailureException(
String.format("The entity with id %s in %s has changed and cannot be deleted! " + System.lineSeparator() + //
"Expected version %s but was %s.", id, collectionName, expectedVersion, actualVersion));
}
private static boolean containsVersionProperty(Document document, MongoPersistentEntity<?> entity) {
return document.containsKey(entity.getRequiredVersionProperty().getFieldName());
}
}

View File

@@ -161,7 +161,7 @@ public class SimpleMongoRepository<T, ID> implements MongoRepository<T, ID> {
Assert.notNull(entity, "The given entity must not be null!");
deleteById(entityInformation.getRequiredId(entity));
mongoOperations.remove(entity, entityInformation.getCollectionName());
}
/*

View File

@@ -355,7 +355,7 @@ public class SimpleReactiveMongoRepository<T, ID extends Serializable> implement
Assert.notNull(entity, "The given entity must not be null!");
return deleteById(entityInformation.getRequiredId(entity));
return mongoOperations.remove(entity, entityInformation.getCollectionName()).then();
}
/*

View File

@@ -43,6 +43,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.assertj.core.api.Assertions;
import org.bson.types.ObjectId;
import org.hamcrest.collection.IsMapContaining;
import org.joda.time.DateTime;
@@ -1646,6 +1647,62 @@ public class MongoTemplateTests {
assertThat(person.version, is(0));
}
@Test // DATAMONGO-2195
public void removeEntityWithMatchingVersion() {
PersonWithVersionPropertyOfTypeInteger person = new PersonWithVersionPropertyOfTypeInteger();
person.firstName = "Dave";
template.insert(person);
assertThat(person.version, is(0));
template.remove(person);
assertThat(template.count(new Query(), Person.class)).isZero();
}
@Test // DATAMONGO-2195
public void removeEntityWithoutMatchingVersionThrowsOptimisticLockingFailureException() {
PersonWithVersionPropertyOfTypeInteger person = new PersonWithVersionPropertyOfTypeInteger();
person.firstName = "Dave";
template.insert(person);
assertThat(person.version, is(0));
template.update(PersonWithVersionPropertyOfTypeInteger.class).matching(query(where("id").is(person.id)))
.apply(new Update().set("firstName", "Walter")).first();
Assertions.assertThatThrownBy(() -> template.remove(person)).isInstanceOf(OptimisticLockingFailureException.class)
.hasMessageContaining("Expected version 0 but was 1");
assertThat(template.count(new Query(), PersonWithVersionPropertyOfTypeInteger.class)).isOne();
}
@Test // DATAMONGO-2195
public void removeNonExistingVersionedEntityJustPasses() {
PersonWithVersionPropertyOfTypeInteger person = new PersonWithVersionPropertyOfTypeInteger();
person.id = "id-1";
person.firstName = "Dave";
person.version = 10;
assertThat(template.remove(person).getDeletedCount()).isZero();
}
@Test // DATAMONGO-2195
@DirtiesContext
public void removeEntityWithoutMatchingVersionWhenUsingUnaknowledgedWriteDoesNotThrowsOptimisticLockingFailureException() {
PersonWithVersionPropertyOfTypeInteger person = new PersonWithVersionPropertyOfTypeInteger();
person.firstName = "Dave";
template.insert(person);
assertThat(person.version, is(0));
template.update(PersonWithVersionPropertyOfTypeInteger.class).matching(query(where("id").is(person.id)))
.apply(new Update().set("firstName", "Walter")).first();
template.setWriteConcern(WriteConcern.UNACKNOWLEDGED);
assertThat(template.remove(person).wasAcknowledged()).isFalse();
}
@Test // DATAMONGO-588
public void initializesVersionOnBatchInsert() {

View File

@@ -704,6 +704,78 @@ public class ReactiveMongoTemplateTests {
StepVerifier.create(template.count(new Query(), Sample.class)).expectNext(2L).verifyComplete();
}
@Test // DATAMONGO-2195
public void removeEntityWithMatchingVersion() {
PersonWithVersionPropertyOfTypeInteger person = new PersonWithVersionPropertyOfTypeInteger();
person.firstName = "Dave";
template.insert(person).as(StepVerifier::create).expectNextCount(1).verifyComplete();
assertThat(person.version).isZero();
template.remove(person).as(StepVerifier::create).expectNextCount(1).verifyComplete();
template.count(new Query(), PersonWithVersionPropertyOfTypeInteger.class) //
.as(StepVerifier::create) //
.expectNext(0L) //
.verifyComplete();
}
@Test // DATAMONGO-2195
public void removeEntityWithoutMatchingVersionThrowsOptimisticLockingFailureException() {
PersonWithVersionPropertyOfTypeInteger person = new PersonWithVersionPropertyOfTypeInteger();
person.firstName = "Dave";
template.insert(person).as(StepVerifier::create).expectNextCount(1).verifyComplete();
assertThat(person.version).isZero();
template.update(PersonWithVersionPropertyOfTypeInteger.class).matching(query(where("id").is(person.id)))
.apply(new Update().set("firstName", "Walter")).first() //
.as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
template.remove(person).as(StepVerifier::create).expectError(OptimisticLockingFailureException.class).verify();
template.count(new Query(), PersonWithVersionPropertyOfTypeInteger.class) //
.as(StepVerifier::create) //
.expectNext(1L) //
.verifyComplete();
}
@Test // DATAMONGO-2195
public void removeNonExistingVersionedEntityJustPasses() {
PersonWithVersionPropertyOfTypeInteger person = new PersonWithVersionPropertyOfTypeInteger();
person.id = "id-1";
person.firstName = "Dave";
person.version = 10;
template.remove(person).as(StepVerifier::create).expectNextCount(1).verifyComplete();
}
@Test // DATAMONGO-2195
@DirtiesContext
public void removeEntityWithoutMatchingVersionWhenUsingUnaknowledgedWriteDoesNotThrowsOptimisticLockingFailureException() {
PersonWithVersionPropertyOfTypeInteger person = new PersonWithVersionPropertyOfTypeInteger();
person.firstName = "Dave";
template.insert(person).as(StepVerifier::create).expectNextCount(1).verifyComplete();
assertThat(person.version).isZero();
template.update(PersonWithVersionPropertyOfTypeInteger.class).matching(query(where("id").is(person.id)))
.apply(new Update().set("firstName", "Walter")).first() //
.as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
template.setWriteConcern(WriteConcern.UNACKNOWLEDGED);
template.remove(person).as(StepVerifier::create).expectNextCount(1).verifyComplete();
template.count(new Query(), PersonWithVersionPropertyOfTypeInteger.class) //
.as(StepVerifier::create) //
.expectNext(1L) //
.verifyComplete();
}
@Test // DATAMONGO-1444
public void optimisticLockingHandling() {

View File

@@ -31,6 +31,7 @@ import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.reactivestreams.Publisher;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.test.util.MongoTestUtils;
@@ -83,6 +84,12 @@ public class ReactiveMongoTemplateTransactionTests {
.expectNext(Success.SUCCESS) //
.verifyComplete();
StepVerifier
.create(
MongoTestUtils.createOrReplaceCollection(DATABASE_NAME, "personWithVersionPropertyOfTypeInteger", client))
.expectNext(Success.SUCCESS) //
.verifyComplete();
StepVerifier.create(template.insert(DOCUMENT, COLLECTION_NAME)).expectNextCount(1).verifyComplete();
template.insertAll(Arrays.asList(AHMANN, ARLEN, LEESHA, RENNA)) //
@@ -270,4 +277,47 @@ public class ReactiveMongoTemplateTransactionTests {
.expectNext(2L) //
.verifyComplete();
}
@Test // DATAMONGO-2195
public void deleteWithMatchingVersion() {
PersonWithVersionPropertyOfTypeInteger rojer = new PersonWithVersionPropertyOfTypeInteger();
rojer.firstName = "rojer";
PersonWithVersionPropertyOfTypeInteger saved = template.insert(rojer).block();
template.inTransaction().execute(action -> action.remove(saved)) //
.as(StepVerifier::create) //
.consumeNextWith(result -> assertThat(result.getDeletedCount()).isOne()) //
.verifyComplete();
}
@Test // DATAMONGO-2195
public void deleteWithVersionMismatch() {
PersonWithVersionPropertyOfTypeInteger rojer = new PersonWithVersionPropertyOfTypeInteger();
rojer.firstName = "rojer";
PersonWithVersionPropertyOfTypeInteger saved = template.insert(rojer).block();
saved.version = 5;
template.inTransaction().execute(action -> action.remove(saved)) //
.as(StepVerifier::create) //
.expectError(OptimisticLockingFailureException.class) //
.verify();
}
@Test // DATAMONGO-2195
public void deleteNonExistingWithVersion() {
PersonWithVersionPropertyOfTypeInteger rojer = new PersonWithVersionPropertyOfTypeInteger();
rojer.id = "deceased";
rojer.firstName = "rojer";
rojer.version = 5;
template.inTransaction().execute(action -> action.remove(rojer)) //
.as(StepVerifier::create) //
.consumeNextWith(result -> assertThat(result.getDeletedCount()).isZero()) //
.verifyComplete();
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.repository;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Version;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
*/
@Document
@Data
@NoArgsConstructor
public class VersionedPerson extends Contact {
private String firstname;
private @Nullable String lastname;
private @Version Long version;
public VersionedPerson(String firstname) {
this(firstname, null);
}
public VersionedPerson(String firstname, @Nullable String lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
}

View File

@@ -0,0 +1,174 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.repository.support;
import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assumptions.assumeThat;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.mongodb.MongoTransactionManager;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.repository.VersionedPerson;
import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
import org.springframework.data.mongodb.test.util.MongoVersion;
import org.springframework.data.mongodb.test.util.ReplicaSet;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.support.TransactionTemplate;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
/**
* @author Christoph Strobl
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class SimpleMongoRepositoryVersionedEntityTests {
@Configuration
static class Config extends AbstractMongoClientConfiguration {
@Override
public MongoClient mongoClient() {
return MongoClients.create();
}
@Override
protected String getDatabaseName() {
return "database";
}
}
private static final MongoPersistentEntity ENTITY = new BasicMongoPersistentEntity(
ClassTypeInformation.from(VersionedPerson.class));
@Autowired //
private MongoTemplate template;
private MongoEntityInformation<VersionedPerson, String> personEntityInformation;
private SimpleMongoRepository<VersionedPerson, String> repository;
private VersionedPerson sarah;
@Before
public void setUp() {
personEntityInformation = new MappingMongoEntityInformation(ENTITY);
repository = new SimpleMongoRepository<>(personEntityInformation, template);
repository.deleteAll();
sarah = repository.save(new VersionedPerson("Sarah", "Connor"));
}
@Test // DATAMONGO-2195
public void deleteWithMatchingVersion() {
repository.delete(sarah);
assertThat(template.count(query(where("id").is(sarah.getId())), VersionedPerson.class)).isZero();
}
@Test // DATAMONGO-2195
@MongoVersion(asOf = "4.0")
public void deleteWithMatchingVersionInTx() {
assumeThat(ReplicaSet.required().runsAsReplicaSet()).isTrue();
long countBefore = repository.count();
initTxTemplate().execute(status -> {
VersionedPerson t800 = repository.save(new VersionedPerson("T-800"));
repository.delete(t800);
return Void.TYPE;
});
assertThat(repository.count()).isEqualTo(countBefore);
}
@Test // DATAMONGO-2195
public void deleteWithVersionMismatch() {
sarah.setVersion(5L);
assertThatExceptionOfType(OptimisticLockingFailureException.class).isThrownBy(() -> repository.delete(sarah)) //
.withMessageContaining("Expected version 5 but was 0");
assertThat(template.count(query(where("id").is(sarah.getId())), VersionedPerson.class)).isOne();
}
@Test // DATAMONGO-2195
@MongoVersion(asOf = "4.0")
public void deleteWithVersionMismatchInTx() {
assumeThat(ReplicaSet.required().runsAsReplicaSet()).isTrue();
long countBefore = repository.count();
assertThatExceptionOfType(OptimisticLockingFailureException.class)
.isThrownBy(() -> initTxTemplate().execute(status -> {
VersionedPerson t800 = repository.save(new VersionedPerson("T-800"));
t800.setVersion(5L);
repository.delete(t800);
return Void.TYPE;
}));
assertThat(repository.count()).isEqualTo(countBefore);
}
@Test // DATAMONGO-2195
public void deleteNonExisting() {
repository.delete(new VersionedPerson("T-800"));
}
@Test // DATAMONGO-2195
@MongoVersion(asOf = "4.0")
public void deleteNonExistingInTx() {
assumeThat(ReplicaSet.required().runsAsReplicaSet()).isTrue();
initTxTemplate().execute(status -> {
repository.delete(new VersionedPerson("T-800"));
return Void.TYPE;
});
}
TransactionTemplate initTxTemplate() {
MongoTransactionManager txmgr = new MongoTransactionManager(template.getMongoDbFactory());
TransactionTemplate tt = new TransactionTemplate(txmgr);
tt.afterPropertiesSet();
return tt;
}
}

View File

@@ -1080,6 +1080,11 @@ template.save(tmp); // throws OptimisticLockingFailureException
IMPORTANT: Optimistic Locking requires to set the `WriteConcern` to `ACKNOWLEDGED`. Otherwise `OptimisticLockingFailureException` can be silently swallowed.
NOTE: As of Version 2.2 `MongoOperations` also includes the `@Version` property when removing an entity from the database, raising an
`OptimisticLockingFailureException` if a document with matching `_id` but a `version` mismatch shall be deleted. In cases
where no Document with matching `_id` can be found the operation passes without error.
To remove a `Document` without version check use `MongoOperations#remove(Query,...)` instead of `MongoOperations#remove(Object)`.
[[mongo.query]]
== Querying Documents