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 ef2761d

Browse files
ykardziyakachristophstrobl
authored andcommittedFeb 21, 2024
Declarative way for setting MongoDB transaction options.
Closes #1628
1 parent 8e01261 commit ef2761d

File tree

8 files changed

+807
-22
lines changed

8 files changed

+807
-22
lines changed
 

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ protected void doBegin(Object transaction, TransactionDefinition definition) thr
134134
}
135135

136136
try {
137-
mongoTransactionObject.startTransaction(options);
137+
mongoTransactionObject.startTransaction(MongoTransactionUtils.extractOptions(definition, options));
138138
} catch (MongoException ex) {
139139
throw new TransactionSystemException(String.format("Could not start Mongo transaction for session %s.",
140140
debugString(mongoTransactionObject.getSession())), ex);
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2023 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+
* https://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;
17+
18+
import java.time.Duration;
19+
import java.util.concurrent.TimeUnit;
20+
21+
import org.apache.commons.logging.Log;
22+
import org.apache.commons.logging.LogFactory;
23+
import org.springframework.lang.Nullable;
24+
import org.springframework.transaction.TransactionDefinition;
25+
import org.springframework.transaction.interceptor.TransactionAttribute;
26+
27+
import com.mongodb.ReadConcern;
28+
import com.mongodb.ReadConcernLevel;
29+
import com.mongodb.ReadPreference;
30+
import com.mongodb.TransactionOptions;
31+
import com.mongodb.WriteConcern;
32+
33+
/**
34+
* Helper class for translating @Transactional labels into Mongo-specific {@link TransactionOptions}.
35+
*
36+
* @author Yan Kardziyaka
37+
*/
38+
public final class MongoTransactionUtils {
39+
private static final Log LOGGER = LogFactory.getLog(MongoTransactionUtils.class);
40+
41+
private static final String MAX_COMMIT_TIME = "mongo:maxCommitTime";
42+
43+
private static final String READ_CONCERN_OPTION = "mongo:readConcern";
44+
45+
private static final String READ_PREFERENCE_OPTION = "mongo:readPreference";
46+
47+
private static final String WRITE_CONCERN_OPTION = "mongo:writeConcern";
48+
49+
private MongoTransactionUtils() {}
50+
51+
@Nullable
52+
public static TransactionOptions extractOptions(TransactionDefinition transactionDefinition,
53+
@Nullable TransactionOptions fallbackOptions) {
54+
if (transactionDefinition instanceof TransactionAttribute transactionAttribute) {
55+
TransactionOptions.Builder builder = null;
56+
for (String label : transactionAttribute.getLabels()) {
57+
String[] tokens = label.split("=", 2);
58+
builder = tokens.length == 2 ? enhanceWithProperty(builder, tokens[0], tokens[1]) : builder;
59+
}
60+
if (builder == null) {
61+
return fallbackOptions;
62+
}
63+
TransactionOptions options = builder.build();
64+
return fallbackOptions == null ? options : TransactionOptions.merge(options, fallbackOptions);
65+
} else {
66+
if (LOGGER.isDebugEnabled()) {
67+
LOGGER.debug("%s cannot be casted to %s. Transaction labels won't be evaluated as options".formatted(
68+
TransactionDefinition.class.getName(), TransactionAttribute.class.getName()));
69+
}
70+
return fallbackOptions;
71+
}
72+
}
73+
74+
@Nullable
75+
private static TransactionOptions.Builder enhanceWithProperty(@Nullable TransactionOptions.Builder builder,
76+
String key, String value) {
77+
return switch (key) {
78+
case MAX_COMMIT_TIME -> nullSafe(builder).maxCommitTime(Duration.parse(value).toMillis(), TimeUnit.MILLISECONDS);
79+
case READ_CONCERN_OPTION -> nullSafe(builder).readConcern(new ReadConcern(ReadConcernLevel.fromString(value)));
80+
case READ_PREFERENCE_OPTION -> nullSafe(builder).readPreference(ReadPreference.valueOf(value));
81+
case WRITE_CONCERN_OPTION -> nullSafe(builder).writeConcern(getWriteConcern(value));
82+
default -> builder;
83+
};
84+
}
85+
86+
private static TransactionOptions.Builder nullSafe(@Nullable TransactionOptions.Builder builder) {
87+
return builder == null ? TransactionOptions.builder() : builder;
88+
}
89+
90+
private static WriteConcern getWriteConcern(String writeConcernAsString) {
91+
WriteConcern writeConcern = WriteConcern.valueOf(writeConcernAsString);
92+
if (writeConcern == null) {
93+
throw new IllegalArgumentException("'%s' is not a valid WriteConcern".formatted(writeConcernAsString));
94+
}
95+
return writeConcern;
96+
}
97+
98+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ protected Mono<Void> doBegin(TransactionSynchronizationManager synchronizationMa
146146

147147
}).doOnNext(resourceHolder -> {
148148

149-
mongoTransactionObject.startTransaction(options);
149+
mongoTransactionObject.startTransaction(MongoTransactionUtils.extractOptions(definition, options));
150150

151151
if (logger.isDebugEnabled()) {
152152
logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getSession())));
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*
2+
* Copyright 2023 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+
* https://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;
17+
18+
import static java.util.UUID.*;
19+
import static org.assertj.core.api.Assertions.*;
20+
21+
import java.util.Set;
22+
import java.util.concurrent.TimeUnit;
23+
24+
import org.junit.jupiter.api.Test;
25+
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
26+
import org.springframework.transaction.interceptor.TransactionAttribute;
27+
import org.springframework.transaction.support.DefaultTransactionDefinition;
28+
29+
import com.mongodb.ReadConcern;
30+
import com.mongodb.ReadPreference;
31+
import com.mongodb.TransactionOptions;
32+
import com.mongodb.WriteConcern;
33+
34+
/**
35+
* @author Yan Kardziyaka
36+
*/
37+
class MongoTransactionUtilsUnitTests {
38+
39+
@Test // GH-1628
40+
public void shouldThrowIllegalArgumentExceptionIfLabelsContainInvalidMaxCommitTime() {
41+
TransactionOptions fallbackOptions = getTransactionOptions();
42+
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
43+
attribute.setLabels(Set.of("mongo:maxCommitTime=-PT5S"));
44+
45+
assertThatThrownBy(() -> MongoTransactionUtils.extractOptions(attribute, fallbackOptions)) //
46+
.isInstanceOf(IllegalArgumentException.class);
47+
}
48+
49+
@Test // GH-1628
50+
public void shouldThrowIllegalArgumentExceptionIfLabelsContainInvalidReadConcern() {
51+
TransactionOptions fallbackOptions = getTransactionOptions();
52+
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
53+
attribute.setLabels(Set.of("mongo:readConcern=invalidValue"));
54+
55+
assertThatThrownBy(() -> MongoTransactionUtils.extractOptions(attribute, fallbackOptions)) //
56+
.isInstanceOf(IllegalArgumentException.class);
57+
}
58+
59+
@Test // GH-1628
60+
public void shouldThrowIllegalArgumentExceptionIfLabelsContainInvalidReadPreference() {
61+
TransactionOptions fallbackOptions = getTransactionOptions();
62+
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
63+
attribute.setLabels(Set.of("mongo:readPreference=invalidValue"));
64+
65+
assertThatThrownBy(() -> MongoTransactionUtils.extractOptions(attribute, fallbackOptions)) //
66+
.isInstanceOf(IllegalArgumentException.class);
67+
}
68+
69+
@Test // GH-1628
70+
public void shouldThrowIllegalArgumentExceptionIfLabelsContainInvalidWriteConcern() {
71+
TransactionOptions fallbackOptions = getTransactionOptions();
72+
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
73+
attribute.setLabels(Set.of("mongo:writeConcern=invalidValue"));
74+
75+
assertThatThrownBy(() -> MongoTransactionUtils.extractOptions(attribute, fallbackOptions)) //
76+
.isInstanceOf(IllegalArgumentException.class);
77+
}
78+
79+
@Test // GH-1628
80+
public void shouldReturnFallbackOptionsIfNotTransactionAttribute() {
81+
TransactionOptions fallbackOptions = getTransactionOptions();
82+
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
83+
84+
TransactionOptions result = MongoTransactionUtils.extractOptions(definition, fallbackOptions);
85+
86+
assertThat(result).isSameAs(fallbackOptions);
87+
}
88+
89+
@Test // GH-1628
90+
public void shouldReturnFallbackOptionsIfNoLabelsProvided() {
91+
TransactionOptions fallbackOptions = getTransactionOptions();
92+
TransactionAttribute attribute = new DefaultTransactionAttribute();
93+
94+
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, fallbackOptions);
95+
96+
assertThat(result).isSameAs(fallbackOptions);
97+
}
98+
99+
@Test // GH-1628
100+
public void shouldReturnFallbackOptionsIfLabelsDoesNotContainValidOptions() {
101+
TransactionOptions fallbackOptions = getTransactionOptions();
102+
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
103+
Set<String> labels = Set.of("mongo:readConcern", "writeConcern", "readPreference=SECONDARY",
104+
"mongo:maxCommitTime PT5M", randomUUID().toString());
105+
attribute.setLabels(labels);
106+
107+
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, fallbackOptions);
108+
109+
assertThat(result).isSameAs(fallbackOptions);
110+
}
111+
112+
@Test // GH-1628
113+
public void shouldReturnMergedOptionsIfLabelsContainMaxCommitTime() {
114+
TransactionOptions fallbackOptions = getTransactionOptions();
115+
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
116+
attribute.setLabels(Set.of("mongo:maxCommitTime=PT5S"));
117+
118+
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, fallbackOptions);
119+
120+
assertThat(result).isNotSameAs(fallbackOptions) //
121+
.returns(5L, from(options -> options.getMaxCommitTime(TimeUnit.SECONDS))) //
122+
.returns(ReadConcern.AVAILABLE, from(TransactionOptions::getReadConcern)) //
123+
.returns(ReadPreference.secondaryPreferred(), from(TransactionOptions::getReadPreference)) //
124+
.returns(WriteConcern.UNACKNOWLEDGED, from(TransactionOptions::getWriteConcern));
125+
}
126+
127+
@Test // GH-1628
128+
public void shouldReturnMergedOptionsIfLabelsContainReadConcern() {
129+
TransactionOptions fallbackOptions = getTransactionOptions();
130+
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
131+
attribute.setLabels(Set.of("mongo:readConcern=majority"));
132+
133+
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, fallbackOptions);
134+
135+
assertThat(result).isNotSameAs(fallbackOptions) //
136+
.returns(1L, from(options -> options.getMaxCommitTime(TimeUnit.MINUTES))) //
137+
.returns(ReadConcern.MAJORITY, from(TransactionOptions::getReadConcern)) //
138+
.returns(ReadPreference.secondaryPreferred(), from(TransactionOptions::getReadPreference)) //
139+
.returns(WriteConcern.UNACKNOWLEDGED, from(TransactionOptions::getWriteConcern));
140+
}
141+
142+
@Test // GH-1628
143+
public void shouldReturnMergedOptionsIfLabelsContainReadPreference() {
144+
TransactionOptions fallbackOptions = getTransactionOptions();
145+
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
146+
attribute.setLabels(Set.of("mongo:readPreference=primaryPreferred"));
147+
148+
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, fallbackOptions);
149+
150+
assertThat(result).isNotSameAs(fallbackOptions) //
151+
.returns(1L, from(options -> options.getMaxCommitTime(TimeUnit.MINUTES))) //
152+
.returns(ReadConcern.AVAILABLE, from(TransactionOptions::getReadConcern)) //
153+
.returns(ReadPreference.primaryPreferred(), from(TransactionOptions::getReadPreference)) //
154+
.returns(WriteConcern.UNACKNOWLEDGED, from(TransactionOptions::getWriteConcern));
155+
}
156+
157+
@Test // GH-1628
158+
public void shouldReturnMergedOptionsIfLabelsContainWriteConcern() {
159+
TransactionOptions fallbackOptions = getTransactionOptions();
160+
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
161+
attribute.setLabels(Set.of("mongo:writeConcern=w3"));
162+
163+
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, fallbackOptions);
164+
165+
assertThat(result).isNotSameAs(fallbackOptions) //
166+
.returns(1L, from(options -> options.getMaxCommitTime(TimeUnit.MINUTES))) //
167+
.returns(ReadConcern.AVAILABLE, from(TransactionOptions::getReadConcern)) //
168+
.returns(ReadPreference.secondaryPreferred(), from(TransactionOptions::getReadPreference)) //
169+
.returns(WriteConcern.W3, from(TransactionOptions::getWriteConcern));
170+
}
171+
172+
@Test // GH-1628
173+
public void shouldReturnNewOptionsIfLabelsContainAllOptions() {
174+
TransactionOptions fallbackOptions = getTransactionOptions();
175+
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
176+
Set<String> labels = Set.of("mongo:maxCommitTime=PT5S", "mongo:readConcern=majority",
177+
"mongo:readPreference=primaryPreferred", "mongo:writeConcern=w3");
178+
attribute.setLabels(labels);
179+
180+
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, fallbackOptions);
181+
182+
assertThat(result).isNotSameAs(fallbackOptions) //
183+
.returns(5L, from(options -> options.getMaxCommitTime(TimeUnit.SECONDS))) //
184+
.returns(ReadConcern.MAJORITY, from(TransactionOptions::getReadConcern)) //
185+
.returns(ReadPreference.primaryPreferred(), from(TransactionOptions::getReadPreference)) //
186+
.returns(WriteConcern.W3, from(TransactionOptions::getWriteConcern));
187+
}
188+
189+
@Test // GH-1628
190+
public void shouldReturnMergedOptionsIfLabelsContainOptionsMixedWithOrdinaryStrings() {
191+
TransactionOptions fallbackOptions = getTransactionOptions();
192+
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
193+
Set<String> labels = Set.of("mongo:maxCommitTime=PT5S", "mongo:nonExistentOption=value", "label",
194+
"mongo:writeConcern=w3");
195+
attribute.setLabels(labels);
196+
197+
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, fallbackOptions);
198+
199+
assertThat(result).isNotSameAs(fallbackOptions) //
200+
.returns(5L, from(options -> options.getMaxCommitTime(TimeUnit.SECONDS))) //
201+
.returns(ReadConcern.AVAILABLE, from(TransactionOptions::getReadConcern)) //
202+
.returns(ReadPreference.secondaryPreferred(), from(TransactionOptions::getReadPreference)) //
203+
.returns(WriteConcern.W3, from(TransactionOptions::getWriteConcern));
204+
}
205+
206+
@Test // GH-1628
207+
public void shouldReturnNewOptionsIFallbackIsNull() {
208+
DefaultTransactionAttribute attribute = new DefaultTransactionAttribute();
209+
Set<String> labels = Set.of("mongo:maxCommitTime=PT5S", "mongo:writeConcern=w3");
210+
attribute.setLabels(labels);
211+
212+
TransactionOptions result = MongoTransactionUtils.extractOptions(attribute, null);
213+
214+
assertThat(result).returns(5L, from(options -> options.getMaxCommitTime(TimeUnit.SECONDS))) //
215+
.returns(null, from(TransactionOptions::getReadConcern)) //
216+
.returns(null, from(TransactionOptions::getReadPreference)) //
217+
.returns(WriteConcern.W3, from(TransactionOptions::getWriteConcern));
218+
}
219+
220+
private TransactionOptions getTransactionOptions() {
221+
return TransactionOptions.builder() //
222+
.maxCommitTime(1L, TimeUnit.MINUTES) //
223+
.readConcern(ReadConcern.AVAILABLE) //
224+
.readPreference(ReadPreference.secondaryPreferred()) //
225+
.writeConcern(WriteConcern.UNACKNOWLEDGED).build();
226+
}
227+
}

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

