Skip to content

Commit e9cded5

Browse files
committed
Introduce JdbcTransactionManager with SQLExceptionTranslator support
Closes gh-24064
1 parent 2aa8aef commit e9cded5

File tree

8 files changed

+2064
-55
lines changed

8 files changed

+2064
-55
lines changed

spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@
9898
* setup analogous to {@code JtaTransactionManager}, in particular with respect to
9999
* lazily registered ORM resources (e.g. a Hibernate {@code Session}).
100100
*
101+
* <p><b>NOTE: As of 5.3, {@link org.springframework.jdbc.support.JdbcTransactionManager}
102+
* is available as an extended subclass which includes commit/rollback exception
103+
* translation, aligned with {@link org.springframework.jdbc.core.JdbcTemplate}.</b>
104+
*
101105
* @author Juergen Hoeller
102106
* @since 02.05.2003
103107
* @see #setNestedTransactionAllowed
@@ -332,7 +336,7 @@ protected void doCommit(DefaultTransactionStatus status) {
332336
con.commit();
333337
}
334338
catch (SQLException ex) {
335-
throw new TransactionSystemException("Could not commit JDBC transaction", ex);
339+
throw translateException("JDBC commit", ex);
336340
}
337341
}
338342

@@ -347,7 +351,7 @@ protected void doRollback(DefaultTransactionStatus status) {
347351
con.rollback();
348352
}
349353
catch (SQLException ex) {
350-
throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
354+
throw translateException("JDBC rollback", ex);
351355
}
352356
}
353357

@@ -418,6 +422,22 @@ protected void prepareTransactionalConnection(Connection con, TransactionDefinit
418422
}
419423
}
420424

425+
/**
426+
* Translate the given JDBC commit/rollback exception to a common Spring
427+
* exception to propagate from the {@link #commit}/{@link #rollback} call.
428+
* <p>The default implementation throws a {@link TransactionSystemException}.
429+
* Subclasses may specifically identify concurrency failures etc.
430+
* @param task the task description (commit or rollback)
431+
* @param ex the SQLException thrown from commit/rollback
432+
* @return the translated exception to throw, either a
433+
* {@link org.springframework.dao.DataAccessException} or a
434+
* {@link org.springframework.transaction.TransactionException}
435+
* @since 5.3
436+
*/
437+
protected RuntimeException translateException(String task, SQLException ex) {
438+
return new TransactionSystemException(task + " failed", ex);
439+
}
440+
421441

