Skip to content

Commit d721579

Browse files
christophstroblmp911de
authored andcommitted
Introduce MongoTransactionResolver.
See #1628 Original pull request: #4552
1 parent 1f2cf88 commit d721579

18 files changed

+1092
-358
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2024 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.util.Map;
19+
import java.util.Set;
20+
21+
import org.springframework.data.util.Lazy;
22+
import org.springframework.lang.Nullable;
23+
24+
/**
25+
* Default implementation of {@link MongoTransactionOptions} using {@literal mongo:} as {@link #getLabelPrefix() label
26+
* prefix} creating {@link SimpleMongoTransactionOptions} out of a given argument {@link Map}. Uses
27+
* {@link SimpleMongoTransactionOptions#KNOWN_KEYS} to validate entries in arguments to resolve and errors on unknown
28+
* entries.
29+
*
30+
* @author Christoph Strobl
31+
* @since 4.3
32+
*/
33+
class DefaultMongoTransactionOptionsResolver implements MongoTransactionOptionsResolver {
34+
35+
static final Lazy<MongoTransactionOptionsResolver> INSTANCE = Lazy.of(DefaultMongoTransactionOptionsResolver::new);
36+
37+
private static final String PREFIX = "mongo:";
38+
39+
private DefaultMongoTransactionOptionsResolver() {}
40+
41+
@Override
42+
public MongoTransactionOptions convert(Map<String, String> options) {
43+
44+
validateKeys(options.keySet());
45+
return SimpleMongoTransactionOptions.of(options);
46+
}
47+
48+
@Nullable
49+
@Override
50+
public String getLabelPrefix() {
51+
return PREFIX;
52+
}
53+
54+
private static void validateKeys(Set<String> keys) {
55+
56+
if (!keys.stream().allMatch(SimpleMongoTransactionOptions.KNOWN_KEYS::contains)) {
57+
58+
throw new IllegalArgumentException("Transaction labels contained invalid values. Has to be one of %s"
59+
.formatted(SimpleMongoTransactionOptions.KNOWN_KEYS));
60+
}
61+
}
62+
}

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java

+20-4
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager
6565
implements ResourceTransactionManager, InitializingBean {
6666

6767
private @Nullable MongoDatabaseFactory dbFactory;
68-
private @Nullable TransactionOptions options;
68+
private MongoTransactionOptions options;
69+
private MongoTransactionOptionsResolver transactionOptionsResolver;
6970

7071
/**
7172
* Create a new {@link MongoTransactionManager} for bean-style usage.
@@ -99,11 +100,25 @@ public MongoTransactionManager(MongoDatabaseFactory dbFactory) {
99100
* @param options can be {@literal null}.
100101
*/
101102
public MongoTransactionManager(MongoDatabaseFactory dbFactory, @Nullable TransactionOptions options) {
103+
this(dbFactory, MongoTransactionOptionsResolver.defaultResolver(), MongoTransactionOptions.of(options));
104+
}
105+
106+
/**
107+
* Create a new {@link MongoTransactionManager} obtaining sessions from the given {@link MongoDatabaseFactory}
108+
* applying the given {@link TransactionOptions options}, if present, when starting a new transaction.
109+
*
110+
* @param dbFactory must not be {@literal null}.
111+
* @param transactionOptionsResolver
112+
* @param defaultTransactionOptions can be {@literal null}.
113+
* @since 4.3
114+
*/
115+
public MongoTransactionManager(MongoDatabaseFactory dbFactory, MongoTransactionOptionsResolver transactionOptionsResolver, MongoTransactionOptions defaultTransactionOptions) {
102116

103117
Assert.notNull(dbFactory, "DbFactory must not be null");
104118

105119
this.dbFactory = dbFactory;
106-
this.options = options;
120+
this.transactionOptionsResolver = transactionOptionsResolver;
121+
this.options = defaultTransactionOptions;
107122
}
108123

109124
@Override
@@ -134,7 +149,8 @@ protected void doBegin(Object transaction, TransactionDefinition definition) thr
134149
}
135150

