From e161476d1c8456e4e90632be8fc6761f3a4a62fb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Mar 2019 12:38:19 +0100 Subject: [PATCH] #57 - Add support for R2DBC subclass exception translation. We now use R2DBC's exception hierarchy to translate exceptions into Spring's DataAccessException hierarchy. Original pull request: #97. --- pom.xml | 3 +- .../config/AbstractR2dbcConfiguration.java | 7 +- .../DefaultDatabaseClientBuilder.java | 4 +- ...tractFallbackR2dbcExceptionTranslator.java | 5 +- .../R2dbcExceptionSubclassTranslator.java | 91 ++++++++++++++ ...bstractDatabaseClientIntegrationTests.java | 5 +- ...cExceptionSubclassTranslatorUnitTests.java | 114 ++++++++++++++++++ 7 files changed, 220 insertions(+), 9 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslator.java create mode 100644 src/test/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslatorUnitTests.java diff --git a/pom.xml b/pom.xml index 03f3665..a577fa5 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ 5.1.47 0.9.38 7.1.2.jre8-preview - Arabba-M7 + Arabba-BUILD-SNAPSHOT 1.0.1 1.10.1 @@ -111,6 +111,7 @@ org.springframework spring-jdbc + true diff --git a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java index 2f197d6..8504398 100644 --- a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java +++ b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java @@ -35,8 +35,9 @@ import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; import org.springframework.data.r2dbc.function.convert.R2dbcCustomConversions; +import org.springframework.data.r2dbc.support.R2dbcExceptionSubclassTranslator; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; -import org.springframework.data.r2dbc.support.SqlErrorCodeR2dbcExceptionTranslator; +import org.springframework.data.r2dbc.support.SqlStateR2dbcExceptionTranslator; import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; @@ -186,10 +187,12 @@ public abstract class AbstractR2dbcConfiguration implements ApplicationContextAw * * @return must not be {@literal null}. * @see #connectionFactory() + * @see R2dbcExceptionSubclassTranslator + * @see SqlStateR2dbcExceptionTranslator */ @Bean public R2dbcExceptionTranslator exceptionTranslator() { - return new SqlErrorCodeR2dbcExceptionTranslator(lookupConnectionFactory()); + return new R2dbcExceptionSubclassTranslator(); } ConnectionFactory lookupConnectionFactory() { diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java index 992cab5..a8d18b5 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java @@ -23,8 +23,8 @@ import java.util.function.Consumer; import org.springframework.data.r2dbc.dialect.Database; import org.springframework.data.r2dbc.dialect.Dialect; import org.springframework.data.r2dbc.function.DatabaseClient.Builder; +import org.springframework.data.r2dbc.support.R2dbcExceptionSubclassTranslator; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; -import org.springframework.data.r2dbc.support.SqlErrorCodeR2dbcExceptionTranslator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -114,7 +114,7 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { R2dbcExceptionTranslator exceptionTranslator = this.exceptionTranslator; if (exceptionTranslator == null) { - exceptionTranslator = new SqlErrorCodeR2dbcExceptionTranslator(connectionFactory); + exceptionTranslator = new R2dbcExceptionSubclassTranslator(); } ReactiveDataAccessStrategy accessStrategy = this.accessStrategy; diff --git a/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java index bb57526..139ea8b 100644 --- a/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java +++ b/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java @@ -19,6 +19,7 @@ import io.r2dbc.spi.R2dbcException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.dao.DataAccessException; import org.springframework.data.r2dbc.UncategorizedR2dbcException; import org.springframework.lang.NonNull; @@ -99,7 +100,7 @@ public abstract class AbstractFallbackR2dbcExceptionTranslator implements R2dbcE protected abstract DataAccessException doTranslate(String task, @Nullable String sql, R2dbcException ex); /** - * Build a message {@code String} for the given {@link java.sql.R2dbcException}. + * Build a message {@code String} for the given {@link R2dbcException}. *

