Skip to content

Commit a8fbd8f

Browse files
committed
GH-611 added JdbcExceptionTranslator
1 parent e4ba477 commit a8fbd8f

12 files changed

+206
-16
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java

+14-9
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@
1515
*/
1616
package org.springframework.data.jdbc.core;
1717

18-
import org.springframework.dao.OptimisticLockingFailureException;
18+
import java.util.List;
19+
import java.util.Optional;
20+
import org.springframework.dao.support.PersistenceExceptionTranslator;
1921
import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
2022
import org.springframework.data.jdbc.core.convert.JdbcConverter;
2123
import org.springframework.data.relational.core.conversion.AggregateChange;
2224
import org.springframework.data.relational.core.conversion.DbAction;
2325
import org.springframework.data.relational.core.conversion.DbActionExecutionException;
2426
import org.springframework.data.relational.core.conversion.MutableAggregateChange;
2527

26-
import java.util.List;
27-
2828
/**
2929
* Executes an {@link MutableAggregateChange}.
3030
*
@@ -37,11 +37,13 @@ class AggregateChangeExecutor {
3737

3838
private final JdbcConverter converter;
3939
private final DataAccessStrategy accessStrategy;
40+
private final PersistenceExceptionTranslator jdbcExceptionTranslator;
4041

4142
AggregateChangeExecutor(JdbcConverter converter, DataAccessStrategy accessStrategy) {
4243

4344
this.converter = converter;
4445
this.accessStrategy = accessStrategy;
46+
this.jdbcExceptionTranslator = new JdbcExceptionTranslator();
4547
}
4648

4749
/**
@@ -110,12 +112,15 @@ private void execute(DbAction<?> action, JdbcAggregateChangeExecutionContext exe
110112
} else {
111113
throw new RuntimeException("unexpected action");
112114
}
113-
} catch (Exception e) {
114-
115-
if (e instanceof OptimisticLockingFailureException) {
116-
throw e;
117-
}
118-
throw new DbActionExecutionException(action, e);
115+
} catch (RuntimeException e) {
116+
117+
throw Optional
118+
.ofNullable(jdbcExceptionTranslator.translateExceptionIfPossible(e))
119+
.map(it -> (RuntimeException) it)
120+
.orElseGet(() -> {
121+
e.addSuppressed(new DbActionExecutionException(action, e));
122+
return e;
123+
});
119124
}
120125
}
121126
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.springframework.data.jdbc.core;
2+
3+
import java.util.Set;
4+
import org.springframework.dao.DataAccessException;
5+
import org.springframework.dao.DuplicateKeyException;
6+
import org.springframework.dao.OptimisticLockingFailureException;
7+
import org.springframework.dao.support.PersistenceExceptionTranslator;
8+
9+
/**
10+
* {@link PersistenceExceptionTranslator} that is capable to translate exceptions for JDBC module.
11+
*
12+
* @author Mikhail Polivakha
13+
*/
14+
public class JdbcExceptionTranslator implements PersistenceExceptionTranslator {
15+
16+
private static final Set<Class<? extends DataAccessException>> PASS_THROUGH_EXCEPTIONS = Set.of(
17+
OptimisticLockingFailureException.class,
18+
DuplicateKeyException.class
19+
);
20+
21+
@Override
22+
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
23+
if (PASS_THROUGH_EXCEPTIONS.contains(ex.getClass())) {
24+
return (DataAccessException) ex;
25+
}
26+
27+
return null;
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package org.springframework.data.jdbc.core;
2+
3+
import java.util.stream.Stream;
4+
import org.assertj.core.api.Assertions;
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.params.ParameterizedTest;
7+
import org.junit.jupiter.params.provider.Arguments;
8+
import org.junit.jupiter.params.provider.MethodSource;
9+
import org.springframework.dao.DataAccessException;
10+
import org.springframework.dao.DuplicateKeyException;
11+
import org.springframework.dao.OptimisticLockingFailureException;
12+
13+
/**
14+
* Unit tests for {@link JdbcExceptionTranslator}
15+
*
16+
* @author Mikhail Polivakha
17+
*/
18+
class JdbcExceptionTranslatorTest {
19+
20+
@ParameterizedTest
21+
@MethodSource(value = "passThroughExceptionsSource")
22+
void testPassThroughExceptions(DataAccessException exception) {
23+
24+
// when
25+
DataAccessException translated = new JdbcExceptionTranslator().translateExceptionIfPossible(exception);
26+
27+
// then.
28+
Assertions.assertThat(translated).isSameAs(exception);
29+
}
30+
31+
@Test
32+
void testUnrecognizedException() {
33+
34+
// when
35+
DataAccessException translated = new JdbcExceptionTranslator().translateExceptionIfPossible(new IllegalArgumentException());
36+
37+
// then.
38+
Assertions.assertThat(translated).isNull();
39+
}
40+
41+
static Stream<Arguments> passThroughExceptionsSource() {
42+
return Stream.of(Arguments.of(new OptimisticLockingFailureException("")), Arguments.of(new DuplicateKeyException("")));
43+
}
44+
}

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java

+60
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.util.function.Consumer;
3838
import java.util.stream.Stream;
3939

40+
import org.assertj.core.api.Assertions;
4041
import org.junit.jupiter.api.BeforeEach;
4142
import org.junit.jupiter.api.Test;
4243
import org.junit.jupiter.params.ParameterizedTest;
@@ -50,14 +51,17 @@
5051
import org.springframework.context.annotation.Configuration;
5152
import org.springframework.context.annotation.Import;
5253
import org.springframework.core.io.ClassPathResource;
54+
import org.springframework.dao.DuplicateKeyException;
5355
import org.springframework.dao.IncorrectResultSizeDataAccessException;
5456
import org.springframework.data.annotation.Id;
57+
import org.springframework.data.annotation.Transient;
5558
import org.springframework.data.domain.Example;
5659
import org.springframework.data.domain.ExampleMatcher;
5760
import org.springframework.data.domain.Limit;
5861
import org.springframework.data.domain.Page;
5962
import org.springframework.data.domain.PageRequest;
6063
import org.springframework.data.domain.Pageable;
64+
import org.springframework.data.domain.Persistable;
6165
import org.springframework.data.domain.ScrollPosition;
6266
import org.springframework.data.domain.Slice;
6367
import org.springframework.data.domain.Sort;
@@ -113,6 +117,8 @@ public class JdbcRepositoryIntegrationTests {
113117

114118
@Autowired NamedParameterJdbcTemplate template;
115119
@Autowired DummyEntityRepository repository;
120+
121+
@Autowired ProvidedIdEntityRepository providedIdEntityRepository;
116122
@Autowired MyEventListener eventListener;
117123
@Autowired RootRepository rootRepository;
118124

@@ -193,6 +199,18 @@ public void findAllFindsAllSpecifiedEntities() {
193199
.containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp());
194200
}
195201

202+
@Test // DATAJDBC-611
203+
public void testDuplicateKeyExceptionIsThrownInCaseOfUniqueKeyViolation() {
204+
205+
// given.
206+
ProvidedIdEntity first = ProvidedIdEntity.newInstance(1L, "name");
207+
ProvidedIdEntity second = ProvidedIdEntity.newInstance(1L, "other");
208+
209+
// when/then
210+
Assertions.assertThatCode(() -> providedIdEntityRepository.save(first)).doesNotThrowAnyException();
211+
Assertions.assertThatThrownBy(() -> providedIdEntityRepository.save(second)).isInstanceOf(DuplicateKeyException.class);
212+
}
213+
196214
@Test // DATAJDBC-97
197215
public void countsEntities() {
198216

@@ -1421,6 +1439,10 @@ interface DummyProjectExample {
14211439
String getName();
14221440
}
14231441

1442+
interface ProvidedIdEntityRepository extends CrudRepository<ProvidedIdEntity, Long> {
1443+
1444+
}
1445+
14241446
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long>, QueryByExampleExecutor<DummyEntity> {
14251447

14261448
@Lock(LockMode.PESSIMISTIC_WRITE)
@@ -1526,6 +1548,11 @@ DummyEntityRepository dummyEntityRepository() {
15261548
return factory.getRepository(DummyEntityRepository.class);
15271549
}
15281550

1551+
@Bean
1552+
ProvidedIdEntityRepository providedIdEntityRepository() {
1553+
return factory.getRepository(ProvidedIdEntityRepository.class);
1554+
}
1555+
15291556
@Bean
15301557
RootRepository rootRepository() {
15311558
return factory.getRepository(RootRepository.class);
@@ -1839,6 +1866,39 @@ private static DummyEntity createEntity(String entityName, Consumer<DummyEntity>
18391866
return entity;
18401867
}
18411868

1869+
1870+
static class ProvidedIdEntity implements Persistable {
1871+
1872+
@Id
1873+
private Long id;
1874+
1875+
private String name;
1876+
1877+
@Transient
1878+
private boolean isNew;
1879+
1880+
private ProvidedIdEntity(Long id, String name, boolean isNew) {
1881+
this.id = id;
1882+
this.name = name;
1883+
this.isNew = isNew;
1884+
}
1885+
1886+
private static ProvidedIdEntity newInstance(Long id, String name) {
1887+
return new ProvidedIdEntity(id, name, true);
1888+
}
1889+
1890+
@Override
1891+
public Object getId() {
1892+
return id;
1893+
}
1894+
1895+
@Override
1896+
public boolean isNew() {
1897+
return isNew;
1898+
}
1899+
}
1900+
1901+
18421902
static class DummyEntity {
18431903

18441904
String name;

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ DROP TABLE ROOT;
33
DROP TABLE INTERMEDIATE;
44
DROP TABLE LEAF;
55
DROP TABLE WITH_DELIMITED_COLUMN;
6+
DROP TABLE PROVIDED_ID_ENTITY;
67

78
CREATE TABLE dummy_entity
89
(
@@ -45,4 +46,10 @@ CREATE TABLE WITH_DELIMITED_COLUMN
4546
ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
4647
"ORG.XTUNIT.IDENTIFIER" VARCHAR(100),
4748
STYPE VARCHAR(100)
48-
);
49+
);
50+
51+
CREATE TABLE PROVIDED_ID_ENTITY
52+
(
53+
ID BIGINT PRIMARY KEY,
54+
NAME VARCHAR(30)
55+
);

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql

+7-1
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,10 @@ CREATE TABLE WITH_DELIMITED_COLUMN
3939
ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
4040
"ORG.XTUNIT.IDENTIFIER" VARCHAR(100),
4141
STYPE VARCHAR(100)
42-
);
42+
);
43+
44+
CREATE TABLE PROVIDED_ID_ENTITY
45+
(
46+
ID BIGINT PRIMARY KEY,
47+
NAME VARCHAR(30)
48+
);

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql

+7-1
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,10 @@ CREATE TABLE WITH_DELIMITED_COLUMN
3939
ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
4040
"ORG.XTUNIT.IDENTIFIER" VARCHAR(100),
4141
STYPE VARCHAR(100)
42-
);
42+
);
43+
44+
CREATE TABLE PROVIDED_ID_ENTITY
45+
(
46+
ID BIGINT PRIMARY KEY,
47+
NAME VARCHAR(30)
48+
);

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql

