From 2ab466eb3589c59cfd99dca8af5e33adfa6f309c Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 28 Jun 2016 09:15:08 +0200 Subject: [PATCH] DATAMONGO-1447 - Add support for isolations on Update. We now allow usage of the $isolated update operator via Update.isolated(). In case isolated is set the query involved in MongoOperations.updateMulti will be enhanced by '$isolated' : 1 in case the isolation level has not already been set explicitly via eg. new BasicQuery("{'$isolated' : 0}"). Original pull request: #371. --- .../data/mongodb/core/MongoTemplate.java | 4 + .../data/mongodb/core/query/Update.java | 25 ++++- .../mongodb/core/MongoTemplateUnitTests.java | 105 +++++++++++++++++- .../data/mongodb/test/util/IsBsonObject.java | 30 ++++- 4 files changed, 154 insertions(+), 10 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index c2c5f1b8b..9d6ccde59 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -1192,6 +1192,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Document updateObj = update == null ? new Document() : updateMapper.getMappedObject(update.getUpdateObject(), entity); + if (multi && update.isIsolated() && !queryObj.containsKey("$isolated")) { + queryObj.put("$isolated", 1); + } + if (LOGGER.isDebugEnabled()) { LOGGER.debug("Calling update using query: {} and update: {} in collection: {}", serializeToJsonSafely(queryObj), serializeToJsonSafely(updateObj), collectionName); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java index 0af4ad3ee..37663c454 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java @@ -53,6 +53,7 @@ public class Update { LAST, FIRST } + private boolean isolated = false; private Set keysToUpdate = new HashSet(); private Map modifierOps = new LinkedHashMap(); private Map pushCommandBuilders = new LinkedHashMap(1); @@ -73,7 +74,7 @@ public class Update { * {@literal $set}. This means fields not given in the {@link Document} will be nulled when executing the update. To * create an only-updating {@link Update} instance of a {@link Document}, call {@link #set(String, Object)} for each * value in it. - * + * * @param object the source {@link Document} to create the update from. * @param exclude the fields to exclude. * @return @@ -364,6 +365,28 @@ public class Update { return new BitwiseOperatorBuilder(this, key); } + /** + * Prevents a write operation that affects multiple documents from yielding to other reads or writes + * once the first document is written.
+ * Use with {@link org.springframework.data.mongodb.core.MongoOperations#updateMulti(Query, Update, Class)}. + * + * @return never {@literal null}. + * @since 2.0 + */ + public Update isolated() { + + isolated = true; + return this; + } + + /** + * @return {@literal true} if update isolated is set. + * @since 2.0 + */ + public Boolean isIsolated() { + return isolated; + } + public Document getUpdateObject() { return new Document(modifierOps); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index 1d5c6da51..cd2bd841b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -18,6 +18,7 @@ package org.springframework.data.mongodb.core; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.mockito.Mockito.*; +import static org.springframework.data.mongodb.test.util.IsBsonObject.*; import java.math.BigInteger; import java.util.Collections; @@ -26,6 +27,7 @@ import java.util.Optional; import java.util.regex.Pattern; import org.bson.Document; +import org.bson.conversions.Bson; import org.bson.types.ObjectId; import org.hamcrest.collection.IsIterableContainingInOrder; import org.hamcrest.core.Is; @@ -337,7 +339,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { public void aggregateShouldHonorReadPreferenceWhenSet() { when(db.runCommand(Mockito.any(org.bson.Document.class), Mockito.any(ReadPreference.class), eq(Document.class))) - .thenReturn(mock(Document.class)); + .thenReturn(mock(Document.class)); template.setReadPreference(ReadPreference.secondary()); template.aggregate(Aggregation.newAggregation(Aggregation.unwind("foo")), "collection-1", Wrapper.class); @@ -361,7 +363,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { public void geoNearShouldHonorReadPreferenceWhenSet() { when(db.runCommand(Mockito.any(org.bson.Document.class), Mockito.any(ReadPreference.class), eq(Document.class))) - .thenReturn(mock(Document.class)); + .thenReturn(mock(Document.class)); template.setReadPreference(ReadPreference.secondary()); NearQuery query = NearQuery.near(new Point(1, 1)); @@ -374,7 +376,8 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { @Test // DATAMONGO-1166 public void geoNearShouldIgnoreReadPreferenceWhenNotSet() { - when(db.runCommand(Mockito.any(Document.class), eq(Document.class))).thenReturn(mock(Document.class)); + when(db.runCommand(Mockito.any(Document.class), eq(Document.class))).thenReturn( + mock(Document.class)); NearQuery query = NearQuery.near(new Point(1, 1)); template.geoNear(query, Wrapper.class); @@ -514,6 +517,102 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { spy.save(entity); } + @Test // DATAMONGO-1447 + public void shouldNotAppend$isolatedToNonMulitUpdate() { + + template.updateFirst(new Query(), new Update().isolated().set("jon", "snow"), Wrapper.class); + + ArgumentCaptor queryCaptor = ArgumentCaptor.forClass(Bson.class); + ArgumentCaptor updateCaptor = ArgumentCaptor.forClass(Bson.class); + + verify(collection).updateOne(queryCaptor.capture(), updateCaptor.capture(), any()); + + assertThat(queryCaptor.getValue(), isBsonObject().notContaining("$isolated")); + assertThat(updateCaptor.getValue(), isBsonObject().containing("$set.jon", "snow").notContaining("$isolated")); + } + + @Test // DATAMONGO-1447 + public void shouldAppend$isolatedToUpdateMultiEmptyQuery() { + + template.updateMulti(new Query(), new Update().isolated().set("jon", "snow"), Wrapper.class); + + ArgumentCaptor queryCaptor = ArgumentCaptor.forClass(Bson.class); + ArgumentCaptor updateCaptor = ArgumentCaptor.forClass(Bson.class); + + verify(collection).updateMany(queryCaptor.capture(), updateCaptor.capture(), any()); + + assertThat(queryCaptor.getValue(), isBsonObject().withSize(1).containing("$isolated", 1)); + assertThat(updateCaptor.getValue(), isBsonObject().containing("$set.jon", "snow").notContaining("$isolated")); + } + + @Test // DATAMONGO-1447 + public void shouldAppend$isolatedToUpdateMultiQueryIfNotPresentAndUpdateSetsValue() { + + Update update = new Update().isolated().set("jon", "snow"); + Query query = new BasicQuery("{'eddard':'stark'}"); + + template.updateMulti(query, update, Wrapper.class); + + ArgumentCaptor queryCaptor = ArgumentCaptor.forClass(Bson.class); + ArgumentCaptor updateCaptor = ArgumentCaptor.forClass(Bson.class); + + verify(collection).updateMany(queryCaptor.capture(), updateCaptor.capture(), any()); + + assertThat(queryCaptor.getValue(), isBsonObject().containing("$isolated", 1).containing("eddard", "stark")); + assertThat(updateCaptor.getValue(), isBsonObject().containing("$set.jon", "snow").notContaining("$isolated")); + } + + @Test // DATAMONGO-1447 + public void shouldNotAppend$isolatedToUpdateMultiQueryIfNotPresentAndUpdateDoesNotSetValue() { + + Update update = new Update().set("jon", "snow"); + Query query = new BasicQuery("{'eddard':'stark'}"); + + template.updateMulti(query, update, Wrapper.class); + + ArgumentCaptor queryCaptor = ArgumentCaptor.forClass(Bson.class); + ArgumentCaptor updateCaptor = ArgumentCaptor.forClass(Bson.class); + + verify(collection).updateMany(queryCaptor.capture(), updateCaptor.capture(), any()); + + assertThat(queryCaptor.getValue(), isBsonObject().notContaining("$isolated").containing("eddard", "stark")); + assertThat(updateCaptor.getValue(), isBsonObject().containing("$set.jon", "snow").notContaining("$isolated")); + } + + @Test // DATAMONGO-1447 + public void shouldNotOverwrite$isolatedToUpdateMultiQueryIfPresentAndUpdateDoesNotSetValue() { + + Update update = new Update().set("jon", "snow"); + Query query = new BasicQuery("{'eddard':'stark', '$isolated' : 1}"); + + template.updateMulti(query, update, Wrapper.class); + + ArgumentCaptor queryCaptor = ArgumentCaptor.forClass(Bson.class); + ArgumentCaptor updateCaptor = ArgumentCaptor.forClass(Bson.class); + + verify(collection).updateMany(queryCaptor.capture(), updateCaptor.capture(), any()); + + assertThat(queryCaptor.getValue(), isBsonObject().containing("$isolated", 1).containing("eddard", "stark")); + assertThat(updateCaptor.getValue(), isBsonObject().containing("$set.jon", "snow").notContaining("$isolated")); + } + + @Test // DATAMONGO-1447 + public void shouldNotOverwrite$isolatedToUpdateMultiQueryIfPresentAndUpdateSetsValue() { + + Update update = new Update().isolated().set("jon", "snow"); + Query query = new BasicQuery("{'eddard':'stark', '$isolated' : 0}"); + + template.updateMulti(query, update, Wrapper.class); + + ArgumentCaptor queryCaptor = ArgumentCaptor.forClass(Bson.class); + ArgumentCaptor updateCaptor = ArgumentCaptor.forClass(Bson.class); + + verify(collection).updateMany(queryCaptor.capture(), updateCaptor.capture(), any()); + + assertThat(queryCaptor.getValue(), isBsonObject().containing("$isolated", 0).containing("eddard", "stark")); + assertThat(updateCaptor.getValue(), isBsonObject().containing("$set.jon", "snow").notContaining("$isolated")); + } + class AutogenerateableId { @Id BigInteger id; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/IsBsonObject.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/IsBsonObject.java index 491b45bc4..e59a8202b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/IsBsonObject.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/IsBsonObject.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2017 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. @@ -35,7 +35,8 @@ import org.springframework.util.ClassUtils; */ public class IsBsonObject extends TypeSafeMatcher { - private List expectations = new ArrayList();; + private List expectations = new ArrayList<>(); + private Integer expectedSize; public static IsBsonObject isBsonObject() { return new IsBsonObject(); @@ -49,22 +50,33 @@ public class IsBsonObject extends TypeSafeMatcher { @Override public void describeTo(Description description) { + if (expectedSize != null) { + description.appendText(String.format("Expected to contain %s fields. ", expectedSize)); + } + for (ExpectedBsonContent expectation : expectations) { if (expectation.not) { - description.appendText(String.format("Path %s should not be present.", expectation.path)); + description.appendText(String.format("Path %s should not be present. ", expectation.path)); } else if (expectation.value == null) { - description.appendText(String.format("Expected to find path %s.", expectation.path)); + description.appendText(String.format("Expected to find path %s. ", expectation.path)); } else { - description.appendText(String.format("Expected to find %s for path %s.", expectation.value, expectation.path)); + description.appendText(String.format("Expected to find %s for path %s. ", expectation.value, expectation.path)); } } - } @Override protected boolean matchesSafely(T item) { + if (expectedSize != null && item instanceof Document) { + + Document document = (Document) item; + if (expectedSize != document.keySet().size()) { + return false; + } + } + if (expectations.isEmpty()) { return true; } @@ -147,6 +159,12 @@ public class IsBsonObject extends TypeSafeMatcher { return this; } + public IsBsonObject withSize(int size) { + + this.expectedSize = Integer.valueOf(size); + return this; + } + static class ExpectedBsonContent { String path; Class type;