Support for read-only transactions through explicit SET TRANSACTION statement
Issue: SPR-15210
This commit is contained in:
parent
9b3131ffba
commit
02195f5abf
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2016 the original author or authors.
|
* Copyright 2002-2017 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -18,6 +18,7 @@ package org.springframework.jdbc.datasource;
|
||||||
|
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
|
@ -111,6 +112,8 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
|
||||||
|
|
||||||
private DataSource dataSource;
|
private DataSource dataSource;
|
||||||
|
|
||||||
|
private boolean enforceReadOnly = false;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new DataSourceTransactionManager instance.
|
* Create a new DataSourceTransactionManager instance.
|
||||||
|
@ -168,6 +171,38 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
|
||||||
return this.dataSource;
|
return this.dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify whether to enforce the read-only nature of a transaction
|
||||||
|
* (as indicated by {@link TransactionDefinition#isReadOnly()}
|
||||||
|
* through an explicit statement on the transactional connection:
|
||||||
|
* "SET TRANSACTION READ ONLY" as understood by Oracle, MySQL and Postgres.
|
||||||
|
* <p>The exact treatment, including any SQL statement executed on the connection,
|
||||||
|
* can be customized through through {@link #prepareTransactionalConnection}.
|
||||||
|
* <p>This mode of read-only handling goes beyond the {@link Connection#setReadOnly}
|
||||||
|
* hint that Spring applies by default. In contrast to that standard JDBC hint,
|
||||||
|
* "SET TRANSACTION READ ONLY" enforces an isolation-level-like connection mode
|
||||||
|
* where data manipulation statements are strictly disallowed. Also, on Oracle,
|
||||||
|
* this read-only mode provides read consistency for the entire transaction.
|
||||||
|
* <p>Note that older Oracle JDBC drivers (9i, 10g) used to enforce this read-only
|
||||||
|
* mode even for {@code Connection.setReadOnly(true}. However, with recent drivers,
|
||||||
|
* this strong enforcement needs to be applied explicitly, e.g. through this flag.
|
||||||
|
* @since 4.3.7
|
||||||
|
* @see #prepareTransactionalConnection
|
||||||
|
*/
|
||||||
|
public void setEnforceReadOnly(boolean enforceReadOnly) {
|
||||||
|
this.enforceReadOnly = enforceReadOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether to enforce the read-only nature of a transaction
|
||||||
|
* through an explicit statement on the transactional connection.
|
||||||
|
* @since 4.3.7
|
||||||
|
* @see #setEnforceReadOnly
|
||||||
|
*/
|
||||||
|
public boolean isEnforceReadOnly() {
|
||||||
|
return this.enforceReadOnly;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterPropertiesSet() {
|
public void afterPropertiesSet() {
|
||||||
if (getDataSource() == null) {
|
if (getDataSource() == null) {
|
||||||
|
@ -231,6 +266,8 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
|
||||||
}
|
}
|
||||||
con.setAutoCommit(false);
|
con.setAutoCommit(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prepareTransactionalConnection(con, definition);
|
||||||
txObject.getConnectionHolder().setTransactionActive(true);
|
txObject.getConnectionHolder().setTransactionActive(true);
|
||||||
|
|
||||||
int timeout = determineTimeout(definition);
|
int timeout = determineTimeout(definition);
|
||||||
|
@ -238,7 +275,7 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
|
||||||
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
|
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind the session holder to the thread.
|
// Bind the connection holder to the thread.
|
||||||
if (txObject.isNewConnectionHolder()) {
|
if (txObject.isNewConnectionHolder()) {
|
||||||
TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
|
TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
|
||||||
}
|
}
|
||||||
|
@ -257,15 +294,12 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
|
||||||
protected Object doSuspend(Object transaction) {
|
protected Object doSuspend(Object transaction) {
|
||||||
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
|
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
|
||||||
txObject.setConnectionHolder(null);
|
txObject.setConnectionHolder(null);
|
||||||
ConnectionHolder conHolder = (ConnectionHolder)
|
return TransactionSynchronizationManager.unbindResource(this.dataSource);
|
||||||
TransactionSynchronizationManager.unbindResource(this.dataSource);
|
|
||||||
return conHolder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doResume(Object transaction, Object suspendedResources) {
|
protected void doResume(Object transaction, Object suspendedResources) {
|
||||||
ConnectionHolder conHolder = (ConnectionHolder) suspendedResources;
|
TransactionSynchronizationManager.bindResource(this.dataSource, suspendedResources);
|
||||||
TransactionSynchronizationManager.bindResource(this.dataSource, conHolder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -340,6 +374,35 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the transactional {@code Connection} right after transaction begin.
|
||||||
|
* <p>The default implementation executes a "SET TRANSACTION READ ONLY" statement
|
||||||
|
* if the {@link #setEnforceReadOnly "enforceReadOnly"} flag is set to {@code true}
|
||||||
|
* and the transaction definition indicates a read-only transaction.
|
||||||
|
* <p>The "SET TRANSACTION READ ONLY" is understood by Oracle, MySQL and Postgres
|
||||||
|
* and may work with other databases as well. If you'd like to adapt this treatment,
|
||||||
|
* override this method accordingly.
|
||||||
|
* @param con the transactional JDBC Connection
|
||||||
|
* @param definition the current transaction definition
|
||||||
|
* @throws SQLException if thrown by JDBC API
|
||||||
|
* @since 4.3.7
|
||||||
|
* @see #setEnforceReadOnly
|
||||||
|
*/
|
||||||
|
protected void prepareTransactionalConnection(Connection con, TransactionDefinition definition)
|
||||||
|
throws SQLException {
|
||||||
|
|
||||||
|
if (isEnforceReadOnly() && definition.isReadOnly()) {
|
||||||
|
Statement stmt = con.createStatement();
|
||||||
|
try {
|
||||||
|
stmt.executeUpdate("SET TRANSACTION READ ONLY");
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
stmt.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DataSource transaction object, representing a ConnectionHolder.
|
* DataSource transaction object, representing a ConnectionHolder.
|
||||||
* Used as transaction object by DataSourceTransactionManager.
|
* Used as transaction object by DataSourceTransactionManager.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2014 the original author or authors.
|
* Copyright 2002-2017 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -21,6 +21,7 @@ import java.sql.DatabaseMetaData;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Savepoint;
|
import java.sql.Savepoint;
|
||||||
|
import java.sql.Statement;
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
|
@ -66,7 +67,7 @@ public class DataSourceTransactionManagerTests {
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
con = mock(Connection.class);
|
con = mock(Connection.class);
|
||||||
ds = mock(DataSource.class);
|
ds = mock(DataSource.class);
|
||||||
tm = new DataSourceTransactionManager(ds);
|
tm = new DataSourceTransactionManager(ds);
|
||||||
given(ds.getConnection()).willReturn(con);
|
given(ds.getConnection()).willReturn(con);
|
||||||
}
|
}
|
||||||
|
@ -111,8 +112,8 @@ public class DataSourceTransactionManagerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doTestTransactionCommitRestoringAutoCommit(
|
private void doTestTransactionCommitRestoringAutoCommit(
|
||||||
boolean autoCommit, boolean lazyConnection, final boolean createStatement)
|
boolean autoCommit, boolean lazyConnection, final boolean createStatement) throws Exception {
|
||||||
throws Exception {
|
|
||||||
if (lazyConnection) {
|
if (lazyConnection) {
|
||||||
given(con.getAutoCommit()).willReturn(autoCommit);
|
given(con.getAutoCommit()).willReturn(autoCommit);
|
||||||
given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED);
|
given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED);
|
||||||
|
@ -198,10 +199,10 @@ public class DataSourceTransactionManagerTests {
|
||||||
|
|
||||||
private void doTestTransactionRollbackRestoringAutoCommit(
|
private void doTestTransactionRollbackRestoringAutoCommit(
|
||||||
boolean autoCommit, boolean lazyConnection, final boolean createStatement) throws Exception {
|
boolean autoCommit, boolean lazyConnection, final boolean createStatement) throws Exception {
|
||||||
|
|
||||||
if (lazyConnection) {
|
if (lazyConnection) {
|
||||||
given(con.getAutoCommit()).willReturn(autoCommit);
|
given(con.getAutoCommit()).willReturn(autoCommit);
|
||||||
given(con.getTransactionIsolation()).willReturn(
|
given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED);
|
||||||
Connection.TRANSACTION_READ_COMMITTED);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lazyConnection || createStatement) {
|
if (!lazyConnection || createStatement) {
|
||||||
|
@ -835,6 +836,37 @@ public class DataSourceTransactionManagerTests {
|
||||||
verify(con).close();
|
verify(con).close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTransactionWithEnforceReadOnly() throws Exception {
|
||||||
|
tm.setEnforceReadOnly(true);
|
||||||
|
|
||||||
|
given(con.getAutoCommit()).willReturn(true);
|
||||||
|
Statement stmt = mock(Statement.class);
|
||||||
|
given(con.createStatement()).willReturn(stmt);
|
||||||
|
|
||||||
|
TransactionTemplate tt = new TransactionTemplate(tm);
|
||||||
|
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
|
||||||
|
tt.setReadOnly(true);
|
||||||
|
assertTrue("Hasn't thread connection", !TransactionSynchronizationManager.hasResource(ds));
|
||||||
|
tt.execute(new TransactionCallbackWithoutResult() {
|
||||||
|
@Override
|
||||||
|
protected void doInTransactionWithoutResult(TransactionStatus status) {
|
||||||
|
assertTrue(TransactionSynchronizationManager.isCurrentTransactionReadOnly());
|
||||||
|
assertTrue(TransactionSynchronizationManager.isActualTransactionActive());
|
||||||
|
// something transactional
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertTrue("Hasn't thread connection", !TransactionSynchronizationManager.hasResource(ds));
|
||||||
|
InOrder ordered = inOrder(con, stmt);
|
||||||
|
ordered.verify(con).setAutoCommit(false);
|
||||||
|
ordered.verify(stmt).executeUpdate("SET TRANSACTION READ ONLY");
|
||||||
|
ordered.verify(stmt).close();
|
||||||
|
ordered.verify(con).commit();
|
||||||
|
ordered.verify(con).setAutoCommit(true);
|
||||||
|
ordered.verify(con).close();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTransactionWithLongTimeout() throws Exception {
|
public void testTransactionWithLongTimeout() throws Exception {
|
||||||
doTestTransactionWithTimeout(10);
|
doTestTransactionWithTimeout(10);
|
||||||
|
|
Loading…
Reference in New Issue