+7-1
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,10 @@ CREATE TABLE WITH_DELIMITED_COLUMN
3939
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
4040
`ORG.XTUNIT.IDENTIFIER` VARCHAR(100),
4141
STYPE VARCHAR(100)
42-
);
42+
);
43+
44+
CREATE TABLE PROVIDED_ID_ENTITY
45+
(
46+
ID BIGINT PRIMARY KEY,
47+
NAME VARCHAR(30)
48+
);

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ DROP TABLE IF EXISTS ROOT;
33
DROP TABLE IF EXISTS INTERMEDIATE;
44
DROP TABLE IF EXISTS LEAF;
55
DROP TABLE IF EXISTS WITH_DELIMITED_COLUMN;
6+
DROP TABLE IF EXISTS PROVIDED_ID_ENTITY;
67

78
CREATE TABLE dummy_entity
89
(
@@ -45,4 +46,10 @@ CREATE TABLE WITH_DELIMITED_COLUMN
4546
ID BIGINT IDENTITY PRIMARY KEY,
4647
"ORG.XTUNIT.IDENTIFIER" VARCHAR(100),
4748
STYPE VARCHAR(100)
48-
);
49+
);
50+
51+
CREATE TABLE PROVIDED_ID_ENTITY
52+
(
53+
ID BIGINT PRIMARY KEY,
54+
NAME VARCHAR(30)
55+
);

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql

