Repository: cayenne Updated Branches: refs/heads/master 8aaf787bf -> 42eda59bc
CAY-2112 Expose callback for "performInTransaction" Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/42eda59b Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/42eda59b Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/42eda59b Branch: refs/heads/master Commit: 42eda59bc1fefddf4920830aa0f142f95c995b23 Parents: 8aaf787 Author: Andrus Adamchik <and...@objectstyle.com> Authored: Mon Sep 19 10:23:13 2016 -0400 Committer: Andrus Adamchik <and...@objectstyle.com> Committed: Mon Sep 19 11:23:04 2016 -0400 ---------------------------------------------------------------------- .../configuration/server/ServerRuntime.java | 20 +++- .../org/apache/cayenne/tx/BaseTransaction.java | 35 ++++++- .../cayenne/tx/DefaultTransactionManager.java | 29 ++++-- .../tx/DoNothingTransactionListener.java | 51 +++++++++++ .../java/org/apache/cayenne/tx/Transaction.java | 4 +- .../apache/cayenne/tx/TransactionListener.java | 36 ++++++++ .../apache/cayenne/tx/TransactionManager.java | 21 ++++- .../cayenne/access/TransactionThreadIT.java | 6 ++ .../cayenne/access/UserTransactionIT.java | 8 +- .../configuration/server/ServerRuntimeIT.java | 96 ++++++++++++++++++++ .../cayenne/tx/DefaultTransactionManagerIT.java | 4 +- docs/doc/src/main/resources/RELEASE-NOTES.txt | 1 + 12 files changed, 291 insertions(+), 20 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cayenne/blob/42eda59b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/ServerRuntime.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/ServerRuntime.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/ServerRuntime.java index 6e4d322..dc71ae5 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/ServerRuntime.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/ServerRuntime.java @@ -18,16 +18,17 @@ ****************************************************************/ package org.apache.cayenne.configuration.server; -import javax.sql.DataSource; - import org.apache.cayenne.access.DataDomain; import org.apache.cayenne.access.DataNode; import org.apache.cayenne.configuration.CayenneRuntime; import org.apache.cayenne.configuration.ModuleCollection; import org.apache.cayenne.di.Module; +import org.apache.cayenne.tx.TransactionListener; import org.apache.cayenne.tx.TransactionManager; import org.apache.cayenne.tx.TransactionalOperation; +import javax.sql.DataSource; + /** * An object representing Cayenne server-stack that connects directly to the * database via JDBC. This is an entry point for user applications to access @@ -80,6 +81,21 @@ public class ServerRuntime extends CayenneRuntime { } /** + * Runs provided operation wrapped in a single transaction. Transaction + * handling delegated to the internal {@link TransactionManager}. Nested + * calls to 'performInTransaction' are safe and attached to the same + * in-progress transaction. TransactionalOperation can be some arbitrary + * user code, which most often than not will consist of multiple Cayenne + * operations. + * + * @since 4.0 + */ + public <T> T performInTransaction(TransactionalOperation<T> op, TransactionListener callback) { + TransactionManager tm = injector.getInstance(TransactionManager.class); + return tm.performInTransaction(op, callback); + } + + /** * Returns the main runtime DataDomain. Note that by default the returned * DataDomain is the same as the main DataChannel returned by * {@link #getChannel()}. Although users may redefine DataChannel provider http://git-wip-us.apache.org/repos/asf/cayenne/blob/42eda59b/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java index fb23db0..8ed44fc 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/BaseTransaction.java @@ -20,8 +20,10 @@ package org.apache.cayenne.tx; import java.sql.Connection; +import java.util.Collection; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.Map; /** @@ -45,6 +47,7 @@ public abstract class BaseTransaction implements Transaction { protected static final int STATUS_MARKED_ROLLEDBACK = 7; protected Map<String, Connection> connections; + protected Collection<TransactionListener> listeners; protected int status; static String decodeStatus(int status) { @@ -100,6 +103,15 @@ public abstract class BaseTransaction implements Transaction { return status == STATUS_MARKED_ROLLEDBACK; } + @Override + public void addListener(TransactionListener listener) { + if(listeners == null) { + listeners = new LinkedHashSet<>(); + } + + listeners.add(listener); + } + /** * Starts a Transaction. If Transaction is not started explicitly, it will * be started when the first connection is added. @@ -126,6 +138,12 @@ public abstract class BaseTransaction implements Transaction { + "Current status: " + BaseTransaction.decodeStatus(status)); } + if(listeners != null) { + for(TransactionListener listener : listeners) { + listener.willCommit(this); + } + } + processCommit(); status = BaseTransaction.STATUS_COMMITTED; @@ -139,6 +157,7 @@ public abstract class BaseTransaction implements Transaction { public void rollback() { try { + if (status == BaseTransaction.STATUS_NO_TRANSACTION || status == BaseTransaction.STATUS_ROLLEDBACK || status == BaseTransaction.STATUS_ROLLING_BACK) { return; @@ -150,6 +169,12 @@ public abstract class BaseTransaction implements Transaction { + "Current status: " + BaseTransaction.decodeStatus(status)); } + if(listeners != null) { + for(TransactionListener listener : listeners) { + listener.willRollback(this); + } + } + processRollback(); status = BaseTransaction.STATUS_ROLLEDBACK; @@ -167,13 +192,19 @@ public abstract class BaseTransaction implements Transaction { } @Override - public void addConnection(String name, Connection connection) { + public void addConnection(String connectionName, Connection connection) { + + if(listeners != null) { + for(TransactionListener listener : listeners) { + listener.willAddConnection(this, connectionName, connection); + } + } if (connections == null) { connections = new HashMap<>(); } - if (connections.put(name, connection) != connection) { + if (connections.put(connectionName, connection) != connection) { connectionAdded(connection); } } http://git-wip-us.apache.org/repos/asf/cayenne/blob/42eda59b/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionManager.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionManager.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionManager.java index b8c284c..61ed4b6 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionManager.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/DefaultTransactionManager.java @@ -37,23 +37,27 @@ public class DefaultTransactionManager implements TransactionManager { @Override public <T> T performInTransaction(TransactionalOperation<T> op) { + return performInTransaction(op, DoNothingTransactionListener.getInstance()); + } + + @Override + public <T> T performInTransaction(TransactionalOperation<T> op, TransactionListener callback) { + + // Either join existing tx (in such case do not try to commit or rollback), or start a new tx and manage it + // till the end - // join existing tx if it is in progress... in such case do not try to - // commit or roll it back Transaction currentTx = BaseTransaction.getThreadTransaction(); - if (currentTx != null) { - return op.perform(); - } + return (currentTx != null) + ? performInTransaction(currentTx, op, callback) + : performInLocalTransaction(op, callback); + } - // start a new tx and manage it till the end + protected <T> T performInLocalTransaction(TransactionalOperation<T> op, TransactionListener callback) { Transaction tx = txFactory.createTransaction(); BaseTransaction.bindThreadTransaction(tx); try { - - T result = op.perform(); - + T result = performInTransaction(tx, op, callback); tx.commit(); - return result; } catch (CayenneRuntimeException ex) { @@ -78,4 +82,9 @@ public class DefaultTransactionManager implements TransactionManager { } } + protected <T> T performInTransaction(Transaction tx, TransactionalOperation<T> op, TransactionListener callback) { + tx.addListener(callback); + return op.perform(); + } + } http://git-wip-us.apache.org/repos/asf/cayenne/blob/42eda59b/cayenne-server/src/main/java/org/apache/cayenne/tx/DoNothingTransactionListener.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/DoNothingTransactionListener.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/DoNothingTransactionListener.java new file mode 100644 index 0000000..f133ad4 --- /dev/null +++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/DoNothingTransactionListener.java @@ -0,0 +1,51 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + ****************************************************************/ +package org.apache.cayenne.tx; + +import java.sql.Connection; + +/** + * Created by andrus on 9/19/16. + */ +class DoNothingTransactionListener implements TransactionListener { + + private static TransactionListener INSTANCE = new DoNothingTransactionListener(); + + public static TransactionListener getInstance() { + return INSTANCE; + } + + private DoNothingTransactionListener() { + } + + @Override + public void willCommit(Transaction tx) { + // do nothing... + } + + @Override + public void willRollback(Transaction tx) { + // do nothing... + } + + @Override + public void willAddConnection(Transaction tx, String connectionName, Connection connection) { + // do nothing... + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/42eda59b/cayenne-server/src/main/java/org/apache/cayenne/tx/Transaction.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/Transaction.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/Transaction.java index efc47b8..789573f 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/tx/Transaction.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/Transaction.java @@ -43,5 +43,7 @@ public interface Transaction { Connection getConnection(String name); - void addConnection(String name, Connection connection); + void addConnection(String connectionName, Connection connection); + + void addListener(TransactionListener listener); } http://git-wip-us.apache.org/repos/asf/cayenne/blob/42eda59b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionListener.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionListener.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionListener.java new file mode 100644 index 0000000..61d15fb --- /dev/null +++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionListener.java @@ -0,0 +1,36 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + ****************************************************************/ +package org.apache.cayenne.tx; + +import java.sql.Connection; + +/** + * A callback that is notified as transaction progresses through stages. It can customize transaction isolation level, + * etc. + * + * @since 4.0 + */ +public interface TransactionListener { + + void willCommit(Transaction tx); + + void willRollback(Transaction tx); + + void willAddConnection(Transaction tx, String connectionName, Connection connection); +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/42eda59b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionManager.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionManager.java b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionManager.java index 142c503..56ae93f 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionManager.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/tx/TransactionManager.java @@ -22,15 +22,30 @@ package org.apache.cayenne.tx; * An optional utility service that simplifies wrapping multiple operations in * transactions. Users only rarely need to invoke it directly, as all standard * Cayenne operations are managing their own transactions internally. - * + * * @since 4.0 */ public interface TransactionManager { /** * Starts a new transaction (or joins an existing one) calling - * {@link org.apache.cayenne.tx.TransactionalOperation#perform()}, and then - * committing or rolling back the transaction. Frees the user + * {@link org.apache.cayenne.tx.TransactionalOperation#perform()}, and then committing or rolling back the + * transaction. + * + * @param op an operation to perform within the trsnaction. + * @return a value returned by the "op" operation. */ <T> T performInTransaction(TransactionalOperation<T> op); + + /** + * Starts a new transaction (or joins an existing one) calling + * {@link org.apache.cayenne.tx.TransactionalOperation#perform()}, and then committing or rolling back the + * transaction. As transaction goes through stages, callback methods are invoked allowing the caller to customize + * transaction parameters. + * + * @param op an operation to perform within the trsnaction. + * @param callback a callback to notify as transaction progresses through stages. + * @return a value returned by the "op" operation. + */ + <T> T performInTransaction(TransactionalOperation<T> op, TransactionListener callback); } http://git-wip-us.apache.org/repos/asf/cayenne/blob/42eda59b/cayenne-server/src/test/java/org/apache/cayenne/access/TransactionThreadIT.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/TransactionThreadIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/TransactionThreadIT.java index a19b86c..50368e2 100644 --- a/cayenne-server/src/test/java/org/apache/cayenne/access/TransactionThreadIT.java +++ b/cayenne-server/src/test/java/org/apache/cayenne/access/TransactionThreadIT.java @@ -26,6 +26,7 @@ import org.apache.cayenne.testdo.testmap.Artist; import org.apache.cayenne.tx.BaseTransaction; import org.apache.cayenne.tx.CayenneTransaction; import org.apache.cayenne.tx.Transaction; +import org.apache.cayenne.tx.TransactionListener; import org.apache.cayenne.unit.di.server.CayenneProjects; import org.apache.cayenne.unit.di.server.ServerCase; import org.apache.cayenne.unit.di.server.UseServerRuntime; @@ -107,5 +108,10 @@ public class TransactionThreadIT extends ServerCase { delegate.addConnection(name, connection); } + + @Override + public void addListener(TransactionListener listener) { + delegate.addListener(listener); + } } } http://git-wip-us.apache.org/repos/asf/cayenne/blob/42eda59b/cayenne-server/src/test/java/org/apache/cayenne/access/UserTransactionIT.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/UserTransactionIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/UserTransactionIT.java index 55b27d1..f644639 100644 --- a/cayenne-server/src/test/java/org/apache/cayenne/access/UserTransactionIT.java +++ b/cayenne-server/src/test/java/org/apache/cayenne/access/UserTransactionIT.java @@ -26,6 +26,7 @@ import org.apache.cayenne.testdo.testmap.Artist; import org.apache.cayenne.tx.BaseTransaction; import org.apache.cayenne.tx.CayenneTransaction; import org.apache.cayenne.tx.Transaction; +import org.apache.cayenne.tx.TransactionListener; import org.apache.cayenne.unit.di.server.CayenneProjects; import org.apache.cayenne.unit.di.server.ServerCase; import org.apache.cayenne.unit.di.server.UseServerRuntime; @@ -66,9 +67,9 @@ public class UserTransactionIT extends ServerCase { class TxWrapper implements Transaction { - private Transaction delegate; int commitCount; int connectionCount; + private Transaction delegate; TxWrapper(Transaction delegate) { this.delegate = delegate; @@ -103,6 +104,11 @@ public class UserTransactionIT extends ServerCase { connectionCount++; delegate.addConnection(name, connection); } + + @Override + public void addListener(TransactionListener listener) { + delegate.addListener(listener); + } } } http://git-wip-us.apache.org/repos/asf/cayenne/blob/42eda59b/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/ServerRuntimeIT.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/ServerRuntimeIT.java b/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/ServerRuntimeIT.java new file mode 100644 index 0000000..4668264 --- /dev/null +++ b/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/ServerRuntimeIT.java @@ -0,0 +1,96 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + ****************************************************************/ +package org.apache.cayenne.configuration.server; + +import org.apache.cayenne.PersistenceState; +import org.apache.cayenne.di.Inject; +import org.apache.cayenne.testdo.testmap.Artist; +import org.apache.cayenne.tx.Transaction; +import org.apache.cayenne.tx.TransactionListener; +import org.apache.cayenne.tx.TransactionalOperation; +import org.apache.cayenne.unit.di.server.CayenneProjects; +import org.apache.cayenne.unit.di.server.ServerCase; +import org.apache.cayenne.unit.di.server.UseServerRuntime; +import org.apache.cayenne.validation.ValidationException; +import org.junit.Test; + +import java.sql.Connection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT) +public class ServerRuntimeIT extends ServerCase { + + @Inject + private ServerRuntime runtime; + + @Test + public void testPerformInTransaction_Local_Callback() { + + TransactionListener callback = mock(TransactionListener.class); + + Artist a = runtime.performInTransaction(new TransactionalOperation<Artist>() { + + @Override + public Artist perform() { + + Artist localArtist = runtime.newContext().newObject(Artist.class); + localArtist.setArtistName("A1"); + localArtist.getObjectContext().commitChanges(); + return localArtist; + } + }, callback); + + assertEquals("A1", a.getArtistName()); + assertEquals(PersistenceState.COMMITTED, a.getPersistenceState()); + verify(callback).willCommit(any(Transaction.class)); + verify(callback).willAddConnection(any(Transaction.class), any(String.class), any(Connection.class)); + verify(callback, times(0)).willRollback(any(Transaction.class)); + } + + @Test + public void testPerformInTransaction_Local_Callback_Rollback() { + + TransactionListener callback = mock(TransactionListener.class); + + try { + runtime.performInTransaction(new TransactionalOperation<Artist>() { + + @Override + public Artist perform() { + + Artist localArtist = runtime.newContext().newObject(Artist.class); + localArtist.getObjectContext().commitChanges(); + return localArtist; + } + }, callback); + + fail("Exception expected"); + } catch (ValidationException v) { + verify(callback).willRollback(any(Transaction.class)); + verify(callback, times(0)).willAddConnection(any(Transaction.class), any(String.class), any(Connection.class)); + verify(callback, times(0)).willCommit(any(Transaction.class)); + } + } +} http://git-wip-us.apache.org/repos/asf/cayenne/blob/42eda59b/cayenne-server/src/test/java/org/apache/cayenne/tx/DefaultTransactionManagerIT.java ---------------------------------------------------------------------- diff --git a/cayenne-server/src/test/java/org/apache/cayenne/tx/DefaultTransactionManagerIT.java b/cayenne-server/src/test/java/org/apache/cayenne/tx/DefaultTransactionManagerIT.java index 3da3af4..6b9f001 100644 --- a/cayenne-server/src/test/java/org/apache/cayenne/tx/DefaultTransactionManagerIT.java +++ b/cayenne-server/src/test/java/org/apache/cayenne/tx/DefaultTransactionManagerIT.java @@ -33,7 +33,7 @@ import static org.mockito.Mockito.when; public class DefaultTransactionManagerIT extends ServerCase { @Test - public void testPerformInTransaction_NoTx() { + public void testPerformInTransaction_Local() { final BaseTransaction tx = mock(BaseTransaction.class); TransactionFactory txFactory = mock(TransactionFactory.class); @@ -78,4 +78,6 @@ public class DefaultTransactionManagerIT extends ServerCase { BaseTransaction.bindThreadTransaction(null); } } + + } http://git-wip-us.apache.org/repos/asf/cayenne/blob/42eda59b/docs/doc/src/main/resources/RELEASE-NOTES.txt ---------------------------------------------------------------------- diff --git a/docs/doc/src/main/resources/RELEASE-NOTES.txt b/docs/doc/src/main/resources/RELEASE-NOTES.txt index d496e84..d6af992 100644 --- a/docs/doc/src/main/resources/RELEASE-NOTES.txt +++ b/docs/doc/src/main/resources/RELEASE-NOTES.txt @@ -30,6 +30,7 @@ CAY-2103 cayenne-crypto: support for mapping non-String and non-binary types CAY-2106 cayenne-crypto: allow DI contribution of type converters inside ValueTransformerFactory CAY-2107 cayenne-crypto: Lazy initialization of crypto subsystem CAY-2111 Unbind transaction object from the current thread for iterated queries +CAY-2112 Expose callback for "performInTransaction" Bug Fixes: