Skip to content

Commit 974e103

Browse files
committed
Add nested propagation support to R2dbcTransactionManager
Closes gh-30134
1 parent 22ef48b commit 974e103

File tree

3 files changed

+232
-78
lines changed

3 files changed

+232
-78
lines changed

spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionHolder.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -41,11 +41,20 @@
4141
*/
4242
public class ConnectionHolder extends ResourceHolderSupport {
4343

44+
/**
45+
* Prefix for savepoint names.
46+
* @since 6.0.10
47+
*/
48+
static final String SAVEPOINT_NAME_PREFIX = "SAVEPOINT_";
49+
50+
4451
@Nullable
4552
private Connection currentConnection;
4653

4754
private boolean transactionActive;
4855

56+
private int savepointCounter = 0;
57+
4958

5059
/**
5160
* Create a new ConnectionHolder for the given R2DBC {@link Connection},
@@ -112,6 +121,17 @@ public Connection getConnection() {
112121
return this.currentConnection;
113122
}
114123

124+
/**
125+
* Create a new savepoint for the current {@link Connection},
126+
* using generated savepoint names that are unique for the Connection.
127+
* @return the name of the new savepoint
128+
* @since 6.0.10
129+
*/
130+
String nextSavepoint() {
131+
this.savepointCounter++;
132+
return SAVEPOINT_NAME_PREFIX + this.savepointCounter;
133+
}
134+
115135
/**
116136
* Releases the current {@link Connection}.
117137
*/

spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java

Lines changed: 81 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import io.r2dbc.spi.Option;
2525
import io.r2dbc.spi.R2dbcException;
2626
import io.r2dbc.spi.Result;
27-
import org.reactivestreams.Publisher;
2827
import reactor.core.publisher.Mono;
2928

3029
import org.springframework.beans.factory.InitializingBean;
@@ -38,46 +37,42 @@
3837
import org.springframework.util.Assert;
3938

4039
/**
41-
* {@link org.springframework.transaction.ReactiveTransactionManager}
42-
* implementation for a single R2DBC {@link ConnectionFactory}. This class is
43-
* capable of working in any environment with any R2DBC driver, as long as the
44-
* setup uses a {@code ConnectionFactory} as its {@link Connection} factory
45-
* mechanism. Binds a R2DBC {@code Connection} from the specified
46-
* {@code ConnectionFactory} to the current subscriber context, potentially
47-
* allowing for one context-bound {@code Connection} per {@code ConnectionFactory}.
40+
* {@link org.springframework.transaction.ReactiveTransactionManager} implementation
41+
* for a single R2DBC {@link ConnectionFactory}. This class is capable of working
42+
* in any environment with any R2DBC driver, as long as the setup uses a
43+
* {@code ConnectionFactory} as its {@link Connection} factory mechanism.
44+
* Binds a R2DBC {@code Connection} from the specified {@code ConnectionFactory}
45+
* to the current subscriber context, potentially allowing for one context-bound
46+
* {@code Connection} per {@code ConnectionFactory}.
4847
*
49-
* <p><b>Note: The {@code ConnectionFactory} that this transaction manager
50-
* operates on needs to return independent {@code Connection}s.</b>
51-
* The {@code Connection}s may come from a pool (the typical case), but the
52-
* {@code ConnectionFactory} must not return scoped {@code Connection}s
53-
* or the like. This transaction manager will associate {@code Connection}
54-
* with context-bound transactions itself, according to the specified propagation
55-
* behavior. It assumes that a separate, independent {@code Connection} can
56-
* be obtained even during an ongoing transaction.
48+
* <p><b>Note: The {@code ConnectionFactory} that this transaction manager operates
49+
* on needs to return independent {@code Connection}s.</b> The {@code Connection}s
50+
* typically come from a connection pool but the {@code ConnectionFactory} must not
51+
* return specifically scoped or constrained {@code Connection}s. This transaction
52+
* manager will associate {@code Connection} with context-bound transactions,
53+
* according to the specified propagation behavior. It assumes that a separate,
54+
* independent {@code Connection} can be obtained even during an ongoing transaction.
5755
*
5856
* <p>Application code is required to retrieve the R2DBC Connection via
5957
* {@link ConnectionFactoryUtils#getConnection(ConnectionFactory)}
6058
* instead of a standard R2DBC-style {@link ConnectionFactory#create()} call.
6159
* Spring classes such as {@code DatabaseClient} use this strategy implicitly.
6260
* If not used in combination with this transaction manager, the
63-
* {@link ConnectionFactoryUtils} lookup strategy behaves exactly like the
64-
* native {@code ConnectionFactory} lookup; it can thus be used in a portable fashion.
61+
* {@link ConnectionFactoryUtils} lookup strategy behaves exactly like the native
62+
* {@code ConnectionFactory} lookup; it can thus be used in a portable fashion.
6563
*
66-
* <p>Alternatively, you can allow application code to work with the standard
67-
* R2DBC lookup pattern {@link ConnectionFactory#create()}, for example for code
68-
* that is not aware of Spring at all. In that case, define a
69-
* {@link TransactionAwareConnectionFactoryProxy} for your target {@code ConnectionFactory},
70-
* and pass that proxy {@code ConnectionFactory} to your DAOs, which will automatically
71-
* participate in Spring-managed transactions when accessing it.
64+
* <p>Alternatively, you can allow application code to work with the lookup pattern
65+
* {@link ConnectionFactory#create()}, for example for code not aware of Spring.
66+
* In that case, define a {@link TransactionAwareConnectionFactoryProxy} for your
67+
* target {@code ConnectionFactory}, and pass that proxy {@code ConnectionFactory}
68+
* to your DAOs which will automatically participate in Spring-managed transactions
69+
* when accessing it.
7270
*
73-
* <p>This transaction manager triggers flush callbacks on registered transaction
74-
* synchronizations (if synchronization is generally active), assuming resources
75-
* operating on the underlying R2DBC {@code Connection}.
76-
*
77-
* <p>Spring's {@code TransactionDefinition} attributes are carried forward to R2DBC drivers
78-
* using extensible R2DBC {@link io.r2dbc.spi.TransactionDefinition}. Subclasses may
79-
* override {@link #createTransactionDefinition(TransactionDefinition)} to customize
80-
* transaction definitions for vendor-specific attributes.
71+
* <p>Spring's {@code TransactionDefinition} attributes are carried forward to
72+
* R2DBC drivers using extensible R2DBC {@link io.r2dbc.spi.TransactionDefinition}.
73+
* Subclasses may override {@link #createTransactionDefinition(TransactionDefinition)}
74+
* to customize transaction definitions for vendor-specific attributes. As of 6.0.10,
75+
* this transaction manager supports nested transactions via R2DBC savepoints as well.
8176
*
8277
* @author Mark Paluch
8378
* @author Juergen Hoeller
@@ -97,7 +92,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
9792

9893
/**
9994
* Create a new {@code R2dbcTransactionManager} instance.
100-
* A ConnectionFactory has to be set to be able to use it.
95+
* A {@code ConnectionFactory} has to be set to be able to use it.
10196
* @see #setConnectionFactory
10297
*/
10398
public R2dbcTransactionManager() {}
@@ -114,12 +109,13 @@ public R2dbcTransactionManager(ConnectionFactory connectionFactory) {
114109

115110

116111
/**
117-
* Set the R2DBC {@link ConnectionFactory} that this instance should manage transactions for.
118-
* <p>This will typically be a locally defined {@code ConnectionFactory}, for example a connection pool.
119-
* <p><b>The {@code ConnectionFactory} passed in here needs to return independent {@link Connection}s.</b>
120-
* The {@code Connection}s may come from a pool (the typical case), but the {@code ConnectionFactory}
121-
* must not return scoped {@code Connection}s or the like.
122-
* @see TransactionAwareConnectionFactoryProxy
112+
* Set the R2DBC {@link ConnectionFactory} that this instance should manage transactions
113+
* for. This will typically be a locally defined {@code ConnectionFactory}, for example
114+
* an R2DBC connection pool.
115+
* <p><b>The {@code ConnectionFactory} passed in here needs to return independent
116+
* {@link Connection}s.</b> The {@code Connection}s typically come from a connection
117+
* pool but the {@code ConnectionFactory} must not return specifically scoped or
118+
* constrained {@code Connection}s.
123119
*/
124120
public void setConnectionFactory(@Nullable ConnectionFactory connectionFactory) {
125121
this.connectionFactory = connectionFactory;
@@ -183,8 +179,7 @@ protected Object doGetTransaction(TransactionSynchronizationManager synchronizat
183179

184180
@Override
185181
protected boolean isExistingTransaction(Object transaction) {
186-
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction;
187-
return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive());
182+
return ((ConnectionFactoryTransactionObject) transaction).isTransactionActive();
188183
}
189184

190185
@Override
@@ -193,6 +188,11 @@ protected Mono<Void> doBegin(TransactionSynchronizationManager synchronizationMa
193188

194189
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction;
195190

191+
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED &&
192+
txObject.isTransactionActive()) {
193+
return txObject.createSavepoint();
194+
}
195+
196196
return Mono.defer(() -> {
197197
Mono<Connection> connectionMono;
198198

@@ -210,7 +210,7 @@ protected Mono<Void> doBegin(TransactionSynchronizationManager synchronizationMa
210210
connectionMono = Mono.just(txObject.getConnectionHolder().getConnection());
211211
}
212212

213-
return connectionMono.flatMap(con -> Mono.from(doBegin(definition, con))
213+
return connectionMono.flatMap(con -> doBegin(definition, con)
214214
.then(prepareTransactionalConnection(con, definition))
215215
.doOnSuccess(v -> {
216216
txObject.getConnectionHolder().setTransactionActive(true);
@@ -234,12 +234,12 @@ protected Mono<Void> doBegin(TransactionSynchronizationManager synchronizationMa
234234
}).then();
235235
}
236236

237-
private Publisher<Void> doBegin(TransactionDefinition definition, Connection con) {
237+
private Mono<Void> doBegin(TransactionDefinition definition, Connection con) {
238238
io.r2dbc.spi.TransactionDefinition transactionDefinition = createTransactionDefinition(definition);
239239
if (logger.isDebugEnabled()) {
240240
logger.debug("Starting R2DBC transaction on Connection [" + con + "] using [" + transactionDefinition + "]");
241241
}
242-
return con.beginTransaction(transactionDefinition);
242+
return Mono.from(con.beginTransaction(transactionDefinition));
243243
}
244244

245245
/**
@@ -300,25 +300,23 @@ protected Mono<Void> doCommit(TransactionSynchronizationManager TransactionSynch
300300
GenericReactiveTransaction status) throws TransactionException {
301301

302302
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction();
303-
Connection connection = txObject.getConnectionHolder().getConnection();
304303
if (status.isDebug()) {
305-
logger.debug("Committing R2DBC transaction on Connection [" + connection + "]");
304+
logger.debug("Committing R2DBC transaction on Connection [" +
305+
txObject.getConnectionHolder().getConnection() + "]");
306306
}
307-
return Mono.from(connection.commitTransaction())
308-
.onErrorMap(R2dbcException.class, ex -> translateException("R2DBC commit", ex));
307+
return txObject.commit().onErrorMap(R2dbcException.class, ex -> translateException("R2DBC commit", ex));
309308
}
310309

311310
@Override
312311
protected Mono<Void> doRollback(TransactionSynchronizationManager TransactionSynchronizationManager,
313312
GenericReactiveTransaction status) throws TransactionException {
314313

315314
ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction();
316-
Connection connection = txObject.getConnectionHolder().getConnection();
317315
if (status.isDebug()) {
318-
logger.debug("Rolling back R2DBC transaction on Connection [" + connection + "]");
316+
logger.debug("Rolling back R2DBC transaction on Connection [" +
317+
txObject.getConnectionHolder().getConnection() + "]");
319318
}
320-
return Mono.from(connection.rollbackTransaction())
321-
.onErrorMap(R2dbcException.class, ex -> translateException("R2DBC rollback", ex));
319+
return txObject.rollback().onErrorMap(R2dbcException.class, ex -> translateException("R2DBC rollback", ex));
322320
}
323321

324322
@Override
@@ -496,6 +494,9 @@ private static class ConnectionFactoryTransactionObject {
496494

497495
private boolean newConnectionHolder;
498496

497+
@Nullable
498+
private String savepointName;
499+
499500
void setConnectionHolder(@Nullable ConnectionHolder connectionHolder, boolean newConnectionHolder) {
500501
setConnectionHolder(connectionHolder);
501502
this.newConnectionHolder = newConnectionHolder;
@@ -505,10 +506,6 @@ boolean isNewConnectionHolder() {
505506
return this.newConnectionHolder;
506507
}
507508

508-
void setRollbackOnly() {
509-
getConnectionHolder().setRollbackOnly();
510-
}
511-
512509
public void setConnectionHolder(@Nullable ConnectionHolder connectionHolder) {
513510
this.connectionHolder = connectionHolder;
514511
}
@@ -521,6 +518,34 @@ public ConnectionHolder getConnectionHolder() {
521518
public boolean hasConnectionHolder() {
522519
return (this.connectionHolder != null);
523520
}
521+
522+
public boolean isTransactionActive() {
523+
return (this.connectionHolder != null && this.connectionHolder.isTransactionActive());
524+
}
525+
526+
public Mono<Void> createSavepoint() {
527+
ConnectionHolder holder = getConnectionHolder();
528+
this.savepointName = holder.nextSavepoint();
529+
return Mono.from(holder.getConnection().createSavepoint(this.savepointName));
530+
}
531+
532+
public Mono<Void> commit() {
533+
Connection connection = getConnectionHolder().getConnection();
534+
return (this.savepointName != null ?
535+
Mono.from(connection.releaseSavepoint(this.savepointName)) :
536+
Mono.from(connection.commitTransaction()));
537+
}
538+
539+
public Mono<Void> rollback() {
540+
Connection connection = getConnectionHolder().getConnection();
541+
return (this.savepointName != null ?
542+
Mono.from(connection.rollbackTransactionToSavepoint(this.savepointName)) :
543+
Mono.from(connection.rollbackTransaction()));
544+
}
545+
546+
public void setRollbackOnly() {
547+
getConnectionHolder().setRollbackOnly();
548+
}
524549
}
525550

526551
}

0 commit comments

Comments
 (0)