+7-1
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,10 @@ CREATE TABLE WITH_DELIMITED_COLUMN
4242
ID BIGINT AUTO_INCREMENT PRIMARY KEY,
4343
`ORG.XTUNIT.IDENTIFIER` VARCHAR(100),
4444
STYPE VARCHAR(100)
45-
);
45+
);
46+
47+
CREATE TABLE PROVIDED_ID_ENTITY
48+
(
49+
ID BIGINT PRIMARY KEY,
50+
NAME VARCHAR(30)
51+
);

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql

+7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ DROP TABLE ROOT CASCADE CONSTRAINTS PURGE;
33
DROP TABLE INTERMEDIATE CASCADE CONSTRAINTS PURGE;
44
DROP TABLE LEAF CASCADE CONSTRAINTS PURGE;
55
DROP TABLE WITH_DELIMITED_COLUMN CASCADE CONSTRAINTS PURGE;
6+
DROP TABLE PROVIDED_ID_ENTITY CASCADE CONSTRAINTS PURGE;
67

78
CREATE TABLE DUMMY_ENTITY
89
(
@@ -46,3 +47,9 @@ CREATE TABLE WITH_DELIMITED_COLUMN
4647
"ORG.XTUNIT.IDENTIFIER" VARCHAR(100),
4748
STYPE VARCHAR(100)
4849
)
50+
51+
CREATE TABLE PROVIDED_ID_ENTITY
52+
(
53+
ID BIGINT PRIMARY KEY,
54+
NAME VARCHAR(30)
55+
);

spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ DROP TABLE ROOT;
33
DROP TABLE INTERMEDIATE;
44
DROP TABLE LEAF;
55
DROP TABLE WITH_DELIMITED_COLUMN;
6+
DROP TABLE PROVIDED_ID_ENTITY;
67

78
CREATE TABLE dummy_entity
89
(
@@ -45,4 +46,10 @@ CREATE TABLE "WITH_DELIMITED_COLUMN"
4546
ID SERIAL PRIMARY KEY,
4647
"ORG.XTUNIT.IDENTIFIER" VARCHAR(100),
4748
"STYPE" VARCHAR(100)
48-
);
49+
);
50+
51+
CREATE TABLE PROVIDED_ID_ENTITY
52+
(
53+
ID BIGINT PRIMARY KEY,
54+
NAME VARCHAR(30)
55+
);

0 commit comments

Comments
 (0)