Lines changed: 144 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.mongodb;
1717

18+
import static java.util.UUID.*;
19+
1820
import reactor.core.publisher.Flux;
1921
import reactor.core.publisher.Mono;
2022
import reactor.test.StepVerifier;
@@ -35,6 +37,7 @@
3537
import org.springframework.context.annotation.Bean;
3638
import org.springframework.context.annotation.Configuration;
3739
import org.springframework.context.support.GenericApplicationContext;
40+
import org.springframework.dao.InvalidDataAccessApiUsageException;
3841
import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration;
3942
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
4043
import org.springframework.data.mongodb.core.mapping.Document;
@@ -44,6 +47,8 @@
4447
import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable;
4548
import org.springframework.data.mongodb.test.util.MongoClientExtension;
4649
import org.springframework.data.mongodb.test.util.MongoTestUtils;
50+
import org.springframework.transaction.TransactionSystemException;
51+
import org.springframework.transaction.annotation.EnableTransactionManagement;
4752
import org.springframework.transaction.annotation.Transactional;
4853
import org.springframework.transaction.reactive.TransactionalOperator;
4954
import org.springframework.transaction.support.DefaultTransactionDefinition;
@@ -55,6 +60,7 @@
5560
*
5661
* @author Mark Paluch
5762
* @author Christoph Strobl
63+
* @author Yan Kardziyaka
5864
*/
5965
@ExtendWith(MongoClientExtension.class)
6066
@EnableIfMongoServerVersion(isGreaterThanEqual = "4.0")
@@ -69,6 +75,7 @@ public class ReactiveTransactionIntegrationTests {
6975

7076
PersonService personService;
7177
ReactiveMongoOperations operations;
78+
ReactiveTransactionOptionsTestService<Person> transactionOptionsTestService;
7279

7380
@BeforeAll
7481
public static void init() {
@@ -85,6 +92,7 @@ public void setUp() {
8592

8693
personService = context.getBean(PersonService.class);
8794
operations = context.getBean(ReactiveMongoOperations.class);
95+
transactionOptionsTestService = context.getBean(ReactiveTransactionOptionsTestService.class);
8896

8997
try (MongoClient client = MongoTestUtils.reactiveClient()) {
9098

@@ -220,7 +228,123 @@ public void errorAfterTxShouldNotAffectPreviousStep() {
220228
.verifyComplete();
221229
}
222230

231+
@Test // GH-1628
232+
public void shouldThrowTransactionSystemExceptionOnTransactionWithInvalidMaxCommitTime() {
233+
234+
Person person = new Person(ObjectId.get(), randomUUID().toString(), randomUUID().toString());
235+
transactionOptionsTestService.saveWithInvalidMaxCommitTime(person) //
236+
.as(StepVerifier::create) //
237+
.verifyError(TransactionSystemException.class);
238+
239+
operations.count(new Query(), Person.class) //
240+
.as(StepVerifier::create) //
241+
.expectNext(0L) //
242+
.verifyComplete();
243+
}
244+
245+
@Test // GH-1628
246+
public void shouldCommitOnTransactionWithinMaxCommitTime() {
247+
248+
Person person = new Person(ObjectId.get(), randomUUID().toString(), randomUUID().toString());
249+
transactionOptionsTestService.saveWithinMaxCommitTime(person) //
250+
.as(StepVerifier::create) //
251+
.expectNext(person) //
252+
.verifyComplete();
253+
254+
operations.count(new Query(), Person.class) //
255+
.as(StepVerifier::create) //
256+
.expectNext(1L) //
257+
.verifyComplete();
258+
}
259+
260+
@Test // GH-1628
261+
public void shouldThrowInvalidDataAccessApiUsageExceptionOnTransactionWithAvailableReadConcern() {
262+
transactionOptionsTestService.availableReadConcernFind(randomUUID().toString()) //
263+
.as(StepVerifier::create) //
264+
.verifyError(InvalidDataAccessApiUsageException.class);
265+
}
266+
267+
@Test // GH-1628
268+
public void shouldThrowTransactionSystemExceptionOnTransactionWithInvalidReadConcern() {
269+
transactionOptionsTestService.invalidReadConcernFind(randomUUID().toString()) //
270+
.as(StepVerifier::create) //
271+
.verifyError(TransactionSystemException.class);
272+
}
273+
274+
@Test // GH-1628
275+
public void shouldNotThrowOnTransactionWithMajorityReadConcern() {
276+
transactionOptionsTestService.majorityReadConcernFind(randomUUID().toString()) //
277+
.as(StepVerifier::create) //
278+
.expectNextCount(0L) //
279+
.verifyComplete();
280+
}
281+
282+
@Test // GH-1628
283+
public void shouldThrowUncategorizedMongoDbExceptionOnTransactionWithPrimaryPreferredReadPreference() {
284+
transactionOptionsTestService.findFromPrimaryPreferredReplica(randomUUID().toString()) //
285+
.as(StepVerifier::create) //
286+
.verifyError(UncategorizedMongoDbException.class);
287+
}
288+
289+
@Test // GH-1628
290+
public void shouldThrowTransactionSystemExceptionOnTransactionWithInvalidReadPreference() {
291+
transactionOptionsTestService.findFromInvalidReplica(randomUUID().toString()) //
292+
.as(StepVerifier::create) //
293+
.verifyError(TransactionSystemException.class);
294+
}
295+
296+
@Test // GH-1628
297+
public void shouldNotThrowOnTransactionWithPrimaryReadPreference() {
298+
transactionOptionsTestService.findFromPrimaryReplica(randomUUID().toString()) //
299+
.as(StepVerifier::create) //
300+
.expectNextCount(0L) //
301+
.verifyComplete();
302+
}
303+
304+
@Test // GH-1628
305+
public void shouldThrowTransactionSystemExceptionOnTransactionWithUnacknowledgedWriteConcern() {
306+
307+
Person person = new Person(ObjectId.get(), randomUUID().toString(), randomUUID().toString());
308+
transactionOptionsTestService.unacknowledgedWriteConcernSave(person) //
309+
.as(StepVerifier::create) //
310+
.verifyError(TransactionSystemException.class);
311+
312+
operations.count(new Query(), Person.class) //
313+
.as(StepVerifier::create).expectNext(0L) //
314+
.verifyComplete();
315+
}
316+
317+
@Test // GH-1628
318+
public void shouldThrowTransactionSystemExceptionOnTransactionWithInvalidWriteConcern() {
319+
320+
Person person = new Person(ObjectId.get(), randomUUID().toString(), randomUUID().toString());
321+
transactionOptionsTestService.invalidWriteConcernSave(person) //
322+
.as(StepVerifier::create) //
323+
.verifyError(TransactionSystemException.class);
324+
325+
operations.count(new Query(), Person.class) //
326+
.as(StepVerifier::create) //
327+
.expectNext(0L) //
328+
.verifyComplete();
329+
}
330+
331+
@Test // GH-1628
332+
public void shouldCommitOnTransactionWithAcknowledgedWriteConcern() {
333+
334+
Person person = new Person(ObjectId.get(), randomUUID().toString(), randomUUID().toString());
335+
transactionOptionsTestService.acknowledgedWriteConcernSave(person) //
336+
.as(StepVerifier::create) //
337+
.expectNext(person) //
338+
.verifyComplete();
339+
340+
operations.count(new Query(), Person.class) //
341+
.as(StepVerifier::create) //
342+
.expectNext(1L) //
343+
.verifyComplete();
344+
}
345+
223346
@Configuration
347+
@EnableTransactionManagement
224348
static class TestMongoConfig extends AbstractReactiveMongoConfiguration {
225349

226350
@Override
@@ -234,10 +358,16 @@ protected String getDatabaseName() {
234358
}
235359

236360
@Bean
237-
public ReactiveMongoTransactionManager transactionManager(ReactiveMongoDatabaseFactory factory) {
361+
public ReactiveMongoTransactionManager txManager(ReactiveMongoDatabaseFactory factory) {
238362
return new ReactiveMongoTransactionManager(factory);
239363
}
240364

365+
@Bean
366+
public ReactiveTransactionOptionsTestService<Person> transactionOptionsTestService(
367+
ReactiveMongoOperations operations) {
368+
return new ReactiveTransactionOptionsTestService<>(operations, Person.class);
369+
}
370+
241371
@Override
242372
protected Set<Class<?>> getInitialEntitySet() {
243373
return Collections.singleton(Person.class);
@@ -291,10 +421,10 @@ public Flux<EventLog> saveWithLogs(Person person) {
291421
new DefaultTransactionDefinition());
292422

293423
return Flux.merge(operations.save(new EventLog(new ObjectId(), "beforeConvert")), //
294-
operations.save(new EventLog(new ObjectId(), "afterConvert")), //
295-
operations.save(new EventLog(new ObjectId(), "beforeInsert")), //
296-
operations.save(person), //
297-
operations.save(new EventLog(new ObjectId(), "afterInsert"))) //
424+
operations.save(new EventLog(new ObjectId(), "afterConvert")), //
425+
operations.save(new EventLog(new ObjectId(), "beforeInsert")), //
426+
operations.save(person), //
427+
operations.save(new EventLog(new ObjectId(), "afterInsert"))) //
298428
.thenMany(operations.query(EventLog.class).all()) //
299429
.as(transactionalOperator::transactional);
300430
}
@@ -305,15 +435,15 @@ public Flux<Void> saveWithErrorLogs(Person person) {
305435
new DefaultTransactionDefinition());
306436

307437
return Flux.merge(operations.save(new EventLog(new ObjectId(), "beforeConvert")), //
308-
operations.save(new EventLog(new ObjectId(), "afterConvert")), //
309-
operations.save(new EventLog(new ObjectId(), "beforeInsert")), //
310-
operations.save(person), //
311-
operations.save(new EventLog(new ObjectId(), "afterInsert"))) //
438+
operations.save(new EventLog(new ObjectId(), "afterConvert")), //
439+
operations.save(new EventLog(new ObjectId(), "beforeInsert")), //
440+
operations.save(person), //
441+
operations.save(new EventLog(new ObjectId(), "afterInsert"))) //
312442
.<Void> flatMap(it -> Mono.error(new RuntimeException("poof"))) //
313443
.as(transactionalOperator::transactional);
314444
}
315445

316-
@Transactional
446+
@Transactional(transactionManager = "txManager")
317447
public Flux<Person> declarativeSavePerson(Person person) {
318448

319449
TransactionalOperator transactionalOperator = TransactionalOperator.create(manager,
@@ -324,7 +454,7 @@ public Flux<Person> declarativeSavePerson(Person person) {
324454
});
325455
}
326456

327-
@Transactional
457+
@Transactional(transactionManager = "txManager")
328458
public Flux<Person> declarativeSavePersonErrors(Person person) {
329459

330460
TransactionalOperator transactionalOperator = TransactionalOperator.create(manager,
@@ -384,8 +514,8 @@ public boolean equals(Object o) {
384514
return false;
385515
}
386516
Person person = (Person) o;
387-
return Objects.equals(id, person.id) && Objects.equals(firstname, person.firstname)
388-
&& Objects.equals(lastname, person.lastname);
517+
return Objects.equals(id, person.id) && Objects.equals(firstname, person.firstname) && Objects.equals(lastname,
518+
person.lastname);
389519
}
390520

391521
@Override
@@ -394,8 +524,7 @@ public int hashCode() {
394524
}
395525

396526
public String toString() {
397-
return "ReactiveTransactionIntegrationTests.Person(id=" + this.getId() + ", firstname=" + this.getFirstname()
398-
+ ", lastname=" + this.getLastname() + ")";
527+
return "ReactiveTransactionIntegrationTests.Person(id=" + this.getId() + ", firstname=" + this.getFirstname() + ", lastname=" + this.getLastname() + ")";
399528
}
400529
}
401530

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2023 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+
* https://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;
17+
18+
import reactor.core.publisher.Mono;
19+
20+
import java.util.function.Function;
21+
22+
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
23+
import org.springframework.transaction.annotation.Transactional;
24+
25+
/**
26+
* Helper class for integration tests of {@link Transactional#label()} MongoDb options in reactive context.
27+
*
28+
* @param <T> root document type
29+
* @author Yan Kardziyaka
30+
* @see org.springframework.data.mongodb.core.TransactionOptionsTestService
31+
*/
32+
public class ReactiveTransactionOptionsTestService<T> {
33+
private final Function<Object, Mono<T>> findByIdFunction;
34+
35+
private final Function<T, Mono<T>> saveFunction;
36+
37+
public ReactiveTransactionOptionsTestService(ReactiveMongoOperations operations, Class<T> entityClass) {
38+
this.findByIdFunction = id -> operations.findById(id, entityClass);
39+
this.saveFunction = operations::save;
40+
}
41+
42+
@Transactional(transactionManager = "txManager", label = { "mongo:maxCommitTime=-PT6H3M" })
43+
public Mono<T> saveWithInvalidMaxCommitTime(T entity) {
44+
return saveFunction.apply(entity);
45+
}
46+
47+
@Transactional(transactionManager = "txManager", label = { "mongo:maxCommitTime=PT1M" })
48+
public Mono<T> saveWithinMaxCommitTime(T entity) {
49+
return saveFunction.apply(entity);
50+
}
51+
52+
@Transactional(transactionManager = "txManager", label = { "mongo:readConcern=available" })
53+
public Mono<T> availableReadConcernFind(Object id) {
54+
return findByIdFunction.apply(id);
55+
}
56+
57+
@Transactional(transactionManager = "txManager", label = { "mongo:readConcern=invalid" })
58+
public Mono<T> invalidReadConcernFind(Object id) {
59+
return findByIdFunction.apply(id);
60+
}
61+
62+
@Transactional(transactionManager = "txManager", label = { "mongo:readConcern=majority" })
63+
public Mono<T> majorityReadConcernFind(Object id) {
64+
return findByIdFunction.apply(id);
65+
}
66+
67+
@Transactional(transactionManager = "txManager", label = { "mongo:readPreference=primaryPreferred" })
68+
public Mono<T> findFromPrimaryPreferredReplica(Object id) {
69+
return findByIdFunction.apply(id);
70+
}
71+
72+
@Transactional(transactionManager = "txManager", label = { "mongo:readPreference=invalid" })
73+
public Mono<T> findFromInvalidReplica(Object id) {
74+
return findByIdFunction.apply(id);
75+
}
76+
77+
@Transactional(transactionManager = "txManager", label = { "mongo:readPreference=primary" })
78+
public Mono<T> findFromPrimaryReplica(Object id) {
79+
return findByIdFunction.apply(id);
80+
}
81+
82+
@Transactional(transactionManager = "txManager", label = { "mongo:writeConcern=unacknowledged" })
83+
public Mono<T> unacknowledgedWriteConcernSave(T entity) {
84+
return saveFunction.apply(entity);
85+
}
86+
87+
@Transactional(transactionManager = "txManager", label = { "mongo:writeConcern=invalid" })
88+
public Mono<T> invalidWriteConcernSave(T entity) {
89+
return saveFunction.apply(entity);
90+
}
91+
92+
@Transactional(transactionManager = "txManager", label = { "mongo:writeConcern=acknowledged" })
93+
public Mono<T> acknowledgedWriteConcernSave(T entity) {
94+
return saveFunction.apply(entity);
95+
}
96+
}

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

Lines changed: 139 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.mongodb.core;
1717

18+
import static java.util.UUID.*;
1819
import static org.assertj.core.api.Assertions.*;
1920
import static org.springframework.data.mongodb.core.query.Criteria.*;
2021
import static org.springframework.data.mongodb.core.query.Query.*;
@@ -33,10 +34,12 @@
3334
import org.springframework.beans.factory.annotation.Autowired;
3435
import org.springframework.context.annotation.Bean;
3536
import org.springframework.context.annotation.Configuration;
37+
import org.springframework.dao.InvalidDataAccessApiUsageException;
3638
import org.springframework.data.annotation.Id;
3739
import org.springframework.data.domain.Persistable;
3840
import org.springframework.data.mongodb.MongoDatabaseFactory;
3941
import org.springframework.data.mongodb.MongoTransactionManager;
42+
import org.springframework.data.mongodb.UncategorizedMongoDbException;
4043
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
4144
import org.springframework.data.mongodb.test.util.AfterTransactionAssertion;
4245
import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion;
@@ -48,6 +51,9 @@
4851
import org.springframework.test.context.junit.jupiter.SpringExtension;
4952
import org.springframework.test.context.transaction.AfterTransaction;
5053
import org.springframework.test.context.transaction.BeforeTransaction;
54+
import org.springframework.transaction.TransactionSystemException;
55+
import org.springframework.transaction.annotation.EnableTransactionManagement;
56+
import org.springframework.transaction.annotation.Propagation;
5157
import org.springframework.transaction.annotation.Transactional;
5258

5359
import com.mongodb.ReadPreference;
@@ -57,6 +63,7 @@
5763

5864
/**
5965
* @author Christoph Strobl
66+
* @author Yan Kardziyaka
6067
* @currentRead Shadow's Edge - Brent Weeks
6168
*/
6269
@ExtendWith({ MongoClientExtension.class, SpringExtension.class })
@@ -72,6 +79,7 @@ public class MongoTemplateTransactionTests {
7279
static @ReplSetClient MongoClient mongoClient;
7380

7481
@Configuration
82+
@EnableTransactionManagement
7583
static class Config extends AbstractMongoClientConfiguration {
7684

7785
@Bean
@@ -98,10 +106,19 @@ MongoTransactionManager txManager(MongoDatabaseFactory dbFactory) {
98106
protected Set<Class<?>> getInitialEntitySet() throws ClassNotFoundException {
99107
return Collections.emptySet();
100108
}
109+
110+
@Bean
111+
public TransactionOptionsTestService<Assassin> transactionOptionsTestService(MongoOperations operations) {
112+
return new TransactionOptionsTestService<>(operations, Assassin.class);
113+
}
101114
}
102115

103-
@Autowired MongoTemplate template;
104-
@Autowired MongoClient client;
116+
@Autowired
117+
MongoTemplate template;
118+
@Autowired
119+
MongoClient client;
120+
@Autowired
121+
TransactionOptionsTestService<Assassin> transactionOptionsTestService;
105122

106123
List<AfterTransactionAssertion<? extends Persistable<?>>> assertionList;
107124

@@ -127,8 +144,8 @@ public void verifyDbState() {
127144

128145
boolean isPresent = collection.countDocuments(Filters.eq("_id", it.getId())) != 0;
129146

130-
assertThat(isPresent).isEqualTo(it.shouldBePresent())
131-
.withFailMessage(String.format("After transaction entity %s should %s.", it.getPersistable(),
147+
assertThat(isPresent).isEqualTo(it.shouldBePresent()).withFailMessage(
148+
String.format("After transaction entity %s should %s.", it.getPersistable(),
132149
it.shouldBePresent() ? "be present" : "NOT be present"));
133150
});
134151
}
@@ -166,6 +183,122 @@ public void shouldBeAbleToViewChangesDuringTransaction() throws InterruptedExcep
166183
assertAfterTransaction(durzo).isNotPresent();
167184
}
168185

186+
@Rollback(false)
187+
@Test // GH-1628
188+
@Transactional(transactionManager = "txManager", propagation = Propagation.NEVER)
189+
public void shouldThrowIllegalArgumentExceptionOnTransactionWithInvalidMaxCommitTime() {
190+
191+
Assassin assassin = new Assassin(randomUUID().toString(), randomUUID().toString());
192+
193+
assertThatThrownBy(() -> transactionOptionsTestService.saveWithInvalidMaxCommitTime(assassin)) //
194+
.isInstanceOf(IllegalArgumentException.class);
195+
196+
assertAfterTransaction(assassin).isNotPresent();
197+
}
198+
199+
@Rollback(false)
200+
@Test // GH-1628
201+
@Transactional(transactionManager = "txManager", propagation = Propagation.NEVER)
202+
public void shouldCommitOnTransactionWithinMaxCommitTime() {
203+
204+
Assassin assassin = new Assassin(randomUUID().toString(), randomUUID().toString());
205+
206+
transactionOptionsTestService.saveWithinMaxCommitTime(assassin);
207+
208+
assertAfterTransaction(assassin).isPresent();
209+
}
210+
211+
@Rollback(false)
212+
@Test // GH-1628
213+
@Transactional(transactionManager = "txManager", propagation = Propagation.NEVER)
214+
public void shouldThrowInvalidDataAccessApiUsageExceptionOnTransactionWithAvailableReadConcern() {
215+
216+
assertThatThrownBy(() -> transactionOptionsTestService.availableReadConcernFind(randomUUID().toString())) //
217+
.isInstanceOf(InvalidDataAccessApiUsageException.class);
218+
}
219+
220+
@Rollback(false)
221+
@Test // GH-1628
222+
@Transactional(transactionManager = "txManager", propagation = Propagation.NEVER)
223+
public void shouldThrowIllegalArgumentExceptionOnTransactionWithInvalidReadConcern() {
224+
225+
assertThatThrownBy(() -> transactionOptionsTestService.invalidReadConcernFind(randomUUID().toString())) //
226+
.isInstanceOf(IllegalArgumentException.class);
227+
}
228+
229+
@Rollback(false)
230+
@Test // GH-1628
231+
@Transactional(transactionManager = "txManager", propagation = Propagation.NEVER)
232+
public void shouldNotThrowOnTransactionWithMajorityReadConcern() {
233+
assertThatNoException() //
234+
.isThrownBy(() -> transactionOptionsTestService.majorityReadConcernFind(randomUUID().toString()));
235+
}
236+
237+
@Rollback(false)
238+
@Test // GH-1628
239+
@Transactional(transactionManager = "txManager", propagation = Propagation.NEVER)
240+
public void shouldThrowUncategorizedMongoDbExceptionOnTransactionWithPrimaryPreferredReadPreference() {
241+
242+
assertThatThrownBy(() -> transactionOptionsTestService.findFromPrimaryPreferredReplica(randomUUID().toString())) //
243+
.isInstanceOf(UncategorizedMongoDbException.class);
244+
}
245+
246+
@Rollback(false)
247+
@Test // GH-1628
248+
@Transactional(transactionManager = "txManager", propagation = Propagation.NEVER)
249+
public void shouldThrowIllegalArgumentExceptionOnTransactionWithInvalidReadPreference() {
250+
251+
assertThatThrownBy(() -> transactionOptionsTestService.findFromInvalidReplica(randomUUID().toString())) //
252+
.isInstanceOf(IllegalArgumentException.class);
253+
}
254+
255+
@Rollback(false)
256+
@Test // GH-1628
257+
@Transactional(transactionManager = "txManager", propagation = Propagation.NEVER)
258+
public void shouldNotThrowOnTransactionWithPrimaryReadPreference() {
259+
260+
assertThatNoException() //
261+
.isThrownBy(() -> transactionOptionsTestService.findFromPrimaryReplica(randomUUID().toString()));
262+
}
263+
264+
@Rollback(false)
265+
@Test // GH-1628
266+
@Transactional(transactionManager = "txManager", propagation = Propagation.NEVER)
267+
public void shouldThrowTransactionSystemExceptionOnTransactionWithUnacknowledgedWriteConcern() {
268+
269+
Assassin assassin = new Assassin(randomUUID().toString(), randomUUID().toString());
270+
271+
assertThatThrownBy(() -> transactionOptionsTestService.unacknowledgedWriteConcernSave(assassin)) //
272+
.isInstanceOf(TransactionSystemException.class);
273+
274+
assertAfterTransaction(assassin).isNotPresent();
275+
}
276+
277+
@Rollback(false)
278+
@Test // GH-1628
279+
@Transactional(transactionManager = "txManager", propagation = Propagation.NEVER)
280+
public void shouldThrowIllegalArgumentExceptionOnTransactionWithInvalidWriteConcern() {
281+
282+
Assassin assassin = new Assassin(randomUUID().toString(), randomUUID().toString());
283+
284+
assertThatThrownBy(() -> transactionOptionsTestService.invalidWriteConcernSave(assassin)) //
285+
.isInstanceOf(IllegalArgumentException.class);
286+
287+
assertAfterTransaction(assassin).isNotPresent();
288+
}
289+
290+
@Rollback(false)
291+
@Test // GH-1628
292+
@Transactional(transactionManager = "txManager", propagation = Propagation.NEVER)
293+
public void shouldCommitOnTransactionWithAcknowledgedWriteConcern() {
294+
295+
Assassin assassin = new Assassin(randomUUID().toString(), randomUUID().toString());
296+
297+
transactionOptionsTestService.acknowledgedWriteConcernSave(assassin);
298+
299+
assertAfterTransaction(assassin).isPresent();
300+
}
301+
169302
// --- Just some helpers and tests entities
170303

171304
private AfterTransactionAssertion assertAfterTransaction(Assassin assassin) {
@@ -178,7 +311,8 @@ private AfterTransactionAssertion assertAfterTransaction(Assassin assassin) {
178311
@org.springframework.data.mongodb.core.mapping.Document(COLLECTION_NAME)
179312
static class Assassin implements Persistable<String> {
180313

181-
@Id String id;
314+
@Id
315+
String id;
182316
String name;
183317

184318
public Assassin(String id, String name) {
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2023 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+
* https://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 java.util.function.Function;
19+
import java.util.function.UnaryOperator;
20+
21+
import org.springframework.lang.Nullable;
22+
import org.springframework.transaction.annotation.Transactional;
23+
24+
/**
25+
* Helper class for integration tests of {@link Transactional#label()} MongoDb options in non-reactive context.
26+
*
27+
* @param <T> root document type
28+
* @author Yan Kardziyaka
29+
* @see org.springframework.data.mongodb.ReactiveTransactionOptionsTestService
30+
*/
31+
public class TransactionOptionsTestService<T> {
32+
33+
private final Function<Object, T> findByIdFunction;
34+
private final UnaryOperator<T> saveFunction;
35+
36+
public TransactionOptionsTestService(MongoOperations operations, Class<T> entityClass) {
37+
this.findByIdFunction = id -> operations.findById(id, entityClass);
38+
this.saveFunction = operations::save;
39+
}
40+
41+
@Transactional(transactionManager = "txManager", label = { "mongo:maxCommitTime=-PT6H3M" })
42+
public T saveWithInvalidMaxCommitTime(T entity) {
43+
return saveFunction.apply(entity);
44+
}
45+
46+
@Transactional(transactionManager = "txManager", label = { "mongo:maxCommitTime=PT1M" })
47+
public T saveWithinMaxCommitTime(T entity) {
48+
return saveFunction.apply(entity);
49+
}
50+
51+
@Nullable
52+
@Transactional(transactionManager = "txManager", label = { "mongo:readConcern=available" })
53+
public T availableReadConcernFind(Object id) {
54+
return findByIdFunction.apply(id);
55+
}
56+
57+
@Nullable
58+
@Transactional(transactionManager = "txManager", label = { "mongo:readConcern=invalid" })
59+
public T invalidReadConcernFind(Object id) {
60+
return findByIdFunction.apply(id);
61+
}
62+
63+
@Nullable
64+
@Transactional(transactionManager = "txManager", label = { "mongo:readConcern=majority" })
65+
public T majorityReadConcernFind(Object id) {
66+
return findByIdFunction.apply(id);
67+
}
68+
69+
@Nullable
70+
@Transactional(transactionManager = "txManager", label = { "mongo:readPreference=primaryPreferred" })
71+
public T findFromPrimaryPreferredReplica(Object id) {
72+
return findByIdFunction.apply(id);
73+
}
74+
75+
@Nullable
76+
@Transactional(transactionManager = "txManager", label = { "mongo:readPreference=invalid" })
77+
public T findFromInvalidReplica(Object id) {
78+
return findByIdFunction.apply(id);
79+
}
80+
81+
@Nullable
82+
@Transactional(transactionManager = "txManager", label = { "mongo:readPreference=primary" })
83+
public T findFromPrimaryReplica(Object id) {
84+
return findByIdFunction.apply(id);
85+
}
86+
87+
@Transactional(transactionManager = "txManager", label = { "mongo:writeConcern=unacknowledged" })
88+
public T unacknowledgedWriteConcernSave(T entity) {
89+
return saveFunction.apply(entity);
90+
}
91+
92+
@Transactional(transactionManager = "txManager", label = { "mongo:writeConcern=invalid" })
93+
public T invalidWriteConcernSave(T entity) {
94+
return saveFunction.apply(entity);
95+
}
96+
97+
@Transactional(transactionManager = "txManager", label = { "mongo:writeConcern=acknowledged" })
98+
public T acknowledgedWriteConcernSave(T entity) {
99+
return saveFunction.apply(entity);
100+
}
101+
}

0 commit comments

Comments
 (0)
Please sign in to comment.