diff --git a/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java b/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java new file mode 100644 index 0000000..f1dd079 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java @@ -0,0 +1,63 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc; + +import io.r2dbc.spi.R2dbcException; + +import org.springframework.dao.InvalidDataAccessResourceUsageException; + +/** + * Exception thrown when SQL specified is invalid. Such exceptions always have a {@link io.r2dbc.spi.R2dbcException} + * root cause. + *
+ * It would be possible to have subclasses for no such table, no such column etc. A custom + * {@link org.springframework.data.r2dbc.support.R2dbcExceptionTranslator} could create such more specific exceptions, + * without affecting code using this class. + * + * @author Mark Paluch + */ +public class BadSqlGrammarException extends InvalidDataAccessResourceUsageException { + + private final String sql; + + /** + * Creates a new {@link BadSqlGrammarException}. + * + * @param task name of current task. + * @param sql the offending SQL statement. + * @param ex the root cause. + */ + public BadSqlGrammarException(String task, String sql, R2dbcException ex) { + + super(task + "; bad SQL grammar [" + sql + "]", ex); + + this.sql = sql; + } + + /** + * Return the wrapped {@link R2dbcException}. + */ + public R2dbcException getR2dbcException() { + return (R2dbcException) getCause(); + } + + /** + * Return the SQL that caused the problem. + */ + public String getSql() { + return this.sql; + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java b/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java new file mode 100644 index 0000000..3dd808a --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java @@ -0,0 +1,80 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc; + +import io.r2dbc.spi.R2dbcException; + +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.lang.Nullable; + +/** + * Exception thrown when a {@link io.r2dbc.spi.Result} has been accessed in an invalid fashion. Such exceptions always + * have a {@link io.r2dbc.spi.R2dbcException} root cause. + *
+ * This typically happens when an invalid {@link Result} column index or name has been specified.
+ *
+ * @author Mark Paluch
+ * @see BadSqlGrammarException
+ */
+@SuppressWarnings("serial")
+public class InvalidResultAccessException extends InvalidDataAccessResourceUsageException {
+
+ private final @Nullable String sql;
+
+ /**
+ * Creates a new {@link InvalidResultAccessException}.
+ *
+ * @param task name of current task.
+ * @param sql the offending SQL statement.
+ * @param ex the root cause.
+ */
+ public InvalidResultAccessException(String task, String sql, R2dbcException ex) {
+
+ super(task + "; invalid Result access for SQL [" + sql + "]", ex);
+
+ this.sql = sql;
+ }
+
+ /**
+ * Creates a new {@link InvalidResultAccessException}.
+ *
+ * @param ex the root cause.
+ */
+ public InvalidResultAccessException(R2dbcException ex) {
+
+ super(ex.getMessage(), ex);
+
+ this.sql = null;
+ }
+
+ /**
+ * Return the wrapped {@link R2dbcException}.
+ */
+ public R2dbcException getR2dbcException() {
+ return (R2dbcException) getCause();
+ }
+
+ /**
+ * Return the SQL that caused the problem.
+ *
+ * @return the offending SQL, if known.
+ */
+ @Nullable
+ public String getSql() {
+ return this.sql;
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java b/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java
new file mode 100644
index 0000000..da20c40
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc;
+
+import io.r2dbc.spi.R2dbcException;
+
+import org.springframework.dao.UncategorizedDataAccessException;
+import org.springframework.lang.Nullable;
+
+/**
+ * Exception thrown when we can't classify a {@link R2dbcException} into one of our generic data access exceptions.
+ *
+ * @author Mark Paluch
+ */
+public class UncategorizedR2dbcException extends UncategorizedDataAccessException {
+
+ /**
+ * SQL that led to the problem
+ */
+ private final @Nullable String sql;
+
+ /**
+ * Creates a new {@link UncategorizedR2dbcException}.
+ *
+ * @param task name of current task
+ * @param sql the offending SQL statement
+ * @param ex the root cause
+ */
+ public UncategorizedR2dbcException(String task, @Nullable String sql, R2dbcException ex) {
+
+ super(String.format("%s; uncategorized R2dbcException%s; %s", task, sql != null ? " for SQL [" + sql + "]" : "",
+ ex.getMessage()), ex);
+ this.sql = sql;
+ }
+
+ /**
+ * Returns the original {@link R2dbcException}.
+ *
+ * @return the original {@link R2dbcException}.
+ */
+ public R2dbcException getR2dbcException() {
+ return (R2dbcException) getCause();
+ }
+
+ /**
+ * Return the SQL that led to the problem (if known).
+ */
+ @Nullable
+ public String getSql() {
+ return this.sql;
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java
index 986200c..3116117 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java
@@ -28,7 +28,7 @@ import java.util.function.Supplier;
import org.reactivestreams.Publisher;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
-import org.springframework.jdbc.support.SQLExceptionTranslator;
+import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
/**
* A non-blocking, reactive client for performing database calls requests with Reactive Streams back pressure. Provides
@@ -91,12 +91,12 @@ public interface DatabaseClient {
Builder connectionFactory(ConnectionFactory factory);
/**
- * Configures a {@link SQLExceptionTranslator}.
+ * Configures a {@link R2dbcExceptionTranslator}.
*
* @param exceptionTranslator must not be {@literal null}.
* @return {@code this} {@link Builder}.
*/
- Builder exceptionTranslator(SQLExceptionTranslator exceptionTranslator);
+ Builder exceptionTranslator(R2dbcExceptionTranslator exceptionTranslator);
/**
* Configures a {@link ReactiveDataAccessStrategy}.
diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
index 1b4378d..5c5adc4 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
@@ -17,6 +17,7 @@ package org.springframework.data.r2dbc.function;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
+import io.r2dbc.spi.R2dbcException;
import io.r2dbc.spi.Result;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
@@ -29,7 +30,6 @@ import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
-import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -48,16 +48,16 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import org.springframework.dao.DataAccessException;
-import org.springframework.dao.UncategorizedDataAccessException;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.NullHandling;
import org.springframework.data.domain.Sort.Order;
+import org.springframework.data.r2dbc.UncategorizedR2dbcException;
import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy;
import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper;
+import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
import org.springframework.data.util.Pair;
import org.springframework.jdbc.core.SqlProvider;
-import org.springframework.jdbc.support.SQLExceptionTranslator;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -74,13 +74,13 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
private final ConnectionFactory connector;
- private final SQLExceptionTranslator exceptionTranslator;
+ private final R2dbcExceptionTranslator exceptionTranslator;
private final ReactiveDataAccessStrategy dataAccessStrategy;
private final DefaultDatabaseClientBuilder builder;
- DefaultDatabaseClient(ConnectionFactory connector, SQLExceptionTranslator exceptionTranslator,
+ DefaultDatabaseClient(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator,
ReactiveDataAccessStrategy dataAccessStrategy, DefaultDatabaseClientBuilder builder) {
this.connector = connector;
@@ -133,7 +133,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
return doInConnection(connectionToUse, action);
}, this::closeConnection, this::closeConnection, this::closeConnection) //
- .onErrorMap(SQLException.class, ex -> translateException("execute", getSql(action), ex));
+ .onErrorMap(R2dbcException.class, ex -> translateException("execute", getSql(action), ex));
}
/**
@@ -160,7 +160,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
return doInConnectionMany(connectionToUse, action);
}, this::closeConnection, this::closeConnection, this::closeConnection) //
- .onErrorMap(SQLException.class, ex -> translateException("executeMany", getSql(action), ex));
+ .onErrorMap(R2dbcException.class, ex -> translateException("executeMany", getSql(action), ex));
}
/**
@@ -185,7 +185,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
/**
* Obtain the {@link ConnectionFactory} for actual use.
*
- * @return the ConnectionFactory (never {@code null})
+ * @return the ConnectionFactory (never {@literal null})
* @throws IllegalStateException in case of no DataSource set
*/
protected ConnectionFactory obtainConnectionFactory() {
@@ -204,17 +204,17 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
}
/**
- * Translate the given {@link SQLException} into a generic {@link DataAccessException}.
+ * Translate the given {@link R2dbcException} into a generic {@link DataAccessException}.
*
- * @param task readable text describing the task being attempted
- * @param sql SQL query or update that caused the problem (may be {@code null})
- * @param ex the offending {@code SQLException}
- * @return a DataAccessException wrapping the {@code SQLException} (never {@code null})
+ * @param task readable text describing the task being attempted.
+ * @param sql SQL query or update that caused the problem (may be {@literal null}).
+ * @param ex the offending {@link R2dbcException}.
+ * @return a DataAccessException wrapping the {@link R2dbcException} (never {@literal null}).
*/
- protected DataAccessException translateException(String task, @Nullable String sql, SQLException ex) {
+ protected DataAccessException translateException(String task, @Nullable String sql, R2dbcException ex) {
DataAccessException dae = exceptionTranslator.translate(task, sql, ex);
- return (dae != null ? dae : new UncategorizedSQLException(task, sql, ex));
+ return (dae != null ? dae : new UncategorizedR2dbcException(task, sql, ex));
}
private static void doBind(Statement statement, Map
+ * The passed-in arguments will have been pre-checked. Furthermore, this method is allowed to return {@literal null}
+ * to indicate that no exception match has been found and that fallback translation should kick in.
+ *
+ * @param task readable text describing the task being attempted.
+ * @param sql SQL query or update that caused the problem (if known).
+ * @param ex the offending {@link R2dbcException}.
+ * @return the DataAccessException, wrapping the {@link R2dbcException}; or {@literal null} if no exception match
+ * found.
+ */
+ @Nullable
+ protected abstract DataAccessException doTranslate(String task, @Nullable String sql, R2dbcException ex);
+
+ /**
+ * Build a message {@code String} for the given {@link java.sql.R2dbcException}.
+ *
+ * To be called by translator subclasses when creating an instance of a generic
+ * {@link org.springframework.dao.DataAccessException} class.
+ *
+ * @param task readable text describing the task being attempted.
+ * @param sql the SQL statement that caused the problem.
+ * @param ex the offending {@link R2dbcException}.
+ * @return the message {@code String} to use.
+ */
+ protected String buildMessage(String task, @Nullable String sql, R2dbcException ex) {
+ return task + "; " + (sql != null ? "SQL [" + sql : "]; " + "") + ex.getMessage();
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionTranslator.java
new file mode 100644
index 0000000..5bbcd4a
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionTranslator.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc.support;
+
+import io.r2dbc.spi.R2dbcException;
+
+import org.springframework.dao.DataAccessException;
+import org.springframework.lang.Nullable;
+
+/**
+ * Strategy interface for translating between {@link io.r2dbc.spi.R2dbcException R2dbcExceptions} and Spring's data
+ * access strategy-agnostic {@link DataAccessException} hierarchy.
+ *
+ * Implementations can be generic (for example, using {@link io.r2dbc.spi.R2dbcException#getSqlState() SQLState} codes
+ * for R2DBC) or wholly proprietary (for example, using Oracle error codes) for greater precision.
+ *
+ * @author Mark Paluch
+ * @see org.springframework.dao.DataAccessException
+ * @see SqlStateR2dbcExceptionTranslator
+ * @see SqlErrorCodeR2dbcExceptionTranslator
+ */
+@FunctionalInterface
+public interface R2dbcExceptionTranslator {
+
+ /**
+ * Translate the given {@link R2dbcException} into a generic {@link DataAccessException}.
+ *
+ * The returned DataAccessException is supposed to contain the original {@link R2dbcException} as root cause. However,
+ * client code may not generally rely on this due to DataAccessExceptions possibly being caused by other resource APIs
+ * as well. That said, a {@code getRootCause() instanceof R2dbcException} check (and subsequent cast) is considered
+ * reliable when expecting R2DBC-based access to have happened.
+ *
+ * @param task readable text describing the task being attempted.
+ * @param sql SQL query or update that caused the problem (if known).
+ * @param ex the offending {@link R2dbcException}.
+ * @return the DataAccessException wrapping the {@code R2dbcException}, or {@literal null} if no translation could be
+ * applied (in a custom translator; the default translators always throw an
+ * {@link org.springframework.data.r2dbc.UncategorizedR2dbcException} in such a case).
+ * @see org.springframework.dao.DataAccessException#getRootCause()
+ */
+ @Nullable
+ DataAccessException translate(String task, @Nullable String sql, R2dbcException ex);
+
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java
new file mode 100644
index 0000000..87046a1
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc.support;
+
+import io.r2dbc.spi.ConnectionFactory;
+import io.r2dbc.spi.R2dbcException;
+
+import java.sql.SQLException;
+import java.util.Arrays;
+
+import org.springframework.dao.CannotAcquireLockException;
+import org.springframework.dao.CannotSerializeTransactionException;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.DataAccessResourceFailureException;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.dao.DeadlockLoserDataAccessException;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.dao.PermissionDeniedDataAccessException;
+import org.springframework.dao.TransientDataAccessResourceException;
+import org.springframework.data.r2dbc.BadSqlGrammarException;
+import org.springframework.data.r2dbc.InvalidResultAccessException;
+import org.springframework.jdbc.support.SQLErrorCodes;
+import org.springframework.jdbc.support.SQLErrorCodesFactory;
+import org.springframework.jdbc.support.SQLExceptionTranslator;
+import org.springframework.lang.Nullable;
+
+/**
+ * Implementation of {@link R2dbcExceptionTranslator} that analyzes vendor-specific error codes. More precise than an
+ * implementation based on SQL state, but heavily vendor-specific.
+ *
+ * This class applies the following matching rules:
+ *
+ * The configuration file named "sql-error-codes.xml" is by default read from the
+ * {@code org.springframework.jdbc.support} package. It can be overridden through a file of the same name in the root of
+ * the class path (e.g. in the "/WEB-INF/classes" directory), as long as the Spring JDBC package is loaded from the same
+ * ClassLoader.
+ *
+ * @author Mark Paluch
+ * @see SQLErrorCodesFactory
+ * @see SqlStateR2dbcExceptionTranslator
+ */
+public class SqlErrorCodeR2dbcExceptionTranslator extends AbstractFallbackR2dbcExceptionTranslator {
+
+ /** Error codes used by this translator */
+ @Nullable private SQLErrorCodes sqlErrorCodes;
+
+ /**
+ * Creates a new {@link SqlErrorCodeR2dbcExceptionTranslator}. The {@link SQLErrorCodes} or
+ * {@link io.r2dbc.spi.ConnectionFactory} property must be set.
+ */
+ public SqlErrorCodeR2dbcExceptionTranslator() {}
+
+ /**
+ * Create a SQL error code translator for the given DataSource. Invoking this constructor will cause a Connection to
+ * be obtained from the DataSource to get the meta-data.
+ *
+ * @param connectionFactory {@link ConnectionFactory} to use to find meta-data and establish which error codes are
+ * usable.
+ * @see SQLErrorCodesFactory
+ */
+ public SqlErrorCodeR2dbcExceptionTranslator(ConnectionFactory connectionFactory) {
+ this();
+ setConnectionFactory(connectionFactory);
+ }
+
+ /**
+ * Create a SQL error code translator for the given database product name. Invoking this constructor will avoid
+ * obtaining a Connection from the DataSource to get the meta-data.
+ *
+ * @param dbName the database product name that identifies the error codes entry
+ * @see SQLErrorCodesFactory
+ * @see java.sql.DatabaseMetaData#getDatabaseProductName()
+ */
+ public SqlErrorCodeR2dbcExceptionTranslator(String dbName) {
+ this();
+ setDatabaseProductName(dbName);
+ }
+
+ /**
+ * Create a SQLErrorCode translator given these error codes. Does not require a database meta-data lookup to be
+ * performed using a connection.
+ *
+ * @param sec error codes
+ */
+ public SqlErrorCodeR2dbcExceptionTranslator(SQLErrorCodes sec) {
+ this();
+ this.sqlErrorCodes = sec;
+ }
+
+ /**
+ * Set the DataSource for this translator.
+ *
+ * Setting this property will cause a Connection to be obtained from the DataSource to get the meta-data.
+ *
+ * @param connectionFactory {@link ConnectionFactory} to use to find meta-data and establish which error codes are
+ * usable.
+ * @see SQLErrorCodesFactory#getErrorCodes(String)
+ * @see io.r2dbc.spi.ConnectionFactoryMetadata#getName()
+ */
+ public void setConnectionFactory(ConnectionFactory connectionFactory) {
+ this.sqlErrorCodes = SQLErrorCodesFactory.getInstance().getErrorCodes(connectionFactory.getMetadata().getName());
+ }
+
+ /**
+ * Set the database product name for this translator.
+ *
+ * Setting this property will avoid obtaining a Connection from the DataSource to get the meta-data.
+ *
+ * @param dbName the database product name that identifies the error codes entry.
+ * @see SQLErrorCodesFactory#getErrorCodes(String)
+ * @see io.r2dbc.spi.ConnectionFactoryMetadata#getName()
+ */
+ public void setDatabaseProductName(String dbName) {
+ this.sqlErrorCodes = SQLErrorCodesFactory.getInstance().getErrorCodes(dbName);
+ }
+
+ /**
+ * Set custom error codes to be used for translation.
+ *
+ * @param sec custom error codes to use.
+ */
+ public void setSqlErrorCodes(@Nullable SQLErrorCodes sec) {
+ this.sqlErrorCodes = sec;
+ }
+
+ /**
+ * Return the error codes used by this translator. Usually determined via a DataSource.
+ *
+ * @see #setConnectionFactory
+ */
+ @Nullable
+ public SQLErrorCodes getSqlErrorCodes() {
+ return this.sqlErrorCodes;
+ }
+
+ @Override
+ @Nullable
+ protected DataAccessException doTranslate(String task, @Nullable String sql, R2dbcException ex) {
+
+ R2dbcException translated = ex;
+
+ // First, try custom translation from overridden method.
+ DataAccessException dex = customTranslate(task, sql, translated);
+ if (dex != null) {
+ return dex;
+ }
+
+ // Next, try the custom SQLExceptionTranslator, if available.
+ if (this.sqlErrorCodes != null) {
+ SQLExceptionTranslator customTranslator = this.sqlErrorCodes.getCustomSqlExceptionTranslator();
+ if (customTranslator != null) {
+ DataAccessException customDex = customTranslator.translate(task, sql,
+ new SQLException(ex.getMessage(), ex.getSqlState(), ex));
+ if (customDex != null) {
+ return customDex;
+ }
+ }
+ }
+
+ // Check SQLErrorCodes with corresponding error code, if available.
+ if (this.sqlErrorCodes != null) {
+ String errorCode;
+ if (this.sqlErrorCodes.isUseSqlStateForTranslation()) {
+ errorCode = translated.getSqlState();
+ } else {
+ // Try to find R2dbcException with actual error code, looping through the causes.
+ R2dbcException current = translated;
+ while (current.getErrorCode() == 0 && current.getCause() instanceof R2dbcException) {
+ current = (R2dbcException) current.getCause();
+ }
+ errorCode = Integer.toString(current.getErrorCode());
+ }
+
+ if (errorCode != null) {
+ // Look for grouped error codes.
+ if (Arrays.binarySearch(this.sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0) {
+ logTranslation(task, sql, translated);
+ return new BadSqlGrammarException(task, (sql != null ? sql : ""), translated);
+ } else if (Arrays.binarySearch(this.sqlErrorCodes.getInvalidResultSetAccessCodes(), errorCode) >= 0) {
+ logTranslation(task, sql, translated);
+ return new InvalidResultAccessException(task, (sql != null ? sql : ""), translated);
+ } else if (Arrays.binarySearch(this.sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) {
+ logTranslation(task, sql, translated);
+ return new DuplicateKeyException(buildMessage(task, sql, translated), translated);
+ } else if (Arrays.binarySearch(this.sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode) >= 0) {
+ logTranslation(task, sql, translated);
+ return new DataIntegrityViolationException(buildMessage(task, sql, translated), translated);
+ } else if (Arrays.binarySearch(this.sqlErrorCodes.getPermissionDeniedCodes(), errorCode) >= 0) {
+ logTranslation(task, sql, translated);
+ return new PermissionDeniedDataAccessException(buildMessage(task, sql, translated), translated);
+ } else if (Arrays.binarySearch(this.sqlErrorCodes.getDataAccessResourceFailureCodes(), errorCode) >= 0) {
+ logTranslation(task, sql, translated);
+ return new DataAccessResourceFailureException(buildMessage(task, sql, translated), translated);
+ } else if (Arrays.binarySearch(this.sqlErrorCodes.getTransientDataAccessResourceCodes(), errorCode) >= 0) {
+ logTranslation(task, sql, translated);
+ return new TransientDataAccessResourceException(buildMessage(task, sql, translated), translated);
+ } else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotAcquireLockCodes(), errorCode) >= 0) {
+ logTranslation(task, sql, translated);
+ return new CannotAcquireLockException(buildMessage(task, sql, translated), translated);
+ } else if (Arrays.binarySearch(this.sqlErrorCodes.getDeadlockLoserCodes(), errorCode) >= 0) {
+ logTranslation(task, sql, translated);
+ return new DeadlockLoserDataAccessException(buildMessage(task, sql, translated), translated);
+ } else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotSerializeTransactionCodes(), errorCode) >= 0) {
+ logTranslation(task, sql, translated);
+ return new CannotSerializeTransactionException(buildMessage(task, sql, translated), translated);
+ }
+ }
+ }
+
+ // We couldn't identify it more precisely - let's hand it over to the SQLState fallback translator.
+ if (logger.isDebugEnabled()) {
+ String codes;
+ if (this.sqlErrorCodes != null && this.sqlErrorCodes.isUseSqlStateForTranslation()) {
+ codes = "SQL state '" + translated.getSqlState() + "', error code '" + translated.getErrorCode();
+ } else {
+ codes = "Error code '" + translated.getErrorCode() + "'";
+ }
+ logger.debug("Unable to translate R2dbcException with " + codes + ", will now try the fallback translator");
+ }
+
+ return null;
+ }
+
+ /**
+ * Subclasses can override this method to attempt a custom mapping from {@link R2dbcException} to
+ * {@link DataAccessException}.
+ *
+ * @param task readable text describing the task being attempted
+ * @param sql SQL query or update that caused the problem. May be {@literal null}.
+ * @param ex the offending {@link R2dbcException}.
+ * @return null if no custom translation was possible, otherwise a {@link DataAccessException} resulting from custom
+ * translation. This exception should include the {@link R2dbcException} parameter as a nested root cause.
+ * This implementation always returns null, meaning that the translator always falls back to the default error
+ * codes.
+ */
+ @Nullable
+ protected DataAccessException customTranslate(String task, @Nullable String sql, R2dbcException ex) {
+ return null;
+ }
+
+ private void logTranslation(String task, @Nullable String sql, R2dbcException exception) {
+
+ if (logger.isDebugEnabled()) {
+
+ String intro = "Translating";
+ logger.debug(intro + " R2dbcException with SQL state '" + exception.getSqlState() + "', error code '"
+ + exception.getErrorCode() + "', message [" + exception.getMessage() + "]"
+ + (sql != null ? "; SQL was [" + sql + "]" : "") + " for task [" + task + "]");
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java
new file mode 100644
index 0000000..97eb4e6
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc.support;
+
+import io.r2dbc.spi.R2dbcException;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.springframework.dao.ConcurrencyFailureException;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.DataAccessResourceFailureException;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.dao.TransientDataAccessResourceException;
+import org.springframework.data.r2dbc.BadSqlGrammarException;
+import org.springframework.lang.Nullable;
+
+/**
+ * {@link R2dbcExceptionTranslator} implementation that analyzes the SQL state in the {@link R2dbcException} based on
+ * the first two digits (the SQL state "class"). Detects standard SQL state values and well-known vendor-specific SQL
+ * states.
+ *
+ * Not able to diagnose all problems, but is portable between databases and does not require special initialization (no
+ * database vendor detection, etc.). For more precise translation, consider
+ * {@link SqlErrorCodeR2dbcExceptionTranslator}.
+ *
+ * @author Mark Paluch
+ * @see io.r2dbc.spi.R2dbcException#getSqlState()
+ * @see SqlErrorCodeR2dbcExceptionTranslator
+ */
+public class SqlStateR2dbcExceptionTranslator extends AbstractFallbackR2dbcExceptionTranslator {
+
+ private static final Set
+ * Some R2DBC drivers nest the actual exception from a batched update, so we might need to dig down into the nested
+ * exception.
+ *
+ * @param ex the exception from which the {@link R2dbcException#getSqlState() SQL state} is to be extracted.
+ * @return the SQL state code.
+ */
+ @Nullable
+ private String getSqlState(R2dbcException ex) {
+
+ String sqlState = ex.getSqlState();
+
+ if (sqlState == null) {
+
+ for (Throwable throwable : ex.getSuppressed()) {
+
+ if (!(throwable instanceof R2dbcException)) {
+ continue;
+ }
+
+ R2dbcException r2dbcException = (R2dbcException) throwable;
+ if (r2dbcException.getSqlState() != null) {
+ sqlState = r2dbcException.getSqlState();
+ break;
+ }
+ }
+ }
+
+ return sqlState;
+ }
+}
diff --git a/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java
index 3f06dfd..2ce5154 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java
@@ -25,6 +25,7 @@ import reactor.test.StepVerifier;
import org.junit.Before;
import org.junit.Test;
+import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jdbc.core.mapping.Table;
@@ -75,6 +76,27 @@ public class DatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport
assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055);
}
+ @Test
+ public void shouldTranslateDuplicateKeyException() {
+
+ DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
+
+ executeInsert();
+
+ databaseClient.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") //
+ .bind(0, 42055) //
+ .bind(1, "SCHAUFELRADBAGGER") //
+ .bindNull("$3") //
+ .fetch().rowsUpdated() //
+ .as(StepVerifier::create) //
+ .expectErrorSatisfies(exception -> {
+
+ assertThat(exception).isInstanceOf(DuplicateKeyException.class)
+ .hasMessageContaining("execute; SQL [INSERT INTO legoset");
+ }) //
+ .verify();
+ }
+
@Test
public void executeSelect() {
diff --git a/src/test/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java b/src/test/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java
new file mode 100644
index 0000000..3f3fb13
--- /dev/null
+++ b/src/test/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc.support;
+
+import static org.assertj.core.api.Assertions.*;
+
+import io.r2dbc.spi.R2dbcException;
+
+import org.junit.Test;
+import org.springframework.dao.CannotAcquireLockException;
+import org.springframework.dao.CannotSerializeTransactionException;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.DataAccessResourceFailureException;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.dao.DeadlockLoserDataAccessException;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.data.r2dbc.BadSqlGrammarException;
+import org.springframework.data.r2dbc.InvalidResultAccessException;
+import org.springframework.data.r2dbc.UncategorizedR2dbcException;
+import org.springframework.jdbc.support.SQLErrorCodes;
+
+/**
+ * Unit tests for {@link SqlErrorCodeR2dbcExceptionTranslator}.
+ *
+ * @author Mark Paluch
+ */
+public class SqlErrorCodeR2dbcExceptionTranslatorUnitTests {
+
+ private static SQLErrorCodes ERROR_CODES = new SQLErrorCodes();
+ static {
+ ERROR_CODES.setBadSqlGrammarCodes("1", "2");
+ ERROR_CODES.setInvalidResultSetAccessCodes("3", "4");
+ ERROR_CODES.setDuplicateKeyCodes("10");
+ ERROR_CODES.setDataAccessResourceFailureCodes("5");
+ ERROR_CODES.setDataIntegrityViolationCodes("6");
+ ERROR_CODES.setCannotAcquireLockCodes("7");
+ ERROR_CODES.setDeadlockLoserCodes("8");
+ ERROR_CODES.setCannotSerializeTransactionCodes("9");
+ }
+
+ @Test
+ public void shouldTranslateToBadGrammarException() {
+
+ R2dbcExceptionTranslator sut = new SqlErrorCodeR2dbcExceptionTranslator(ERROR_CODES);
+
+ R2dbcException cause = new MyR2dbcException("", "", 1);
+ BadSqlGrammarException exception = (BadSqlGrammarException) sut.translate("task", "SQL", cause);
+
+ assertThat(exception.getSql()).isEqualTo("SQL");
+ assertThat(exception.getR2dbcException()).isEqualTo(cause);
+ }
+
+ @Test
+ public void shouldTranslateToResultException() {
+
+ R2dbcExceptionTranslator sut = new SqlErrorCodeR2dbcExceptionTranslator(ERROR_CODES);
+
+ R2dbcException cause = new MyR2dbcException("", "", 4);
+ InvalidResultAccessException exception = (InvalidResultAccessException) sut.translate("task", "SQL", cause);
+
+ assertThat(exception.getSql()).isEqualTo("SQL");
+ assertThat(exception.getR2dbcException()).isEqualTo(cause);
+ }
+
+ @Test
+ public void shouldFallbackToUncategorized() {
+
+ R2dbcExceptionTranslator sut = new SqlErrorCodeR2dbcExceptionTranslator(ERROR_CODES);
+
+ // Test fallback. We assume that no database will ever return this error code,
+ // but 07xxx will be bad grammar picked up by the fallback SQLState translator
+ R2dbcException cause = new MyR2dbcException("", "07xxx", 666666666);
+ UncategorizedR2dbcException exception = (UncategorizedR2dbcException) sut.translate("task", "SQL2", cause);
+
+ assertThat(exception.getSql()).isEqualTo("SQL2");
+ assertThat(exception.getR2dbcException()).isEqualTo(cause);
+ }
+
+ @Test
+ public void shouldTranslateDataIntegrityViolationException() {
+
+ R2dbcExceptionTranslator sut = new SqlErrorCodeR2dbcExceptionTranslator(ERROR_CODES);
+
+ R2dbcException cause = new MyR2dbcException("", "", 10);
+ DataAccessException exception = sut.translate("task", "SQL", cause);
+
+ assertThat(exception).isInstanceOf(DataIntegrityViolationException.class);
+ }
+
+ @Test
+ public void errorCodeTranslation() {
+
+ R2dbcExceptionTranslator sut = new SqlErrorCodeR2dbcExceptionTranslator(ERROR_CODES);
+
+ checkTranslation(sut, 5, DataAccessResourceFailureException.class);
+ checkTranslation(sut, 6, DataIntegrityViolationException.class);
+ checkTranslation(sut, 7, CannotAcquireLockException.class);
+ checkTranslation(sut, 8, DeadlockLoserDataAccessException.class);
+ checkTranslation(sut, 9, CannotSerializeTransactionException.class);
+ checkTranslation(sut, 10, DuplicateKeyException.class);
+ }
+
+ private static void checkTranslation(R2dbcExceptionTranslator sext, int errorCode, Class> exClass) {
+
+ R2dbcException cause = new MyR2dbcException("", "", errorCode);
+ DataAccessException exception = sext.translate("", "", cause);
+
+ assertThat(exception).isInstanceOf(exClass).hasCause(cause);
+ }
+
+ @Test
+ public void shouldApplyCustomTranslation() {
+
+ String TASK = "TASK";
+ String SQL = "SQL SELECT *";
+ DataAccessException custom = new DataAccessException("") {};
+
+ R2dbcException cause = new MyR2dbcException("", "", 1);
+ R2dbcException intVioEx = new MyR2dbcException("", "", 6);
+
+ SqlErrorCodeR2dbcExceptionTranslator translator = new SqlErrorCodeR2dbcExceptionTranslator() {
+ @Override
+ protected DataAccessException customTranslate(String task, String sql, R2dbcException sqlex) {
+
+ assertThat(task).isEqualTo(TASK);
+ assertThat(sql).isEqualTo(SQL);
+ return (sqlex == cause) ? custom : null;
+ }
+ };
+ translator.setSqlErrorCodes(ERROR_CODES);
+
+ // Shouldn't custom translate this
+ assertThat(translator.translate(TASK, SQL, cause)).isEqualTo(custom);
+
+ DataIntegrityViolationException diex = (DataIntegrityViolationException) translator.translate(TASK, SQL, intVioEx);
+ assertThat(diex).hasCause(intVioEx);
+ }
+
+ static class MyR2dbcException extends R2dbcException {
+
+ MyR2dbcException(String reason, String sqlState, int errorCode) {
+ super(reason, sqlState, errorCode);
+ }
+ }
+}
diff --git a/src/test/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslatorUnitTests.java b/src/test/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslatorUnitTests.java
new file mode 100644
index 0000000..39b6f8f
--- /dev/null
+++ b/src/test/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslatorUnitTests.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc.support;
+
+import static org.assertj.core.api.Assertions.*;
+
+import io.r2dbc.spi.R2dbcException;
+
+import org.junit.Test;
+import org.springframework.dao.ConcurrencyFailureException;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.DataAccessResourceFailureException;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.dao.TransientDataAccessResourceException;
+import org.springframework.data.r2dbc.BadSqlGrammarException;
+import org.springframework.data.r2dbc.UncategorizedR2dbcException;
+
+/**
+ * Unit tests for {@link SqlStateR2dbcExceptionTranslator}.
+ *
+ * @author Mark Paluch
+ */
+public class SqlStateR2dbcExceptionTranslatorUnitTests {
+
+ private static final String REASON = "The game is afoot!";
+ private static final String TASK = "Counting sheep... yawn.";
+ private static final String SQL = "select count(0) from t_sheep where over_fence = ... yawn... 1";
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testTranslateNullException() {
+ new SqlStateR2dbcExceptionTranslator().translate("", "", null);
+ }
+
+ @Test
+ public void testTranslateBadSqlGrammar() {
+ doTest("07", BadSqlGrammarException.class);
+ }
+
+ @Test
+ public void testTranslateDataIntegrityViolation() {
+ doTest("23", DataIntegrityViolationException.class);
+ }
+
+ @Test
+ public void testTranslateDataAccessResourceFailure() {
+ doTest("53", DataAccessResourceFailureException.class);
+ }
+
+ @Test
+ public void testTranslateTransientDataAccessResourceFailure() {
+ doTest("S1", TransientDataAccessResourceException.class);
+ }
+
+ @Test
+ public void testTranslateConcurrencyFailure() {
+ doTest("40", ConcurrencyFailureException.class);
+ }
+
+ @Test
+ public void testTranslateUncategorized() {
+ doTest("00000000", UncategorizedR2dbcException.class);
+ }
+
+ private static void doTest(String sqlState, Class> dataAccessExceptionType) {
+
+ R2dbcException ex = new R2dbcException(REASON, sqlState) {};
+ SqlStateR2dbcExceptionTranslator translator = new SqlStateR2dbcExceptionTranslator();
+ DataAccessException dax = translator.translate(TASK, SQL, ex);
+
+ assertThat(dax).isNotNull().isInstanceOf(dataAccessExceptionType).hasCause(ex);
+ }
+}
+ *
+ *