* To be called by translator subclasses when creating an instance of a generic * {@link org.springframework.dao.DataAccessException} class. @@ -110,6 +111,6 @@ public abstract class AbstractFallbackR2dbcExceptionTranslator implements R2dbcE * @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(); + return task + "; " + (sql != null ? "SQL [" + sql + "]; " : "") + ex.getMessage(); } } diff --git a/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslator.java new file mode 100644 index 0000000..16ed7a6 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslator.java @@ -0,0 +1,91 @@ +/* + * Copyright 2019 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 + * + * https://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.R2dbcBadGrammarException; +import io.r2dbc.spi.R2dbcDataIntegrityViolationException; +import io.r2dbc.spi.R2dbcException; +import io.r2dbc.spi.R2dbcNonTransientException; +import io.r2dbc.spi.R2dbcNonTransientResourceException; +import io.r2dbc.spi.R2dbcPermissionDeniedException; +import io.r2dbc.spi.R2dbcRollbackException; +import io.r2dbc.spi.R2dbcTimeoutException; +import io.r2dbc.spi.R2dbcTransientException; +import io.r2dbc.spi.R2dbcTransientResourceException; + +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.dao.QueryTimeoutException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.data.r2dbc.BadSqlGrammarException; +import org.springframework.lang.Nullable; + +/** + * {@link R2dbcExceptionTranslator} implementation which analyzes the specific {@link R2dbcException} subclass thrown by + * the R2DBC driver. + *

+ * Falls back to a standard {@link SqlStateR2dbcExceptionTranslator}. + * + * @author Mark Paluch + */ +public class R2dbcExceptionSubclassTranslator extends AbstractFallbackR2dbcExceptionTranslator { + + public R2dbcExceptionSubclassTranslator() { + setFallbackTranslator(new SqlStateR2dbcExceptionTranslator()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.support.AbstractFallbackR2dbcExceptionTranslator#doTranslate(java.lang.String, java.lang.String, io.r2dbc.spi.R2dbcException) + */ + @Override + @Nullable + protected DataAccessException doTranslate(String task, @Nullable String sql, R2dbcException ex) { + + if (ex instanceof R2dbcTransientException) { + if (ex instanceof R2dbcTransientResourceException) { + return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof R2dbcRollbackException) { + return new ConcurrencyFailureException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof R2dbcTimeoutException) { + return new QueryTimeoutException(buildMessage(task, sql, ex), ex); + } + } + + if (ex instanceof R2dbcNonTransientException) { + if (ex instanceof R2dbcNonTransientResourceException) { + return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof R2dbcDataIntegrityViolationException) { + return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof R2dbcPermissionDeniedException) { + return new PermissionDeniedDataAccessException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof R2dbcBadGrammarException) { + return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex); + } + } + + // Fallback to Spring's own R2DBC state translation... + return null; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java index 36818df..fb72765 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java @@ -27,8 +27,9 @@ import javax.sql.DataSource; import org.junit.Before; import org.junit.Test; + import org.springframework.dao.DataAccessException; -import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; @@ -125,7 +126,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr .fetch().rowsUpdated() // .as(StepVerifier::create) // .expectErrorSatisfies(exception -> assertThat(exception) // - .isInstanceOf(DuplicateKeyException.class) // + .isInstanceOf(DataIntegrityViolationException.class) // .hasMessageContaining("execute; SQL [INSERT INTO legoset")) // .verify(); } diff --git a/src/test/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslatorUnitTests.java b/src/test/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslatorUnitTests.java new file mode 100644 index 0000000..c65f090 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslatorUnitTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2019 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 + * + * https://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.R2dbcBadGrammarException; +import io.r2dbc.spi.R2dbcDataIntegrityViolationException; +import io.r2dbc.spi.R2dbcException; +import io.r2dbc.spi.R2dbcNonTransientResourceException; +import io.r2dbc.spi.R2dbcPermissionDeniedException; +import io.r2dbc.spi.R2dbcRollbackException; +import io.r2dbc.spi.R2dbcTimeoutException; +import io.r2dbc.spi.R2dbcTransientResourceException; + +import org.junit.Test; + +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.dao.QueryTimeoutException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.data.r2dbc.BadSqlGrammarException; +import org.springframework.data.r2dbc.UncategorizedR2dbcException; + +/** + * Unit tests for {@link R2dbcExceptionSubclassTranslator}. + * + * @author Mark Paluch + */ +public class R2dbcExceptionSubclassTranslatorUnitTests { + + R2dbcExceptionSubclassTranslator translator = new R2dbcExceptionSubclassTranslator(); + + @Test // gh-57 + public void shouldTranslateTransientResourceException() { + + Exception exception = translator.translate("", "", new R2dbcTransientResourceException()); + + assertThat(exception).isInstanceOf(TransientDataAccessResourceException.class); + } + + @Test // gh-57 + public void shouldTranslateRollbackException() { + + Exception exception = translator.translate("", "", new R2dbcRollbackException()); + + assertThat(exception).isInstanceOf(ConcurrencyFailureException.class); + } + + @Test // gh-57 + public void shouldTranslateTimeoutException() { + + Exception exception = translator.translate("", "", new R2dbcTimeoutException()); + + assertThat(exception).isInstanceOf(QueryTimeoutException.class); + } + + @Test // gh-57 + public void shouldNotTranslateUnknownExceptions() { + + Exception exception = translator.translate("", "", new MyTransientExceptions()); + + assertThat(exception).isInstanceOf(UncategorizedR2dbcException.class); + } + + @Test // gh-57 + public void shouldTranslateNonTransientResourceException() { + + Exception exception = translator.translate("", "", new R2dbcNonTransientResourceException()); + + assertThat(exception).isInstanceOf(DataAccessResourceFailureException.class); + } + + @Test // gh-57 + public void shouldTranslateIntegrityViolationException() { + + Exception exception = translator.translate("", "", new R2dbcDataIntegrityViolationException()); + + assertThat(exception).isInstanceOf(DataIntegrityViolationException.class); + } + + @Test // gh-57 + public void shouldTranslatePermissionDeniedException() { + + Exception exception = translator.translate("", "", new R2dbcPermissionDeniedException()); + + assertThat(exception).isInstanceOf(PermissionDeniedDataAccessException.class); + } + + @Test // gh-57 + public void shouldTranslateBadSqlGrammarException() { + + Exception exception = translator.translate("", "", new R2dbcBadGrammarException()); + + assertThat(exception).isInstanceOf(BadSqlGrammarException.class); + } + + private static class MyTransientExceptions extends R2dbcException {} +}