#329 - Translate driver exceptions in R2dbcTransactionManager.
This commit is contained in:
@@ -18,13 +18,17 @@ package org.springframework.data.r2dbc.connectionfactory;
|
||||
import io.r2dbc.spi.Connection;
|
||||
import io.r2dbc.spi.ConnectionFactory;
|
||||
import io.r2dbc.spi.IsolationLevel;
|
||||
import io.r2dbc.spi.R2dbcException;
|
||||
import io.r2dbc.spi.Result;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.r2dbc.core.DatabaseClient;
|
||||
import org.springframework.data.r2dbc.support.R2dbcExceptionSubclassTranslator;
|
||||
import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.transaction.CannotCreateTransactionException;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
@@ -76,6 +80,8 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
|
||||
|
||||
private boolean enforceReadOnly = false;
|
||||
|
||||
private R2dbcExceptionTranslator exceptionTranslator = new R2dbcExceptionSubclassTranslator();
|
||||
|
||||
/**
|
||||
* Create a new @link ConnectionFactoryTransactionManager} instance. A ConnectionFactory has to be set to be able to
|
||||
* use it.
|
||||
@@ -124,6 +130,19 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
|
||||
return this.connectionFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the exception translator for this instance.
|
||||
* <p>
|
||||
* If no custom translator is provided, a default {@link R2dbcExceptionSubclassTranslator} is used which translates
|
||||
* {@link R2dbcException}'s subclasses into Springs {@link DataAccessException} hierarchy.
|
||||
*
|
||||
* @see R2dbcExceptionSubclassTranslator
|
||||
* @since 1.1
|
||||
*/
|
||||
public void setExceptionTranslator(R2dbcExceptionTranslator exceptionTranslator) {
|
||||
this.exceptionTranslator = exceptionTranslator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the {@link ConnectionFactory} for actual use.
|
||||
*
|
||||
@@ -242,17 +261,22 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
|
||||
}
|
||||
}).thenReturn(con).onErrorResume(e -> {
|
||||
|
||||
CannotCreateTransactionException ex = new CannotCreateTransactionException(
|
||||
"Could not open R2DBC Connection for transaction", e);
|
||||
|
||||
if (txObject.isNewConnectionHolder()) {
|
||||
return ConnectionFactoryUtils.releaseConnection(con, obtainConnectionFactory()).doOnTerminate(() -> {
|
||||
|
||||
txObject.setConnectionHolder(null, false);
|
||||
}).then(Mono.error(ex));
|
||||
}).then(Mono.error(e));
|
||||
}
|
||||
return Mono.error(ex);
|
||||
return Mono.error(e);
|
||||
});
|
||||
}).onErrorResume(e -> {
|
||||
|
||||
CannotCreateTransactionException ex = new CannotCreateTransactionException(
|
||||
"Could not open R2DBC Connection for transaction",
|
||||
e instanceof R2dbcException ? potentiallyTranslateException("Open R2DBC Connection", (R2dbcException) e)
|
||||
: e);
|
||||
|
||||
return Mono.error(ex);
|
||||
});
|
||||
}).then();
|
||||
}
|
||||
@@ -321,7 +345,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
|
||||
}
|
||||
|
||||
return Mono.from(connection.commitTransaction())
|
||||
.onErrorMap(ex -> new TransactionSystemException("Could not commit R2DBC transaction", ex));
|
||||
.onErrorMap(R2dbcException.class, ex -> translateException("R2DBC commit", ex));
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -339,7 +363,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
|
||||
}
|
||||
|
||||
return Mono.from(connection.rollbackTransaction())
|
||||
.onErrorMap(ex -> new TransactionSystemException("Could not roll back R2DBC transaction", ex));
|
||||
.onErrorMap(R2dbcException.class, ex -> translateException("R2DBC rollback", ex));
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -496,6 +520,31 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the given R2DBC commit/rollback exception to a common Spring exception to propagate from the
|
||||
* {@link #commit}/{@link #rollback} call.
|
||||
* <p>
|
||||
* The default implementation throws a {@link TransactionSystemException}. Subclasses may specifically identify
|
||||
* concurrency failures etc.
|
||||
*
|
||||
* @param task the task description (commit or rollback).
|
||||
* @param ex the SQLException thrown from commit/rollback.
|
||||
* @return the translated exception to throw, either a {@link org.springframework.dao.DataAccessException} or a
|
||||
* {@link org.springframework.transaction.TransactionException}
|
||||
* @since 1.1
|
||||
*/
|
||||
protected RuntimeException translateException(String task, R2dbcException ex) {
|
||||
|
||||
Exception translated = potentiallyTranslateException(task, ex);
|
||||
return new TransactionSystemException(task + " failed", translated);
|
||||
}
|
||||
|
||||
private Exception potentiallyTranslateException(String task, R2dbcException ex) {
|
||||
|
||||
DataAccessException translated = exceptionTranslator.translate(task, null, ex);
|
||||
return translated != null ? translated : ex;
|
||||
}
|
||||
|
||||
/**
|
||||
* ConnectionFactory transaction object, representing a ConnectionHolder. Used as transaction object by
|
||||
* ConnectionFactoryTransactionManager.
|
||||
|
||||
@@ -23,6 +23,7 @@ import static org.mockito.Mockito.*;
|
||||
import io.r2dbc.spi.Connection;
|
||||
import io.r2dbc.spi.ConnectionFactory;
|
||||
import io.r2dbc.spi.IsolationLevel;
|
||||
import io.r2dbc.spi.R2dbcBadGrammarException;
|
||||
import io.r2dbc.spi.Statement;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
@@ -32,6 +33,8 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.data.r2dbc.BadSqlGrammarException;
|
||||
import org.springframework.transaction.CannotCreateTransactionException;
|
||||
import org.springframework.transaction.IllegalTransactionStateException;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.reactive.TransactionSynchronization;
|
||||
@@ -94,6 +97,29 @@ public class R2dbcTransactionManagerUnitTests {
|
||||
assertThat(sync.afterCompletionCalled).isTrue();
|
||||
}
|
||||
|
||||
@Test // gh-329
|
||||
public void testBeginFails() {
|
||||
|
||||
reset(connectionFactoryMock);
|
||||
when(connectionFactoryMock.create()).thenReturn(Mono.error(new R2dbcBadGrammarException("fail")));
|
||||
|
||||
when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty());
|
||||
|
||||
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
|
||||
definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
|
||||
|
||||
TransactionalOperator operator = TransactionalOperator.create(tm, definition);
|
||||
|
||||
ConnectionFactoryUtils.getConnection(connectionFactoryMock).as(operator::transactional) //
|
||||
.as(StepVerifier::create) //
|
||||
.expectErrorSatisfies(actual -> {
|
||||
|
||||
assertThat(actual).isInstanceOf(CannotCreateTransactionException.class)
|
||||
.hasCauseInstanceOf(BadSqlGrammarException.class);
|
||||
|
||||
}).verify();
|
||||
}
|
||||
|
||||
@Test // gh-107
|
||||
public void appliesIsolationLevel() {
|
||||
|
||||
@@ -214,9 +240,11 @@ public class R2dbcTransactionManagerUnitTests {
|
||||
public void testCommitFails() {
|
||||
|
||||
when(connectionMock.commitTransaction()).thenReturn(Mono.defer(() -> {
|
||||
return Mono.error(new IllegalStateException("Commit should fail"));
|
||||
return Mono.error(new R2dbcBadGrammarException("Commit should fail"));
|
||||
}));
|
||||
|
||||
when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty());
|
||||
|
||||
TransactionalOperator operator = TransactionalOperator.create(tm);
|
||||
|
||||
ConnectionFactoryUtils.getConnection(connectionFactoryMock) //
|
||||
@@ -225,7 +253,7 @@ public class R2dbcTransactionManagerUnitTests {
|
||||
}).then() //
|
||||
.as(operator::transactional) //
|
||||
.as(StepVerifier::create) //
|
||||
.verifyError();
|
||||
.verifyError(IllegalTransactionStateException.class);
|
||||
|
||||
verify(connectionMock).isAutoCommit();
|
||||
verify(connectionMock).beginTransaction();
|
||||
@@ -263,6 +291,35 @@ public class R2dbcTransactionManagerUnitTests {
|
||||
verifyNoMoreInteractions(connectionMock);
|
||||
}
|
||||
|
||||
@Test // gh-329
|
||||
public void testRollbackFails() {
|
||||
|
||||
when(connectionMock.rollbackTransaction()).thenReturn(Mono.defer(() -> {
|
||||
return Mono.error(new R2dbcBadGrammarException("Commit should fail"));
|
||||
}), Mono.empty());
|
||||
|
||||
TransactionalOperator operator = TransactionalOperator.create(tm);
|
||||
|
||||
operator.execute(reactiveTransaction -> {
|
||||
|
||||
reactiveTransaction.setRollbackOnly();
|
||||
|
||||
return ConnectionFactoryUtils.getConnection(connectionFactoryMock) //
|
||||
.doOnNext(it -> {
|
||||
it.createStatement("foo");
|
||||
}).then();
|
||||
}).as(StepVerifier::create) //
|
||||
.verifyError(IllegalTransactionStateException.class);
|
||||
|
||||
verify(connectionMock).isAutoCommit();
|
||||
verify(connectionMock).beginTransaction();
|
||||
verify(connectionMock).createStatement("foo");
|
||||
verify(connectionMock, never()).commitTransaction();
|
||||
verify(connectionMock).rollbackTransaction();
|
||||
verify(connectionMock).close();
|
||||
verifyNoMoreInteractions(connectionMock);
|
||||
}
|
||||
|
||||
@Test // gh-107
|
||||
public void testTransactionSetRollbackOnly() {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user