From f24ed4105174727765bb15f4aae550cd4e5d839d Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Fri, 2 May 2014 18:25:40 +0200 Subject: [PATCH] [#3229] Add DSLContext.transaction(Transactional) to implement nested transaction semantics through functional interfaces --- .../test/_/testcases/TransactionTests.java | 274 ++++++++++++++++++ .../java/org/jooq/test/jOOQAbstractTest.java | 21 ++ .../src/main/java/org/jooq/Configuration.java | 33 +++ jOOQ/src/main/java/org/jooq/DSLContext.java | 15 + jOOQ/src/main/java/org/jooq/Transaction.java | 50 ++++ .../java/org/jooq/TransactionProvider.java | 108 +++++++ .../src/main/java/org/jooq/Transactional.java | 67 +++++ .../org/jooq/impl/DefaultConfiguration.java | 65 ++++- .../java/org/jooq/impl/DefaultDSLContext.java | 42 ++- .../jooq/impl/DefaultTransactionProvider.java | 153 ++++++++++ .../org/jooq/impl/NoTransactionProvider.java | 68 +++++ jOOQ/src/main/java/org/jooq/impl/Utils.java | 13 + 12 files changed, 904 insertions(+), 5 deletions(-) create mode 100644 jOOQ-test/src/test/java/org/jooq/test/_/testcases/TransactionTests.java create mode 100644 jOOQ/src/main/java/org/jooq/Transaction.java create mode 100644 jOOQ/src/main/java/org/jooq/TransactionProvider.java create mode 100644 jOOQ/src/main/java/org/jooq/Transactional.java create mode 100644 jOOQ/src/main/java/org/jooq/impl/DefaultTransactionProvider.java create mode 100644 jOOQ/src/main/java/org/jooq/impl/NoTransactionProvider.java diff --git a/jOOQ-test/src/test/java/org/jooq/test/_/testcases/TransactionTests.java b/jOOQ-test/src/test/java/org/jooq/test/_/testcases/TransactionTests.java new file mode 100644 index 0000000000..511e9203e9 --- /dev/null +++ b/jOOQ-test/src/test/java/org/jooq/test/_/testcases/TransactionTests.java @@ -0,0 +1,274 @@ +/** + * Copyright (c) 2009-2014, Data Geekery GmbH (http://www.datageekery.com) + * All rights reserved. + * + * This work is dual-licensed + * - under the Apache Software License 2.0 (the "ASL") + * - under the jOOQ License and Maintenance Agreement (the "jOOQ License") + * ============================================================================= + * You may choose which license applies to you: + * + * - If you're using this work with Open Source databases, you may choose + * either ASL or jOOQ License. + * - If you're using this work with at least one commercial database, you must + * choose jOOQ License + * + * For more information, please visit http://www.jooq.org/licenses + * + * Apache Software License 2.0: + * ----------------------------------------------------------------------------- + * 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 + * + * http://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. + * + * jOOQ License and Maintenance Agreement: + * ----------------------------------------------------------------------------- + * Data Geekery grants the Customer the non-exclusive, timely limited and + * non-transferable license to install and use the Software under the terms of + * the jOOQ License and Maintenance Agreement. + * + * This library is distributed with a LIMITED WARRANTY. See the jOOQ License + * and Maintenance Agreement for more details: http://www.jooq.org/licensing + */ +package org.jooq.test._.testcases; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.sql.Date; + +import org.jooq.Configuration; +import org.jooq.Record1; +import org.jooq.Record2; +import org.jooq.Record3; +import org.jooq.Record6; +import org.jooq.TableRecord; +import org.jooq.Transactional; +import org.jooq.UpdatableRecord; +import org.jooq.exception.DataAccessException; +import org.jooq.impl.DSL; +import org.jooq.impl.DefaultConnectionProvider; +import org.jooq.test.BaseTest; +import org.jooq.test.jOOQAbstractTest; + +/** + * @author Lukas Eder + */ +@SuppressWarnings("serial") +public class TransactionTests< + A extends UpdatableRecord & Record6, + AP, + B extends UpdatableRecord, + S extends UpdatableRecord & Record1, + B2S extends UpdatableRecord & Record3, + BS extends UpdatableRecord, + L extends TableRecord & Record2, + X extends TableRecord, + DATE extends UpdatableRecord, + BOOL extends UpdatableRecord, + D extends UpdatableRecord, + T extends UpdatableRecord, + U extends TableRecord, + UU extends UpdatableRecord, + I extends TableRecord, + IPK extends UpdatableRecord, + T725 extends UpdatableRecord, + T639 extends UpdatableRecord, + T785 extends TableRecord, + CASE extends UpdatableRecord> +extends BaseTest { + + public TransactionTests(jOOQAbstractTest delegate) { + super(delegate); + } + + static class MyRuntimeException extends RuntimeException { + public MyRuntimeException(String message) { + super(message); + } + } + + static class MyCheckedException extends Exception { + public MyCheckedException(String message) { + super(message); + } + } + + public void testTransactionsWithJDBCSimple() throws Exception { + try { + create().transaction(new Transactional() { + @Override + public Integer run(Configuration configuration) { + assertFalse(((DefaultConnectionProvider) configuration.connectionProvider()).getAutoCommit()); + + assertEquals(1, + DSL.using(configuration) + .insertInto(TAuthor(), TAuthor_ID(), TAuthor_LAST_NAME()) + .values(3, "Koontz") + .execute()); + + throw new MyRuntimeException("No"); + } + }); + + fail(); + } + catch (MyRuntimeException expected) { + assertEquals("No", expected.getMessage()); + assertEquals(2, create().fetchCount(TAuthor())); + assertTrue(create().configuration().connectionProvider().acquire().getAutoCommit()); + } + + assertEquals(2, create().fetchCount(TAuthor())); + } + + public void testTransactionsWithJDBCCheckedException() throws Exception { + try { + create().transaction(new Transactional() { + @Override + public Integer run(Configuration configuration) throws MyCheckedException { + assertFalse(((DefaultConnectionProvider) configuration.connectionProvider()).getAutoCommit()); + + assertEquals(1, + DSL.using(configuration) + .insertInto(TAuthor(), TAuthor_ID(), TAuthor_LAST_NAME()) + .values(3, "Koontz") + .execute()); + + throw new MyCheckedException("No"); + } + }); + + fail(); + } + catch (DataAccessException expected) { + assertEquals(MyCheckedException.class, expected.getCause().getClass()); + assertEquals("No", expected.getCause().getMessage()); + assertEquals(2, create().fetchCount(TAuthor())); + assertTrue(create().configuration().connectionProvider().acquire().getAutoCommit()); + } + + assertEquals(2, create().fetchCount(TAuthor())); + } + + public void testTransactionsWithJDBCNestedWithSavepoints() throws Exception { + jOOQAbstractTest.reset = false; + + final int[] inserted = new int[1]; + final int[] updated = new int[1]; + + Integer result = + create().transaction(new Transactional() { + @Override + public Integer run(Configuration c1) throws MyCheckedException { + assertFalse(((DefaultConnectionProvider) c1.connectionProvider()).getAutoCommit()); + + inserted[0] = + DSL.using(c1) + .insertInto(TAuthor(), TAuthor_ID(), TAuthor_LAST_NAME()) + .values(3, "Koontz") + .execute(); + + assertEquals(1, inserted[0]); + + // Implicit savepoint here + try { + DSL.using(c1).transaction(new Transactional() { + + @Override + public Integer run(Configuration c2) throws Exception { + assertFalse(((DefaultConnectionProvider) c2.connectionProvider()).getAutoCommit()); + + updated[0] = + DSL.using(c2) + .update(TAuthor()) + .set(TAuthor_FIRST_NAME(), "Dean") + .where(TAuthor_ID().eq(3)) + .execute(); + + assertEquals(1, updated[0]); + + throw new MyRuntimeException("No"); + } + }); + } + + // Rollback to savepoint must have happened + catch (MyRuntimeException expected) { + assertNull(DSL.using(c1).fetchOne(TAuthor(), TAuthor_ID().eq(3)).getValue(TAuthor_FIRST_NAME())); + assertEquals(MyRuntimeException.class, expected.getClass()); + assertEquals("No", expected.getMessage()); + } + + return 42; + } + }); + + assertEquals(3, create().fetchCount(TAuthor())); + assertEquals(42, (int) result); + } + + + public void testTransactionsWithJDBCNestedWithoutSavepoints() throws Exception { + final int[] inserted = new int[1]; + final int[] updated = new int[1]; + + try { + create().transaction(new Transactional() { + @Override + public Integer run(Configuration c1) throws MyCheckedException { + assertFalse(((DefaultConnectionProvider) c1.connectionProvider()).getAutoCommit()); + + inserted[0] = + DSL.using(c1) + .insertInto(TAuthor(), TAuthor_ID(), TAuthor_LAST_NAME()) + .values(3, "Koontz") + .execute(); + + assertEquals(1, inserted[0]); + + // No savepoint here + DSL.using(c1).transaction(new Transactional() { + + @Override + public Integer run(Configuration c2) throws Exception { + assertFalse(((DefaultConnectionProvider) c2.connectionProvider()).getAutoCommit()); + + updated[0] = + DSL.using(c2) + .update(TAuthor()) + .set(TAuthor_FIRST_NAME(), "Dean") + .where(TAuthor_ID().eq(3)) + .execute(); + + assertEquals(1, updated[0]); + + throw new MyRuntimeException("No"); + } + }); + + // This code should never be reached, as exception should propagate + fail(); + return 42; + } + }); + } + + catch (MyRuntimeException expected) { + assertEquals(2, create().fetchCount(TAuthor())); + assertEquals(MyRuntimeException.class, expected.getClass()); + assertEquals("No", expected.getMessage()); + } + } +} diff --git a/jOOQ-test/src/test/java/org/jooq/test/jOOQAbstractTest.java b/jOOQ-test/src/test/java/org/jooq/test/jOOQAbstractTest.java index 064d2b369c..8517f966e8 100644 --- a/jOOQ-test/src/test/java/org/jooq/test/jOOQAbstractTest.java +++ b/jOOQ-test/src/test/java/org/jooq/test/jOOQAbstractTest.java @@ -150,6 +150,7 @@ import org.jooq.test._.testcases.SelectTests; import org.jooq.test._.testcases.StatementTests; import org.jooq.test._.testcases.TableFunctionTests; import org.jooq.test._.testcases.ThreadSafetyTests; +import org.jooq.test._.testcases.TransactionTests; import org.jooq.test._.testcases.TruncateTests; import org.jooq.test._.testcases.ValuesConstructorTests; import org.jooq.test._.testcases.VisitListenerTests; @@ -2701,4 +2702,24 @@ public abstract class jOOQAbstractTest< public void testDialectGuessing() throws Exception { new JDBCTests(this).testDialectGuessing(); } + + @Test + public void testTransactionsWithJDBCSimple() throws Exception { + new TransactionTests(this).testTransactionsWithJDBCSimple(); + } + + @Test + public void testTransactionsWithJDBCCheckedException() throws Exception { + new TransactionTests(this).testTransactionsWithJDBCCheckedException(); + } + + @Test + public void testTransactionsWithJDBCNestedWithSavepoints() throws Exception { + new TransactionTests(this).testTransactionsWithJDBCNestedWithSavepoints(); + } + + @Test + public void testTransactionsWithJDBCNestedWithoutSavepoints() throws Exception { + new TransactionTests(this).testTransactionsWithJDBCNestedWithoutSavepoints(); + } } diff --git a/jOOQ/src/main/java/org/jooq/Configuration.java b/jOOQ/src/main/java/org/jooq/Configuration.java index 33ba9e8921..8af76574b8 100644 --- a/jOOQ/src/main/java/org/jooq/Configuration.java +++ b/jOOQ/src/main/java/org/jooq/Configuration.java @@ -44,6 +44,8 @@ import java.io.Serializable; import java.util.Map; import org.jooq.conf.Settings; +import org.jooq.impl.DefaultConnectionProvider; +import org.jooq.impl.DefaultTransactionProvider; /** * A Configuration configures a {@link DSLContext}, providing it @@ -139,6 +141,15 @@ public interface Configuration extends Serializable { */ ConnectionProvider connectionProvider(); + /** + * Get this configuration's underlying transaction provider. + *

+ * If no explicit transaction provider was specified, and if + * {@link #connectionProvider()} is a {@link DefaultConnectionProvider}, + * then this will return a {@link DefaultTransactionProvider}. + */ + TransactionProvider transactionProvider(); + /** * Get this configuration's underlying record mapper provider. */ @@ -237,6 +248,18 @@ public interface Configuration extends Serializable { */ Configuration set(ConnectionProvider newConnectionProvider); + /** + * Change this configuration to hold a new transaction provider. + *

+ * This method is not thread-safe and should not be used in globally + * available Configuration objects. + * + * @param newTransactionProvider The new transaction provider to be + * contained in the changed configuration. + * @return The changed configuration. + */ + Configuration set(TransactionProvider newTransactionProvider); + /** * Change this configuration to hold a new record mapper provider. *

@@ -331,6 +354,16 @@ public interface Configuration extends Serializable { */ Configuration derive(ConnectionProvider newConnectionProvider); + /** + * Create a derived configuration from this one, with a new transaction + * provider. + * + * @param newTransactionProvider The new transaction provider to be + * contained in the derived configuration. + * @return The derived configuration. + */ + Configuration derive(TransactionProvider newTransactionProvider); + /** * Create a derived configuration from this one, with a new record mapper * provider. diff --git a/jOOQ/src/main/java/org/jooq/DSLContext.java b/jOOQ/src/main/java/org/jooq/DSLContext.java index ec9ca6f381..7363ca645f 100644 --- a/jOOQ/src/main/java/org/jooq/DSLContext.java +++ b/jOOQ/src/main/java/org/jooq/DSLContext.java @@ -161,6 +161,21 @@ public interface DSLContext { */ Meta meta(); + // ------------------------------------------------------------------------- + // XXX Transaction API + // ------------------------------------------------------------------------- + + /** + * Run a {@link Transactional} in the context of this + * DSLContext's underlying {@link #configuration()}'s + * {@link Configuration#transactionProvider()}, and return the + * transactional's outcome. + * + * @param transactional The transactional code + * @return The transactional outcome + */ + T transaction(Transactional transactional); + // ------------------------------------------------------------------------- // XXX RenderContext and BindContext accessors // ------------------------------------------------------------------------- diff --git a/jOOQ/src/main/java/org/jooq/Transaction.java b/jOOQ/src/main/java/org/jooq/Transaction.java new file mode 100644 index 0000000000..fcde44d449 --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/Transaction.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2009-2014, Data Geekery GmbH (http://www.datageekery.com) + * All rights reserved. + * + * This work is dual-licensed + * - under the Apache Software License 2.0 (the "ASL") + * - under the jOOQ License and Maintenance Agreement (the "jOOQ License") + * ============================================================================= + * You may choose which license applies to you: + * + * - If you're using this work with Open Source databases, you may choose + * either ASL or jOOQ License. + * - If you're using this work with at least one commercial database, you must + * choose jOOQ License + * + * For more information, please visit http://www.jooq.org/licenses + * + * Apache Software License 2.0: + * ----------------------------------------------------------------------------- + * 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 + * + * http://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. + * + * jOOQ License and Maintenance Agreement: + * ----------------------------------------------------------------------------- + * Data Geekery grants the Customer the non-exclusive, timely limited and + * non-transferable license to install and use the Software under the terms of + * the jOOQ License and Maintenance Agreement. + * + * This library is distributed with a LIMITED WARRANTY. See the jOOQ License + * and Maintenance Agreement for more details: http://www.jooq.org/licensing + */ +package org.jooq; + +/** + * A custom transaction object. + * + * @author Lukas Eder + */ +public interface Transaction { + +} diff --git a/jOOQ/src/main/java/org/jooq/TransactionProvider.java b/jOOQ/src/main/java/org/jooq/TransactionProvider.java new file mode 100644 index 0000000000..0615914b67 --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/TransactionProvider.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2009-2014, Data Geekery GmbH (http://www.datageekery.com) + * All rights reserved. + * + * This work is dual-licensed + * - under the Apache Software License 2.0 (the "ASL") + * - under the jOOQ License and Maintenance Agreement (the "jOOQ License") + * ============================================================================= + * You may choose which license applies to you: + * + * - If you're using this work with Open Source databases, you may choose + * either ASL or jOOQ License. + * - If you're using this work with at least one commercial database, you must + * choose jOOQ License + * + * For more information, please visit http://www.jooq.org/licenses + * + * Apache Software License 2.0: + * ----------------------------------------------------------------------------- + * 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 + * + * http://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. + * + * jOOQ License and Maintenance Agreement: + * ----------------------------------------------------------------------------- + * Data Geekery grants the Customer the non-exclusive, timely limited and + * non-transferable license to install and use the Software under the terms of + * the jOOQ License and Maintenance Agreement. + * + * This library is distributed with a LIMITED WARRANTY. See the jOOQ License + * and Maintenance Agreement for more details: http://www.jooq.org/licensing + */ +package org.jooq; + +import java.sql.Savepoint; + +import org.jooq.exception.DataAccessException; +import org.jooq.impl.DefaultTransactionProvider; + +/** + * The TransactionProvider SPI can be used to implement custom + * transaction behaviour that is applied when calling + * {@link DSLContext#transaction(Transactional)}. + *

+ * A new {@link Configuration} copy is created from the calling + * {@link DSLContext} for the scope of a single transactions. Implementors may + * freely add custom data to {@link Configuration#data()}, in order to share + * information between {@link #begin(Configuration)} and + * {@link #commit(Configuration, Transaction)} or + * {@link #rollback(Configuration, Transaction, Exception)}, as well as to share + * information with nested transactions. + *

+ * Implementors may freely choose whether they support nested transactions. An + * example implementation supporting nested transactions is + * {@link DefaultTransactionProvider}, which implements such transactions using + * JDBC {@link Savepoint}s. + * + * @author Lukas Eder + */ +public interface TransactionProvider { + + /** + * Begin a new transaction. + *

+ * This method begins a new transaction with a {@link Configuration} scoped + * for this transaction. The resulting {@link Transaction} object may be + * used by implementors to identify the transaction when + * {@link #commit(Configuration, Transaction)} or + * {@link #rollback(Configuration, Transaction, Exception)} is called. + * + * @param Configuration the configuration scoped to this transaction and its + * nested transactions. + * @return A user-defined transaction object. May be null. + * @throws DataAccessException Any exception issued by the underlying + * database. + */ + Transaction begin(Configuration configuration) throws DataAccessException; + + /** + * @param Configuration the configuration scoped to this transaction and its + * nested transactions. + * @param transaction The user-defined transaction object returned from + * {@link #begin(Configuration)}. May be null. + * @throws DataAccessException Any exception issued by the underlying + * database. + */ + void commit(Configuration configuration, Transaction transaction) throws DataAccessException; + + /** + * @param Configuration the configuration scoped to this transaction and its + * nested transactions. + * @param transaction The user-defined transaction object returned from + * {@link #begin(Configuration)}. May be null. + * @param cause The exception that has caused the rollback. + * @throws DataAccessException Any exception issued by the underlying + * database. + */ + void rollback(Configuration configuration, Transaction transaction, Exception cause) throws DataAccessException; + +} diff --git a/jOOQ/src/main/java/org/jooq/Transactional.java b/jOOQ/src/main/java/org/jooq/Transactional.java new file mode 100644 index 0000000000..2671994db4 --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/Transactional.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2009-2014, Data Geekery GmbH (http://www.datageekery.com) + * All rights reserved. + * + * This work is dual-licensed + * - under the Apache Software License 2.0 (the "ASL") + * - under the jOOQ License and Maintenance Agreement (the "jOOQ License") + * ============================================================================= + * You may choose which license applies to you: + * + * - If you're using this work with Open Source databases, you may choose + * either ASL or jOOQ License. + * - If you're using this work with at least one commercial database, you must + * choose jOOQ License + * + * For more information, please visit http://www.jooq.org/licenses + * + * Apache Software License 2.0: + * ----------------------------------------------------------------------------- + * 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 + * + * http://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. + * + * jOOQ License and Maintenance Agreement: + * ----------------------------------------------------------------------------- + * Data Geekery grants the Customer the non-exclusive, timely limited and + * non-transferable license to install and use the Software under the terms of + * the jOOQ License and Maintenance Agreement. + * + * This library is distributed with a LIMITED WARRANTY. See the jOOQ License + * and Maintenance Agreement for more details: http://www.jooq.org/licensing + */ +package org.jooq; + +/** + * An FunctionalInterface that wraps transactional code. + * + * @author Lukas Eder + */ +public interface Transactional { + + /** + * Run the transactional code. + *

+ * If this method completes normally, and this is not a nested transaction, + * then the transaction will be committed. If this method completes with an + * exception, then the transaction is rolled back to the beginning of this + * Transactional. + * + * @param configuration The Configuration in whose context the + * transaction is run. + * @return The outcome of the transaction. + * @throws Exception Any exception that will cause a rollback of the code + * contained in this transaction. If this is a nested + * transaction, the rollback may be performed only to the state + * before executing this Transactional. + */ + T run(Configuration configuration) throws Exception; +} diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultConfiguration.java b/jOOQ/src/main/java/org/jooq/impl/DefaultConfiguration.java index f9290413e2..d20850e795 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultConfiguration.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultConfiguration.java @@ -62,6 +62,7 @@ import org.jooq.RecordListenerProvider; import org.jooq.RecordMapperProvider; import org.jooq.SQLDialect; import org.jooq.SchemaMapping; +import org.jooq.TransactionProvider; import org.jooq.VisitListenerProvider; import org.jooq.conf.Settings; import org.jooq.conf.SettingsTools; @@ -89,6 +90,7 @@ public class DefaultConfiguration implements Configuration { // Non-serializable Configuration objects private transient ConnectionProvider connectionProvider; + private transient TransactionProvider transactionProvider; private transient RecordMapperProvider recordMapperProvider; private transient RecordListenerProvider[] recordListenerProviders; private transient ExecuteListenerProvider[] executeListenerProviders; @@ -128,6 +130,7 @@ public class DefaultConfiguration implements Configuration { null, null, null, + null, dialect, SettingsTools.defaultSettings(), null @@ -145,6 +148,7 @@ public class DefaultConfiguration implements Configuration { DefaultConfiguration(Configuration configuration) { this( configuration.connectionProvider(), + configuration.transactionProvider(), configuration.recordMapperProvider(), configuration.recordListenerProviders(), configuration.executeListenerProviders(), @@ -165,6 +169,7 @@ public class DefaultConfiguration implements Configuration { */ DefaultConfiguration( ConnectionProvider connectionProvider, + TransactionProvider transactionProvider, RecordMapperProvider recordMapperProvider, RecordListenerProvider[] recordListenerProviders, ExecuteListenerProvider[] executeListenerProviders, @@ -174,6 +179,7 @@ public class DefaultConfiguration implements Configuration { Map data) { set(connectionProvider); + set(transactionProvider); set(recordMapperProvider); set(recordListenerProviders); set(executeListenerProviders); @@ -221,6 +227,25 @@ public class DefaultConfiguration implements Configuration { public final Configuration derive(ConnectionProvider newConnectionProvider) { return new DefaultConfiguration( newConnectionProvider, + transactionProvider, + recordMapperProvider, + recordListenerProviders, + executeListenerProviders, + visitListenerProviders, + dialect, + settings, + data + ); + } + + /** + * {@inheritDoc} + */ + @Override + public final Configuration derive(TransactionProvider newTransactionProvider) { + return new DefaultConfiguration( + connectionProvider, + newTransactionProvider, recordMapperProvider, recordListenerProviders, executeListenerProviders, @@ -238,6 +263,7 @@ public class DefaultConfiguration implements Configuration { public final Configuration derive(RecordMapperProvider newRecordMapperProvider) { return new DefaultConfiguration( connectionProvider, + transactionProvider, newRecordMapperProvider, recordListenerProviders, executeListenerProviders, @@ -255,6 +281,7 @@ public class DefaultConfiguration implements Configuration { public final Configuration derive(RecordListenerProvider... newRecordListenerProviders) { return new DefaultConfiguration( connectionProvider, + transactionProvider, recordMapperProvider, newRecordListenerProviders, executeListenerProviders, @@ -272,6 +299,7 @@ public class DefaultConfiguration implements Configuration { public final Configuration derive(ExecuteListenerProvider... newExecuteListenerProviders) { return new DefaultConfiguration( connectionProvider, + transactionProvider, recordMapperProvider, recordListenerProviders, newExecuteListenerProviders, @@ -289,6 +317,7 @@ public class DefaultConfiguration implements Configuration { public final Configuration derive(VisitListenerProvider... newVisitListenerProviders) { return new DefaultConfiguration( connectionProvider, + transactionProvider, recordMapperProvider, recordListenerProviders, executeListenerProviders, @@ -306,6 +335,7 @@ public class DefaultConfiguration implements Configuration { public final Configuration derive(SQLDialect newDialect) { return new DefaultConfiguration( connectionProvider, + transactionProvider, recordMapperProvider, recordListenerProviders, executeListenerProviders, @@ -323,6 +353,7 @@ public class DefaultConfiguration implements Configuration { public final Configuration derive(Settings newSettings) { return new DefaultConfiguration( connectionProvider, + transactionProvider, recordMapperProvider, recordListenerProviders, executeListenerProviders, @@ -365,6 +396,18 @@ public class DefaultConfiguration implements Configuration { return this; } + /** + * {@inheritDoc} + */ + @Override + public final Configuration set(TransactionProvider newTransactionProvider) { + this.transactionProvider = newTransactionProvider != null + ? newTransactionProvider + : new NoTransactionProvider(); + + return this; + } + /** * {@inheritDoc} */ @@ -447,6 +490,20 @@ public class DefaultConfiguration implements Configuration { return connectionProvider; } + /** + * {@inheritDoc} + */ + @Override + public final TransactionProvider transactionProvider() { + if (transactionProvider instanceof NoTransactionProvider && + connectionProvider instanceof DefaultConnectionProvider) { + + return new DefaultTransactionProvider((DefaultConnectionProvider) connectionProvider); + } + + return transactionProvider; + } + /** * {@inheritDoc} */ @@ -533,7 +590,9 @@ public class DefaultConfiguration implements Configuration { StringWriter writer = new StringWriter(); JAXB.marshal(settings, writer); - return "DefaultConfiguration [\n\tconnected=" + (connectionProvider != null && !(connectionProvider instanceof NoConnectionProvider)) + + return "DefaultConfiguration " + + "[\n\tconnected=" + (connectionProvider != null && !(connectionProvider instanceof NoConnectionProvider)) + + ",\n\ttransactional=" + (transactionProvider != null && !(transactionProvider instanceof NoTransactionProvider)) + ",\n\tdialect=" + dialect + ",\n\tdata=" + data + ",\n\tsettings=\n\t\t" + writer.toString().trim().replace("\n", "\n\t\t") + @@ -551,6 +610,9 @@ public class DefaultConfiguration implements Configuration { oos.writeObject(connectionProvider instanceof Serializable ? connectionProvider : null); + oos.writeObject(transactionProvider instanceof Serializable + ? transactionProvider + : null); oos.writeObject(recordMapperProvider instanceof Serializable ? recordMapperProvider : null); @@ -576,6 +638,7 @@ public class DefaultConfiguration implements Configuration { ois.defaultReadObject(); connectionProvider = (ConnectionProvider) ois.readObject(); + transactionProvider = (TransactionProvider) ois.readObject(); recordMapperProvider = (RecordMapperProvider) ois.readObject(); executeListenerProviders = (ExecuteListenerProvider[]) ois.readObject(); recordListenerProviders = (RecordListenerProvider[]) ois.readObject(); diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultDSLContext.java b/jOOQ/src/main/java/org/jooq/impl/DefaultDSLContext.java index 60db31b268..6158052f5f 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultDSLContext.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultDSLContext.java @@ -176,6 +176,9 @@ import org.jooq.Sequence; import org.jooq.Table; import org.jooq.TableLike; import org.jooq.TableRecord; +import org.jooq.Transaction; +import org.jooq.TransactionProvider; +import org.jooq.Transactional; import org.jooq.TruncateIdentityStep; import org.jooq.UDT; import org.jooq.UDTRecord; @@ -218,7 +221,7 @@ public class DefaultDSLContext implements DSLContext, Serializable { } public DefaultDSLContext(SQLDialect dialect, Settings settings) { - this(new DefaultConfiguration(new NoConnectionProvider(), null, null, null, null, dialect, settings, null)); + this(new DefaultConfiguration(new NoConnectionProvider(), null, null, null, null, null, dialect, settings, null)); } public DefaultDSLContext(Connection connection, SQLDialect dialect) { @@ -226,7 +229,7 @@ public class DefaultDSLContext implements DSLContext, Serializable { } public DefaultDSLContext(Connection connection, SQLDialect dialect, Settings settings) { - this(new DefaultConfiguration(new DefaultConnectionProvider(connection), null, null, null, null, dialect, settings, null)); + this(new DefaultConfiguration(new DefaultConnectionProvider(connection), null, null, null, null, null, dialect, settings, null)); } public DefaultDSLContext(DataSource datasource, SQLDialect dialect) { @@ -234,7 +237,7 @@ public class DefaultDSLContext implements DSLContext, Serializable { } public DefaultDSLContext(DataSource datasource, SQLDialect dialect, Settings settings) { - this(new DefaultConfiguration(new DataSourceConnectionProvider(datasource), null, null, null, null, dialect, settings, null)); + this(new DefaultConfiguration(new DataSourceConnectionProvider(datasource), null, null, null, null, null, dialect, settings, null)); } public DefaultDSLContext(ConnectionProvider connectionProvider, SQLDialect dialect) { @@ -242,7 +245,7 @@ public class DefaultDSLContext implements DSLContext, Serializable { } public DefaultDSLContext(ConnectionProvider connectionProvider, SQLDialect dialect, Settings settings) { - this(new DefaultConfiguration(connectionProvider, null, null, null, null, dialect, settings, null)); + this(new DefaultConfiguration(connectionProvider, null, null, null, null, null, dialect, settings, null)); } public DefaultDSLContext(Configuration configuration) { @@ -284,6 +287,37 @@ public class DefaultDSLContext implements DSLContext, Serializable { return new MetaImpl(configuration); } + // ------------------------------------------------------------------------- + // XXX Transaction API + // ------------------------------------------------------------------------- + + @Override + public T transaction(Transactional transactional) { + Configuration local = configuration.derive(); + TransactionProvider provider = local.transactionProvider(); + + Transaction transaction = null; + T result = null; + + try { + transaction = provider.begin(local); + result = transactional.run(local); + provider.commit(local, transaction); + } + catch (Exception cause) { + provider.rollback(local, transaction, cause); + + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + else { + throw new DataAccessException("Rollback caused", cause); + } + } + + return result; + } + // ------------------------------------------------------------------------- // XXX RenderContext and BindContext accessors // ------------------------------------------------------------------------- diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultTransactionProvider.java b/jOOQ/src/main/java/org/jooq/impl/DefaultTransactionProvider.java new file mode 100644 index 0000000000..ba95290cb5 --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultTransactionProvider.java @@ -0,0 +1,153 @@ +/** + * Copyright (c) 2009-2014, Data Geekery GmbH (http://www.datageekery.com) + * All rights reserved. + * + * This work is dual-licensed + * - under the Apache Software License 2.0 (the "ASL") + * - under the jOOQ License and Maintenance Agreement (the "jOOQ License") + * ============================================================================= + * You may choose which license applies to you: + * + * - If you're using this work with Open Source databases, you may choose + * either ASL or jOOQ License. + * - If you're using this work with at least one commercial database, you must + * choose jOOQ License + * + * For more information, please visit http://www.jooq.org/licenses + * + * Apache Software License 2.0: + * ----------------------------------------------------------------------------- + * 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 + * + * http://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. + * + * jOOQ License and Maintenance Agreement: + * ----------------------------------------------------------------------------- + * Data Geekery grants the Customer the non-exclusive, timely limited and + * non-transferable license to install and use the Software under the terms of + * the jOOQ License and Maintenance Agreement. + * + * This library is distributed with a LIMITED WARRANTY. See the jOOQ License + * and Maintenance Agreement for more details: http://www.jooq.org/licensing + */ +package org.jooq.impl; + +import static org.jooq.impl.Utils.DATA_DEFAULT_TRANSACTION_PROVIDER_AUTOCOMMIT; +import static org.jooq.impl.Utils.DATA_DEFAULT_TRANSACTION_PROVIDER_SAVEPOINTS; + +import java.sql.Connection; +import java.sql.Savepoint; +import java.util.Stack; + +import org.jooq.Configuration; +import org.jooq.Transaction; +import org.jooq.TransactionProvider; + +/** + * A default implementation for the {@link TransactionProvider} SPI. + *

+ * This implementation is entirely based on JDBC transactions and is intended to + * work with {@link DefaultConnectionProvider} (which is implicitly created when + * using {@link DSL#using(Connection)}). It supports nested transactions by + * modelling them implicitly with JDBC {@link Savepoint}s, if supported by the + * underlying JDBC driver. + * + * @author Lukas Eder + */ +public class DefaultTransactionProvider implements TransactionProvider { + + private final DefaultConnectionProvider connection; + + public DefaultTransactionProvider(DefaultConnectionProvider connection) { + this.connection = connection; + } + + @SuppressWarnings("unchecked") + private final Stack savepoints(Configuration configuration) { + Stack savepoints = (Stack) configuration.data(DATA_DEFAULT_TRANSACTION_PROVIDER_SAVEPOINTS); + + if (savepoints == null) { + savepoints = new Stack(); + configuration.data(DATA_DEFAULT_TRANSACTION_PROVIDER_SAVEPOINTS, savepoints); + } + + return savepoints; + } + + private final boolean autoCommit(Configuration configuration) { + Boolean autoCommit = (Boolean) configuration.data(DATA_DEFAULT_TRANSACTION_PROVIDER_AUTOCOMMIT); + + if (autoCommit == null) { + autoCommit = connection.getAutoCommit(); + configuration.data(DATA_DEFAULT_TRANSACTION_PROVIDER_AUTOCOMMIT, autoCommit); + } + + return autoCommit; + } + + @Override + public final Transaction begin(Configuration configuration) { + Stack savepoints = savepoints(configuration); + + // This is the top-level transaction + if (savepoints.isEmpty()) { + autoCommit(configuration, false); + } + + savepoints.push(connection.setSavepoint()); + return null; + } + + @Override + public final void commit(Configuration configuration, Transaction transaction) { + Stack savepoints = savepoints(configuration); + savepoints.pop(); + + // This is the top-level transaction + if (savepoints.isEmpty()) { + connection.commit(); + autoCommit(configuration, true); + } + + // Nested commits have no effect + else { + } + } + + @Override + public final void rollback(Configuration configuration, Transaction transaction, Exception cause) { + Stack savepoints = savepoints(configuration); + Savepoint savepoint = savepoints.pop(); + + try { + connection.rollback(savepoint); + } + + finally { + if (savepoints.isEmpty()) + autoCommit(configuration, true); + } + } + + /** + * Ensure an autoCommit value on the connection, if it was set + * to true, originally. + */ + private void autoCommit(Configuration configuration, boolean newValue) { + boolean oldValue = autoCommit(configuration); + + // Transactions cannot run with autoCommit = true. Change the value for + // the duration of a transaction + if (oldValue == true) { + connection.setAutoCommit(newValue); + } + } +} diff --git a/jOOQ/src/main/java/org/jooq/impl/NoTransactionProvider.java b/jOOQ/src/main/java/org/jooq/impl/NoTransactionProvider.java new file mode 100644 index 0000000000..e6d0194946 --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/impl/NoTransactionProvider.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2009-2014, Data Geekery GmbH (http://www.datageekery.com) + * All rights reserved. + * + * This work is dual-licensed + * - under the Apache Software License 2.0 (the "ASL") + * - under the jOOQ License and Maintenance Agreement (the "jOOQ License") + * ============================================================================= + * You may choose which license applies to you: + * + * - If you're using this work with Open Source databases, you may choose + * either ASL or jOOQ License. + * - If you're using this work with at least one commercial database, you must + * choose jOOQ License + * + * For more information, please visit http://www.jooq.org/licenses + * + * Apache Software License 2.0: + * ----------------------------------------------------------------------------- + * 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 + * + * http://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. + * + * jOOQ License and Maintenance Agreement: + * ----------------------------------------------------------------------------- + * Data Geekery grants the Customer the non-exclusive, timely limited and + * non-transferable license to install and use the Software under the terms of + * the jOOQ License and Maintenance Agreement. + * + * This library is distributed with a LIMITED WARRANTY. See the jOOQ License + * and Maintenance Agreement for more details: http://www.jooq.org/licensing + */ +package org.jooq.impl; + +import org.jooq.Configuration; +import org.jooq.Transaction; +import org.jooq.TransactionProvider; + +/** + * An "empty" implementation that is never transactional. + * + * @author Lukas Eder + */ +public class NoTransactionProvider implements TransactionProvider { + + @Override + public final Transaction begin(Configuration configuration) { + throw new UnsupportedOperationException("No transaction provider configured"); + } + + @Override + public final void commit(Configuration configuration, Transaction transaction) { + throw new UnsupportedOperationException("No transaction provider configured"); + } + + @Override + public final void rollback(Configuration configuration, Transaction transaction, Exception cause) { + throw new UnsupportedOperationException("No transaction provider configured"); + } +} diff --git a/jOOQ/src/main/java/org/jooq/impl/Utils.java b/jOOQ/src/main/java/org/jooq/impl/Utils.java index 7881dd21e7..4e16bb431a 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Utils.java +++ b/jOOQ/src/main/java/org/jooq/impl/Utils.java @@ -67,6 +67,7 @@ import java.sql.Array; import java.sql.Blob; import java.sql.CallableStatement; import java.sql.Clob; +import java.sql.Connection; import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -248,6 +249,18 @@ final class Utils { */ static final String DATA_RENDERING_DB2_FINAL_TABLE_CLAUSE = "org.jooq.configuration.rendering-db2-final-table-clause"; + /** + * [#1629] The {@link Connection#getAutoCommit()} flag value before starting + * a new transaction. + */ + static final String DATA_DEFAULT_TRANSACTION_PROVIDER_AUTOCOMMIT = "org.jooq.configuration.default-transaction-provider-autocommit"; + + /** + * [#1629] The {@link Connection#getAutoCommit()} flag value before starting + * a new transaction. + */ + static final String DATA_DEFAULT_TRANSACTION_PROVIDER_SAVEPOINTS = "org.jooq.configuration.default-transaction-provider-savepoints"; + /** * [#2965] These are {@link ConcurrentHashMap}s containing caches for * reflection information.