#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.
This commit is contained in:
Mark Paluch
2019-03-19 12:38:19 +01:00
committed by Jens Schauder
parent 9c6142f941
commit e161476d1c
7 changed files with 220 additions and 9 deletions

View File

@@ -34,7 +34,7 @@
<mysql.version>5.1.47</mysql.version>
<jasync.version>0.9.38</jasync.version>
<mssql-jdbc.version>7.1.2.jre8-preview</mssql-jdbc.version>
<r2dbc-releasetrain.version>Arabba-M7</r2dbc-releasetrain.version>
<r2dbc-releasetrain.version>Arabba-BUILD-SNAPSHOT</r2dbc-releasetrain.version>
<reactive-streams.version>1.0.1</reactive-streams.version>
<testcontainers.version>1.10.1</testcontainers.version>
@@ -111,6 +111,7 @@
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<optional>true</optional>
</dependency>
<dependency>

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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}.
* <p>
* 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();
}
}

View File

@@ -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.
* <p>
* 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;
}
}

View File

@@ -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();
}

View File

@@ -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 {}
}