From 02195f5abfdfe6e99588446bbcad3046113bcacb Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 2 Feb 2017 20:06:28 +0100 Subject: [PATCH] Support for read-only transactions through explicit SET TRANSACTION statement Issue: SPR-15210 --- .../DataSourceTransactionManager.java | 77 +++++++++++++++++-- .../DataSourceTransactionManagerTests.java | 44 +++++++++-- 2 files changed, 108 insertions(+), 13 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java index 839412a010..6119b47ca5 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java @@ -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"); * 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.SQLException; +import java.sql.Statement; import javax.sql.DataSource; import org.springframework.beans.factory.InitializingBean; @@ -111,6 +112,8 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan private DataSource dataSource; + private boolean enforceReadOnly = false; + /** * Create a new DataSourceTransactionManager instance. @@ -168,6 +171,38 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan 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. + *

The exact treatment, including any SQL statement executed on the connection, + * can be customized through through {@link #prepareTransactionalConnection}. + *

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. + *

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 public void afterPropertiesSet() { if (getDataSource() == null) { @@ -231,6 +266,8 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan } con.setAutoCommit(false); } + + prepareTransactionalConnection(con, definition); txObject.getConnectionHolder().setTransactionActive(true); int timeout = determineTimeout(definition); @@ -238,7 +275,7 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan txObject.getConnectionHolder().setTimeoutInSeconds(timeout); } - // Bind the session holder to the thread. + // Bind the connection holder to the thread. if (txObject.isNewConnectionHolder()) { TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder()); } @@ -257,15 +294,12 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan protected Object doSuspend(Object transaction) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; txObject.setConnectionHolder(null); - ConnectionHolder conHolder = (ConnectionHolder) - TransactionSynchronizationManager.unbindResource(this.dataSource); - return conHolder; + return TransactionSynchronizationManager.unbindResource(this.dataSource); } @Override protected void doResume(Object transaction, Object suspendedResources) { - ConnectionHolder conHolder = (ConnectionHolder) suspendedResources; - TransactionSynchronizationManager.bindResource(this.dataSource, conHolder); + TransactionSynchronizationManager.bindResource(this.dataSource, suspendedResources); } @Override @@ -340,6 +374,35 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionMan } + /** + * Prepare the transactional {@code Connection} right after transaction begin. + *

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. + *

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. * Used as transaction object by DataSourceTransactionManager. diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java index 211445a15f..578dc8e9b0 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceTransactionManagerTests.java @@ -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"); * 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.SQLException; import java.sql.Savepoint; +import java.sql.Statement; import javax.sql.DataSource; import org.junit.After; @@ -66,7 +67,7 @@ public class DataSourceTransactionManagerTests { @Before public void setUp() throws Exception { con = mock(Connection.class); - ds = mock(DataSource.class); + ds = mock(DataSource.class); tm = new DataSourceTransactionManager(ds); given(ds.getConnection()).willReturn(con); } @@ -111,8 +112,8 @@ public class DataSourceTransactionManagerTests { } private void doTestTransactionCommitRestoringAutoCommit( - boolean autoCommit, boolean lazyConnection, final boolean createStatement) - throws Exception { + boolean autoCommit, boolean lazyConnection, final boolean createStatement) throws Exception { + if (lazyConnection) { given(con.getAutoCommit()).willReturn(autoCommit); given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED); @@ -198,10 +199,10 @@ public class DataSourceTransactionManagerTests { private void doTestTransactionRollbackRestoringAutoCommit( boolean autoCommit, boolean lazyConnection, final boolean createStatement) throws Exception { + if (lazyConnection) { given(con.getAutoCommit()).willReturn(autoCommit); - given(con.getTransactionIsolation()).willReturn( - Connection.TRANSACTION_READ_COMMITTED); + given(con.getTransactionIsolation()).willReturn(Connection.TRANSACTION_READ_COMMITTED); } if (!lazyConnection || createStatement) { @@ -835,6 +836,37 @@ public class DataSourceTransactionManagerTests { 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 public void testTransactionWithLongTimeout() throws Exception { doTestTransactionWithTimeout(10);