136151
try {
137-
mongoTransactionObject.startTransaction(MongoTransactionUtils.extractOptions(definition, options));
152+
MongoTransactionOptions mongoTransactionOptions = transactionOptionsResolver.resolve(definition).mergeWith(options);
153+
mongoTransactionObject.startTransaction(mongoTransactionOptions.toDriverOptions());
138154
} catch (MongoException ex) {
139155
throw new TransactionSystemException(String.format("Could not start Mongo transaction for session %s.",
140156
debugString(mongoTransactionObject.getSession())), ex);
@@ -276,7 +292,7 @@ public void setDbFactory(MongoDatabaseFactory dbFactory) {
276292
* @param options can be {@literal null}.
277293
*/
278294
public void setOptions(@Nullable TransactionOptions options) {
279-
this.options = options;
295+
this.options = MongoTransactionOptions.of(options);
280296
}
281297

282298
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
* Copyright 2024 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.springframework.data.mongodb.core.ReadConcernAware;
22+
import org.springframework.data.mongodb.core.ReadPreferenceAware;
23+
import org.springframework.data.mongodb.core.WriteConcernAware;
24+
import org.springframework.lang.Nullable;
25+
26+
import com.mongodb.Function;
27+
import com.mongodb.ReadConcern;
28+
import com.mongodb.ReadPreference;
29+
import com.mongodb.TransactionOptions;
30+
import com.mongodb.WriteConcern;
31+
32+
/**
33+
* Options to be applied within a specific transaction scope.
34+
*
35+
* @author Christoph Strobl
36+
* @since 4.3
37+
*/
38+
public interface MongoTransactionOptions
39+
extends TransactionMetadata, ReadConcernAware, ReadPreferenceAware, WriteConcernAware {
40+
41+
/**
42+
* Value Object representing empty options enforcing client defaults. Returns {@literal null} for all getter methods.
43+
*/
44+
MongoTransactionOptions NONE = new MongoTransactionOptions() {
45+
46+
@Nullable
47+
@Override
48+
public Duration getMaxCommitTime() {
49+
return null;
50+
}
51+
52+
@Nullable
53+
@Override
54+
public ReadConcern getReadConcern() {
55+
return null;
56+
}
57+
58+
@Nullable
59+
@Override
60+
public ReadPreference getReadPreference() {
61+
return null;
62+
}
63+
64+
@Nullable
65+
@Override
66+
public WriteConcern getWriteConcern() {
67+
return null;
68+
}
69+
};
70+
71+
/**
72+
* Merge current options with given ones. Will return first non {@literal null} value from getters whereas the
73+
* {@literal this} has precedence over the given fallbackOptions.
74+
*
75+
* @param fallbackOptions can be {@literal null}.
76+
* @return new instance of {@link MongoTransactionOptions} or this if {@literal fallbackOptions} is {@literal null} or
77+
* {@link #NONE}.
78+
*/
79+
default MongoTransactionOptions mergeWith(@Nullable MongoTransactionOptions fallbackOptions) {
80+
81+
if (fallbackOptions == null || MongoTransactionOptions.NONE.equals(fallbackOptions)) {
82+
return this;
83+
}
84+
85+
return new MongoTransactionOptions() {
86+
87+
@Nullable
88+
@Override
89+
public Duration getMaxCommitTime() {
90+
return MongoTransactionOptions.this.hasMaxCommitTime() ? MongoTransactionOptions.this.getMaxCommitTime()
91+
: fallbackOptions.getMaxCommitTime();
92+
}
93+
94+
@Nullable
95+
@Override
96+
public ReadConcern getReadConcern() {
97+
return MongoTransactionOptions.this.hasReadConcern() ? MongoTransactionOptions.this.getReadConcern()
98+
: fallbackOptions.getReadConcern();
99+
}
100+
101+
@Nullable
102+
@Override
103+
public ReadPreference getReadPreference() {
104+
return MongoTransactionOptions.this.hasReadPreference() ? MongoTransactionOptions.this.getReadPreference()
105+
: fallbackOptions.getReadPreference();
106+
}
107+
108+
@Nullable
109+
@Override
110+
public WriteConcern getWriteConcern() {
111+
return MongoTransactionOptions.this.hasWriteConcern() ? MongoTransactionOptions.this.getWriteConcern()
112+
: fallbackOptions.getWriteConcern();
113+
}
114+
};
115+
}
116+
117+
/**
118+
* Map the current options using the given mapping {@link Function}.
119+
*
120+
* @param mappingFunction
121+
* @return instance of T.
122+
* @param <T>
123+
*/
124+
default <T> T as(Function<MongoTransactionOptions, T> mappingFunction) {
125+
return mappingFunction.apply(this);
126+
}
127+
128+
/**
129+
* @return MongoDB driver native {@link TransactionOptions}.
130+
* @see MongoTransactionOptions#as(Function)
131+
*/
132+
@Nullable
133+
default TransactionOptions toDriverOptions() {
134+
135+
return as(it -> {
136+
137+
if (MongoTransactionOptions.NONE.equals(it)) {
138+
return null;
139+
}
140+
141+
TransactionOptions.Builder builder = TransactionOptions.builder();
142+
if (it.hasMaxCommitTime()) {
143+
builder.maxCommitTime(it.getMaxCommitTime().toMillis(), TimeUnit.MILLISECONDS);
144+
}
145+
if (it.hasReadConcern()) {
146+
builder.readConcern(it.getReadConcern());
147+
}
148+
if (it.hasReadPreference()) {
149+
builder.readPreference(it.getReadPreference());
150+
}
151+
if (it.hasWriteConcern()) {
152+
builder.writeConcern(it.getWriteConcern());
153+
}
154+
return builder.build();
155+
});
156+
}
157+
158+
/**
159+
* Factory method to wrap given MongoDB driver native {@link TransactionOptions} into {@link MongoTransactionOptions}.
160+
*
161+
* @param options
162+
* @return {@link MongoTransactionOptions#NONE} if given object is {@literal null}.
163+
*/
164+
static MongoTransactionOptions of(@Nullable TransactionOptions options) {
165+
166+
if (options == null) {
167+
return NONE;
168+
}
169+
170+
return new MongoTransactionOptions() {
171+
172+
@Nullable
173+
@Override
174+
public Duration getMaxCommitTime() {
175+
176+
Long millis = options.getMaxCommitTime(TimeUnit.MILLISECONDS);
177+
return millis != null ? Duration.ofMillis(millis) : null;
178+
}
179+
180+
@Nullable
181+
@Override
182+
public ReadConcern getReadConcern() {
183+
return options.getReadConcern();
184+
}
185+
186+
@Nullable
187+
@Override
188+
public ReadPreference getReadPreference() {
189+
return options.getReadPreference();
190+
}
191+
192+
@Nullable
193+
@Override
194+
public WriteConcern getWriteConcern() {
195+
return options.getWriteConcern();
196+
}
197+
198+
@Nullable
199+
@Override
200+
public TransactionOptions toDriverOptions() {
201+
return options;
202+
}
203+
};
204+
}
205+
}

0 commit comments

Comments
 (0)