Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2ab466e

Browse files
christophstroblmp911de
authored andcommittedApr 3, 2017
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: spring-projects#371.
1 parent e9da449 commit 2ab466e

File tree

4 files changed

+154
-10
lines changed

4 files changed

+154
-10
lines changed
 

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,10 @@ public UpdateResult doInCollection(MongoCollection<Document> collection)
11921192
Document updateObj = update == null ? new Document()
11931193
: updateMapper.getMappedObject(update.getUpdateObject(), entity);
11941194

1195+
if (multi && update.isIsolated() && !queryObj.containsKey("$isolated")) {
1196+
queryObj.put("$isolated", 1);
1197+
}
1198+
11951199
if (LOGGER.isDebugEnabled()) {
11961200
LOGGER.debug("Calling update using query: {} and update: {} in collection: {}",
11971201
serializeToJsonSafely(queryObj), serializeToJsonSafely(updateObj), collectionName);

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public enum Position {
5353
LAST, FIRST
5454
}
5555

56+
private boolean isolated = false;
5657
private Set<String> keysToUpdate = new HashSet<String>();
5758
private Map<String, Object> modifierOps = new LinkedHashMap<String, Object>();
5859
private Map<String, PushOperatorBuilder> pushCommandBuilders = new LinkedHashMap<String, PushOperatorBuilder>(1);
@@ -73,7 +74,7 @@ public static Update update(String key, Object value) {
7374
* {@literal $set}. This means fields not given in the {@link Document} will be nulled when executing the update. To
7475
* create an only-updating {@link Update} instance of a {@link Document}, call {@link #set(String, Object)} for each
7576
* value in it.
76-
*
77+
*
7778
* @param object the source {@link Document} to create the update from.
7879
* @param exclude the fields to exclude.
7980
* @return
@@ -364,6 +365,28 @@ public BitwiseOperatorBuilder bitwise(String key) {
364365
return new BitwiseOperatorBuilder(this, key);
365366
}
366367

368+
/**
369+
* Prevents a write operation that affects <strong>multiple</strong> documents from yielding to other reads or writes
370+
* once the first document is written. <br />
371+
* Use with {@link org.springframework.data.mongodb.core.MongoOperations#updateMulti(Query, Update, Class)}.
372+
*
373+
* @return never {@literal null}.
374+
* @since 2.0
375+
*/
376+
public Update isolated() {
377+
378+
isolated = true;
379+
return this;
380+
}
381+
382+
/**
383+
* @return {@literal true} if update isolated is set.
384+
* @since 2.0
385+
*/
386+
public Boolean isIsolated() {
387+
return isolated;
388+
}
389+
367390
public Document getUpdateObject() {
368391
return new Document(modifierOps);
369392
}

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

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static org.hamcrest.Matchers.*;
1919
import static org.junit.Assert.*;
2020
import static org.mockito.Mockito.*;
21+
import static org.springframework.data.mongodb.test.util.IsBsonObject.*;
2122

2223
import java.math.BigInteger;
2324
import java.util.Collections;
@@ -26,6 +27,7 @@
2627
import java.util.regex.Pattern;
2728

2829
import org.bson.Document;
30+
import org.bson.conversions.Bson;
2931
import org.bson.types.ObjectId;
3032
import org.hamcrest.collection.IsIterableContainingInOrder;
3133
import org.hamcrest.core.Is;
@@ -337,7 +339,7 @@ public void processDocument(Document document) throws MongoException, DataAccess
337339
public void aggregateShouldHonorReadPreferenceWhenSet() {
338340

339341
when(db.runCommand(Mockito.any(org.bson.Document.class), Mockito.any(ReadPreference.class), eq(Document.class)))
340-
.thenReturn(mock(Document.class));
342+
.thenReturn(mock(Document.class));
341343
template.setReadPreference(ReadPreference.secondary());
342344

343345
template.aggregate(Aggregation.newAggregation(Aggregation.unwind("foo")), "collection-1", Wrapper.class);
@@ -361,7 +363,7 @@ public void aggregateShouldIgnoreReadPreferenceWhenNotSet() {
361363
public void geoNearShouldHonorReadPreferenceWhenSet() {
362364

363365
when(db.runCommand(Mockito.any(org.bson.Document.class), Mockito.any(ReadPreference.class), eq(Document.class)))
364-
.thenReturn(mock(Document.class));
366+
.thenReturn(mock(Document.class));
365367
template.setReadPreference(ReadPreference.secondary());
366368

367369
NearQuery query = NearQuery.near(new Point(1, 1));
@@ -374,7 +376,8 @@ public void geoNearShouldHonorReadPreferenceWhenSet() {
374376
@Test // DATAMONGO-1166
375377
public void geoNearShouldIgnoreReadPreferenceWhenNotSet() {
376378

377-
when(db.runCommand(Mockito.any(Document.class), eq(Document.class))).thenReturn(mock(Document.class));
379+
when(db.runCommand(Mockito.any(Document.class), eq(Document.class))).thenReturn(
380+
mock(Document.class));
378381

379382
NearQuery query = NearQuery.near(new Point(1, 1));
380383
template.geoNear(query, Wrapper.class);
@@ -514,6 +517,102 @@ public void onBeforeConvert(BeforeConvertEvent<VersionedEntity> event) {
514517
spy.save(entity);
515518
}
516519

520+
@Test // DATAMONGO-1447
521+
public void shouldNotAppend$isolatedToNonMulitUpdate() {
522+
523+
template.updateFirst(new Query(), new Update().isolated().set("jon", "snow"), Wrapper.class);
524+
525+
ArgumentCaptor<Bson> queryCaptor = ArgumentCaptor.forClass(Bson.class);
526+
ArgumentCaptor<Bson> updateCaptor = ArgumentCaptor.forClass(Bson.class);
527+
528+
verify(collection).updateOne(queryCaptor.capture(), updateCaptor.capture(), any());
529+
530+
assertThat(queryCaptor.getValue(), isBsonObject().notContaining("$isolated"));
531+
assertThat(updateCaptor.getValue(), isBsonObject().containing("$set.jon", "snow").notContaining("$isolated"));
532+
}
533+
534+
@Test // DATAMONGO-1447
535+
public void shouldAppend$isolatedToUpdateMultiEmptyQuery() {
536+
537+
template.updateMulti(new Query(), new Update().isolated().set("jon", "snow"), Wrapper.class);
538+
539+
ArgumentCaptor<Bson> queryCaptor = ArgumentCaptor.forClass(Bson.class);
540+
ArgumentCaptor<Bson> updateCaptor = ArgumentCaptor.forClass(Bson.class);
541+
542+
verify(collection).updateMany(queryCaptor.capture(), updateCaptor.capture(), any());
543+
544+
assertThat(queryCaptor.getValue(), isBsonObject().withSize(1).containing("$isolated", 1));
545+
assertThat(updateCaptor.getValue(), isBsonObject().containing("$set.jon", "snow").notContaining("$isolated"));
546+
}
547+
548+
@Test // DATAMONGO-1447
549+
public void shouldAppend$isolatedToUpdateMultiQueryIfNotPresentAndUpdateSetsValue() {
550+
551+
Update update = new Update().isolated().set("jon", "snow");
552+
Query query = new BasicQuery("{'eddard':'stark'}");
553+
554+
template.updateMulti(query, update, Wrapper.class);
555+
556+
ArgumentCaptor<Bson> queryCaptor = ArgumentCaptor.forClass(Bson.class);
557+
ArgumentCaptor<Bson> updateCaptor = ArgumentCaptor.forClass(Bson.class);
558+
559+
verify(collection).updateMany(queryCaptor.capture(), updateCaptor.capture(), any());
560+
561+
assertThat(queryCaptor.getValue(), isBsonObject().containing("$isolated", 1).containing("eddard", "stark"));
562+
assertThat(updateCaptor.getValue(), isBsonObject().containing("$set.jon", "snow").notContaining("$isolated"));
563+
}
564+
565+
@Test // DATAMONGO-1447
566+
public void shouldNotAppend$isolatedToUpdateMultiQueryIfNotPresentAndUpdateDoesNotSetValue() {
567+
568+
Update update = new Update().set("jon", "snow");
569+
Query query = new BasicQuery("{'eddard':'stark'}");
570+
571+
template.updateMulti(query, update, Wrapper.class);
572+
573+
ArgumentCaptor<Bson> queryCaptor = ArgumentCaptor.forClass(Bson.class);
574+
ArgumentCaptor<Bson> updateCaptor = ArgumentCaptor.forClass(Bson.class);
575+
576+
verify(collection).updateMany(queryCaptor.capture(), updateCaptor.capture(), any());
577+
578+
assertThat(queryCaptor.getValue(), isBsonObject().notContaining("$isolated").containing("eddard", "stark"));
579+
assertThat(updateCaptor.getValue(), isBsonObject().containing("$set.jon", "snow").notContaining("$isolated"));
580+
}
581+
582+
@Test // DATAMONGO-1447
583+
public void shouldNotOverwrite$isolatedToUpdateMultiQueryIfPresentAndUpdateDoesNotSetValue() {
584+
585+
Update update = new Update().set("jon", "snow");
586+
Query query = new BasicQuery("{'eddard':'stark', '$isolated' : 1}");
587+
588+
template.updateMulti(query, update, Wrapper.class);
589+
590+
ArgumentCaptor<Bson> queryCaptor = ArgumentCaptor.forClass(Bson.class);
591+
ArgumentCaptor<Bson> updateCaptor = ArgumentCaptor.forClass(Bson.class);
592+
593+
verify(collection).updateMany(queryCaptor.capture(), updateCaptor.capture(), any());
594+
595+
assertThat(queryCaptor.getValue(), isBsonObject().containing("$isolated", 1).containing("eddard", "stark"));
596+
assertThat(updateCaptor.getValue(), isBsonObject().containing("$set.jon", "snow").notContaining("$isolated"));
597+
}
598+
599+
@Test // DATAMONGO-1447
600+
public void shouldNotOverwrite$isolatedToUpdateMultiQueryIfPresentAndUpdateSetsValue() {
601+
602+
Update update = new Update().isolated().set("jon", "snow");
603+
Query query = new BasicQuery("{'eddard':'stark', '$isolated' : 0}");
604+
605+
template.updateMulti(query, update, Wrapper.class);
606+
607+
ArgumentCaptor<Bson> queryCaptor = ArgumentCaptor.forClass(Bson.class);
608+
ArgumentCaptor<Bson> updateCaptor = ArgumentCaptor.forClass(Bson.class);
609+
610+
verify(collection).updateMany(queryCaptor.capture(), updateCaptor.capture(), any());
611+
612+
assertThat(queryCaptor.getValue(), isBsonObject().containing("$isolated", 0).containing("eddard", "stark"));
613+
assertThat(updateCaptor.getValue(), isBsonObject().containing("$set.jon", "snow").notContaining("$isolated"));
614+
}
615+
517616
class AutogenerateableId {
518617

519618
@Id BigInteger id;

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/IsBsonObject.java

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2015 the original author or authors.
2+
* Copyright 2015-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -35,7 +35,8 @@
3535
*/
3636
public class IsBsonObject<T extends Bson> extends TypeSafeMatcher<T> {
3737

38-
private List<ExpectedBsonContent> expectations = new ArrayList<ExpectedBsonContent>();;
38+
private List<ExpectedBsonContent> expectations = new ArrayList<>();
39+
private Integer expectedSize;
3940

4041
public static <T extends Bson> IsBsonObject<T> isBsonObject() {
4142
return new IsBsonObject<T>();
@@ -49,22 +50,33 @@ protected void describeMismatchSafely(T item, Description mismatchDescription) {
4950
@Override
5051
public void describeTo(Description description) {
5152

53+
if (expectedSize != null) {
54+
description.appendText(String.format("Expected to contain %s fields. ", expectedSize));
55+
}
56+
5257
for (ExpectedBsonContent expectation : expectations) {
5358

5459
if (expectation.not) {
55-
description.appendText(String.format("Path %s should not be present.", expectation.path));
60+
description.appendText(String.format("Path %s should not be present. ", expectation.path));
5661
} else if (expectation.value == null) {
57-
description.appendText(String.format("Expected to find path %s.", expectation.path));
62+
description.appendText(String.format("Expected to find path %s. ", expectation.path));
5863
} else {
59-
description.appendText(String.format("Expected to find %s for path %s.", expectation.value, expectation.path));
64+
description.appendText(String.format("Expected to find %s for path %s. ", expectation.value, expectation.path));
6065
}
6166
}
62-
6367
}
6468

6569
@Override
6670
protected boolean matchesSafely(T item) {
6771

72+
if (expectedSize != null && item instanceof Document) {
73+
74+
Document document = (Document) item;
75+
if (expectedSize != document.keySet().size()) {
76+
return false;
77+
}
78+
}
79+
6880
if (expectations.isEmpty()) {
6981
return true;
7082
}
@@ -147,6 +159,12 @@ public IsBsonObject<T> notContaining(String key) {
147159
return this;
148160
}
149161

162+
public IsBsonObject<T> withSize(int size) {
163+
164+
this.expectedSize = Integer.valueOf(size);
165+
return this;
166+
}
167+
150168
static class ExpectedBsonContent {
151169
String path;
152170
Class<?> type;

0 commit comments

Comments
 (0)
Failed to load comments.