422442
/**
423443
* DataSource transaction object, representing a ConnectionHolder.

spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,8 +22,6 @@
2222
import org.apache.commons.logging.LogFactory;
2323

2424
import org.springframework.dao.DataAccessException;
25-
import org.springframework.jdbc.UncategorizedSQLException;
26-
import org.springframework.lang.NonNull;
2725
import org.springframework.lang.Nullable;
2826
import org.springframework.util.Assert;
2927

@@ -65,7 +63,7 @@ public SQLExceptionTranslator getFallbackTranslator() {
6563
* {@link #getFallbackTranslator() fallback translator} if necessary.
6664
*/
6765
@Override
68-
@NonNull
66+
@Nullable
6967
public DataAccessException translate(String task, @Nullable String sql, SQLException ex) {
7068
Assert.notNull(ex, "Cannot translate a null SQLException");
7169

@@ -78,15 +76,10 @@ public DataAccessException translate(String task, @Nullable String sql, SQLExcep
7876
// Looking for a fallback...
7977
SQLExceptionTranslator fallback = getFallbackTranslator();
8078
if (fallback != null) {
81-
dae = fallback.translate(task, sql, ex);
82-
if (dae != null) {
83-
// Fallback exception match found.
84-
return dae;
85-
}
79+
return fallback.translate(task, sql, ex);
8680
}
8781

88-
// We couldn't identify it more precisely.
89-
return new UncategorizedSQLException(task, sql, ex);
82+
return null;
9083
}
9184

9285
/**
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.jdbc.support;
18+
19+
import java.sql.SQLException;
20+
21+
import javax.sql.DataSource;
22+
23+
import org.springframework.dao.DataAccessException;
24+
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
25+
import org.springframework.lang.Nullable;
26+
27+
/**
28+
* {@link JdbcAccessor}-aligned subclass of the plain {@link DataSourceTransactionManager},
29+
* adding common JDBC exception translation for the commit and rollback step.
30+
* Typically used in combination with {@link org.springframework.jdbc.core.JdbcTemplate}
31+
* which applies the same {@link SQLExceptionTranslator} infrastructure by default.
32+
*
33+
* <p>Exception translation is specifically relevant for commit steps in serializable
34+
* transactions (e.g. on Postgres) where concurrency failures may occur late on commit.
35+
* This allows for throwing {@link org.springframework.dao.ConcurrencyFailureException} to
36+
* callers instead of {@link org.springframework.transaction.TransactionSystemException}.
37+
*
38+
* <p>Analogous to {@code HibernateTransactionManager} and {@code JpaTransactionManager},
39+
* this transaction manager may throw {@link DataAccessException} from {@link #commit}
40+
* and possibly also from {@link #rollback}. Calling code should be prepared for handling
41+
* such exceptions next to {@link org.springframework.transaction.TransactionException},
42+
* which is generally sensible since {@code TransactionSynchronization} implementations
43+
* may also throw such exceptions in their {@code flush} and {@code beforeCommit} phases.
44+
*
45+
* @author Juergen Hoeller
46+
* @since 5.3
47+
* @see DataSourceTransactionManager
48+
* @see #setDataSource
49+
* @see #setExceptionTranslator
50+
*/
51+
@SuppressWarnings("serial")
52+
public class JdbcTransactionManager extends DataSourceTransactionManager {
53+
54+
@Nullable
55+
private volatile SQLExceptionTranslator exceptionTranslator;
56+
57+
private boolean lazyInit = true;
58+
59+
60+
/**
61+
* Create a new JdbcTransactionManager instance.
62+
* A DataSource has to be set to be able to use it.
63+
* @see #setDataSource
64+
*/
65+
public JdbcTransactionManager() {
66+
super();
67+
}
68+
69+
/**
70+
* Create a new JdbcTransactionManager instance.
71+
* @param dataSource the JDBC DataSource to manage transactions for
72+
*/
73+
public JdbcTransactionManager(DataSource dataSource) {
74+
this();
75+
setDataSource(dataSource);
76+
afterPropertiesSet();
77+
}
78+
79+
80+
/**
81+
* Specify the database product name for the DataSource that this transaction manager
82+
* uses. This allows to initialize an SQLErrorCodeSQLExceptionTranslator without
83+
* obtaining a Connection from the DataSource to get the meta-data.
84+
* @param dbName the database product name that identifies the error codes entry
85+
* @see JdbcAccessor#setDatabaseProductName
86+
* @see SQLErrorCodeSQLExceptionTranslator#setDatabaseProductName
87+
* @see java.sql.DatabaseMetaData#getDatabaseProductName()
88+
*/
89+
public void setDatabaseProductName(String dbName) {
90+
this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dbName);
91+
}
92+
93+
/**
94+
* Set the exception translator for this instance.
95+
* <p>If no custom translator is provided, a default
96+
* {@link SQLErrorCodeSQLExceptionTranslator} is used
97+
* which examines the SQLException's vendor-specific error code.
98+
* @see JdbcAccessor#setExceptionTranslator
99+
* @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
100+
*/
101+
public void setExceptionTranslator(SQLExceptionTranslator exceptionTranslator) {
102+
this.exceptionTranslator = exceptionTranslator;
103+
}
104+
105+
/**
106+
* Return the exception translator for this instance.
107+
* <p>Creates a default {@link SQLErrorCodeSQLExceptionTranslator}
108+
* for the specified DataSource if none set.
109+
* @see #getDataSource()
110+
*/
111+
public SQLExceptionTranslator getExceptionTranslator() {
112+
SQLExceptionTranslator exceptionTranslator = this.exceptionTranslator;
113+
if (exceptionTranslator != null) {
114+
return exceptionTranslator;
115+
}
116+
synchronized (this) {
117+
exceptionTranslator = this.exceptionTranslator;
118+
if (exceptionTranslator == null) {
119+
exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(obtainDataSource());
120+
this.exceptionTranslator = exceptionTranslator;
121+
}
122+
return exceptionTranslator;
123+
}
124+
}
125+
126+
/**
127+
* Set whether to lazily initialize the SQLExceptionTranslator for this transaction manager,
128+
* on first encounter of an SQLException. Default is "true"; can be switched to
129+
* "false" for initialization on startup.
130+
* <p>Early initialization just applies if {@code afterPropertiesSet()} is called.
131+
* @see #getExceptionTranslator()
132+
* @see #afterPropertiesSet()
133+
*/
134+
public void setLazyInit(boolean lazyInit) {
135+
this.lazyInit = lazyInit;
136+
}
137+
138+
/**
139+
* Return whether to lazily initialize the SQLExceptionTranslator for this transaction manager.
140+
* @see #getExceptionTranslator()
141+
*/
142+
public boolean isLazyInit() {
143+
return this.lazyInit;
144+
}
145+
146+
/**
147+
* Eagerly initialize the exception translator, if demanded,
148+
* creating a default one for the specified DataSource if none set.
149+
*/
150+
@Override
151+
public void afterPropertiesSet() {
152+
super.afterPropertiesSet();
153+
if (!isLazyInit()) {
154+
getExceptionTranslator();
155+
}
156+
}
157+
158+
159+
/**
160+
* This implementation attempts to use the {@link SQLExceptionTranslator},
161+
* falling back to a {@link org.springframework.transaction.TransactionSystemException}.
162+
* @see #getExceptionTranslator()
163+
* @see DataSourceTransactionManager#translateException
164+
*/
165+
@Override
166+
protected RuntimeException translateException(String task, SQLException ex) {
167+
DataAccessException dae = getExceptionTranslator().translate(task, null, ex);
168+
if (dae != null) {
169+
return dae;
170+
}
171+
return super.translateException(task, ex);
172+
}
173+
174+
}

spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -49,9 +49,7 @@ public interface SQLExceptionTranslator {
4949
* @param sql the SQL query or update that caused the problem (if known)
5050
* @param ex the offending {@code SQLException}
5151
* @return the DataAccessException wrapping the {@code SQLException},
52-
* or {@code null} if no translation could be applied
53-
* (in a custom translator; the default translators always throw an
54-
* {@link org.springframework.jdbc.UncategorizedSQLException} in such a case)
52+
* or {@code null} if no specific translation could be applied
5553
* @see org.springframework.dao.DataAccessException#getRootCause()
5654
*/
5755
@Nullable

spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -63,6 +63,7 @@
6363
/**
6464
* @author Juergen Hoeller
6565
* @since 04.07.2003
66+
* @see org.springframework.jdbc.support.JdbcTransactionManagerTests
6667
*/
6768
public class DataSourceTransactionManagerTests {
6869

@@ -284,8 +285,7 @@ public void testTransactionRollbackOnly() throws Exception {
284285
boolean condition1 = !TransactionSynchronizationManager.isSynchronizationActive();
285286
assertThat(condition1).as("Synchronization not active").isTrue();
286287

287-
ConnectionHolder conHolder = new ConnectionHolder(con);
288-
conHolder.setTransactionActive(true);
288+
ConnectionHolder conHolder = new ConnectionHolder(con, true);
289289
TransactionSynchronizationManager.bindResource(ds, conHolder);
290290
final RuntimeException ex = new RuntimeException("Application exception");
291291
try {

0 commit comments

Comments
 (0)