diff --git a/src/main/java/com/avaje/ebean/Ebean.java b/src/main/java/com/avaje/ebean/Ebean.java index 8acf3f719..f06638c9b 100644 --- a/src/main/java/com/avaje/ebean/Ebean.java +++ b/src/main/java/com/avaje/ebean/Ebean.java @@ -281,7 +281,7 @@ public final class Ebean { * Register the server with this Ebean singleton. Specify if the registered * server is the primary/default server. */ - protected static void register(EbeanServer server, boolean isPrimaryServer) { + public static void register(EbeanServer server, boolean isPrimaryServer) { serverMgr.register(server, isPrimaryServer); } @@ -372,6 +372,19 @@ public final class Ebean { return serverMgr.getPrimaryServer().currentTransaction(); } + /** + * Register a TransactionCallback on the currently active transaction. + *

+ * If there is no currently active transaction then a PersistenceException is thrown. + * + * @param transactionCallback The transaction callback to be registered with the current transaction. + * + * @throws PersistenceException If there is no currently active transaction + */ + public static void register(TransactionCallback transactionCallback) throws PersistenceException { + serverMgr.getPrimaryServer().register(transactionCallback); + } + /** * Commit the current transaction. */ diff --git a/src/main/java/com/avaje/ebean/EbeanServer.java b/src/main/java/com/avaje/ebean/EbeanServer.java index e730a4b35..45f4bdc18 100644 --- a/src/main/java/com/avaje/ebean/EbeanServer.java +++ b/src/main/java/com/avaje/ebean/EbeanServer.java @@ -7,6 +7,7 @@ import java.util.Map; import java.util.Set; import javax.persistence.OptimisticLockException; +import javax.persistence.PersistenceException; import com.avaje.ebean.annotation.CacheStrategy; import com.avaje.ebean.cache.ServerCacheManager; @@ -307,6 +308,17 @@ public interface EbeanServer { */ public SqlUpdate createNamedSqlUpdate(String namedQuery); + /** + * Register a TransactionCallback on the currently active transaction. + *

+ * If there is no currently active transaction then a PersistenceException is thrown. + * + * @param transactionCallback The transaction callback to be registered with the current transaction. + * + * @throws PersistenceException If there is no currently active transaction + */ + public void register(TransactionCallback transactionCallback) throws PersistenceException; + /** * Create a new transaction that is not held in TransactionThreadLocal. *

diff --git a/src/main/java/com/avaje/ebean/Transaction.java b/src/main/java/com/avaje/ebean/Transaction.java index 331cd8f1f..1621352df 100644 --- a/src/main/java/com/avaje/ebean/Transaction.java +++ b/src/main/java/com/avaje/ebean/Transaction.java @@ -35,6 +35,11 @@ public interface Transaction extends Closeable { */ public static final int SERIALIZABLE = java.sql.Connection.TRANSACTION_SERIALIZABLE; + /** + * Register a TransactionCallback with this transaction. + */ + public void register(TransactionCallback callback); + /** * Return true if this transaction is read only. */ diff --git a/src/main/java/com/avaje/ebean/TransactionCallback.java b/src/main/java/com/avaje/ebean/TransactionCallback.java new file mode 100644 index 000000000..449cd33a7 --- /dev/null +++ b/src/main/java/com/avaje/ebean/TransactionCallback.java @@ -0,0 +1,34 @@ +package com.avaje.ebean; + +/** + * Provides a callback that can be registered with a Transaction. + *

+ * The callback methods are called just prior to and after the transaction performs a commit or rollback. + *

+ * A typical use of TransactionCallback would be to clean up non-transactional resources like files. For example, + * when processing files on postCommit/postRollback clean up the associated files. As another example when + * on postCommit of a delete remove associated resources from the file system or remote service. + */ +public interface TransactionCallback { + + /** + * Perform processing just prior to the transaction commit. + */ + void preCommit(); + + /** + * Perform processing just after the transaction commit. + */ + void postCommit(); + + /** + * Perform processing just prior to the transaction rollback. + */ + void preRollback(); + + /** + * Perform processing just after the transaction rollback. + */ + void postRollback(); + +} diff --git a/src/main/java/com/avaje/ebean/TransactionCallbackAdapter.java b/src/main/java/com/avaje/ebean/TransactionCallbackAdapter.java new file mode 100644 index 000000000..6ba299e31 --- /dev/null +++ b/src/main/java/com/avaje/ebean/TransactionCallbackAdapter.java @@ -0,0 +1,42 @@ +package com.avaje.ebean; + +/** + * Adapter that can be extended for easier implementation of TransactionCallback. + *

+ * Provides 'no operation' implementation for each of the TransactionCallback methods. It is expected that this + * class is extended and override the methods you need to. + */ +public abstract class TransactionCallbackAdapter implements TransactionCallback { + + /** + * Perform processing just prior to the transaction commit. + */ + @Override + public void preCommit() { + // do nothing - override as necessary + } + + /** + * Perform processing just after the transaction commit. + */ + @Override + public void postCommit() { + // do nothing - override as necessary + } + + /** + * Perform processing just prior to the transaction rollback. + */ + @Override + public void preRollback() { + // do nothing - override as necessary + } + + /** + * Perform processing just after the transaction rollback. + */ + @Override + public void postRollback() { + // do nothing - override as necessary + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/api/SpiTransaction.java b/src/main/java/com/avaje/ebeaninternal/api/SpiTransaction.java index f46215278..601d1b4d9 100644 --- a/src/main/java/com/avaje/ebeaninternal/api/SpiTransaction.java +++ b/src/main/java/com/avaje/ebeaninternal/api/SpiTransaction.java @@ -19,179 +19,179 @@ public interface SpiTransaction extends Transaction { * End the transaction when had query only use. */ public void endQueryOnly(); - + /** * Return the string prefix with the transactin id and label used in logging. */ public String getLogPrefix(); - - /** - * Return true if generated SQL and Bind values should be logged to the - * transaction log. - */ - public boolean isLogSql(); - /** - * Return true if summary level events should be logged to the transaction - * log. - */ - public boolean isLogSummary(); + /** + * Return true if generated SQL and Bind values should be logged to the + * transaction log. + */ + public boolean isLogSql(); - /** - * Log a message to the SQL logger. - */ - public void logSql(String msg); + /** + * Return true if summary level events should be logged to the transaction + * log. + */ + public boolean isLogSummary(); - /** - * Log a message to the SUMMARY logger. - */ - public void logSummary(String msg); + /** + * Log a message to the SQL logger. + */ + public void logSql(String msg); - /** - * Register a "Derived Relationship" (that requires an additional update). - */ - public void registerDerivedRelationship(DerivedRelationshipData assocBean); + /** + * Log a message to the SUMMARY logger. + */ + public void logSummary(String msg); - /** - * Return the list of "Derived Relationships" that must be maintained after - * insert. - */ - public List getDerivedRelationship(Object bean); + /** + * Register a "Derived Relationship" (that requires an additional update). + */ + public void registerDerivedRelationship(DerivedRelationshipData assocBean); - /** - * Add a deleting bean to the registered list. - *

- * This is to handle bi-directional relationships where both sides Cascade. - *

- */ - public void registerDeleteBean(Integer hash); + /** + * Return the list of "Derived Relationships" that must be maintained after + * insert. + */ + public List getDerivedRelationship(Object bean); - /** - * Unregister the hash of the bean. - */ - public void unregisterDeleteBean(Integer hash); + /** + * Add a deleting bean to the registered list. + *

+ * This is to handle bi-directional relationships where both sides Cascade. + *

+ */ + public void registerDeleteBean(Integer hash); - /** - * Return true if this is a bean that has already been saved/deleted. - */ - public boolean isRegisteredDeleteBean(Integer hash); + /** + * Unregister the hash of the bean. + */ + public void unregisterDeleteBean(Integer hash); - /** - * Unregister the persisted bean. - */ - public void unregisterBean(Object bean); + /** + * Return true if this is a bean that has already been saved/deleted. + */ + public boolean isRegisteredDeleteBean(Integer hash); - /** - * Return true if this is a bean that has already been persisted in the - * current recursive save request. The goal is to stop recursively saving - * the bean when cascade persist is on both sides of a relationship). - *

- * This will register the bean if it is not already. - *

- */ - public boolean isRegisteredBean(Object bean); + /** + * Unregister the persisted bean. + */ + public void unregisterBean(Object bean); - /** - * Returns a String used to identify the transaction. This id is used for - * Transaction logging. - */ - public String getId(); + /** + * Return true if this is a bean that has already been persisted in the + * current recursive save request. The goal is to stop recursively saving + * the bean when cascade persist is on both sides of a relationship). + *

+ * This will register the bean if it is not already. + *

+ */ + public boolean isRegisteredBean(Object bean); - /** - * Return the batchSize specifically set for this transaction or 0. - *

- * Returning 0 implies to use the system wide default batch size. - *

- */ - public int getBatchSize(); + /** + * Returns a String used to identify the transaction. This id is used for + * Transaction logging. + */ + public String getId(); - /** - * Modify and return the current 'depth' of the transaction. - *

- * As we cascade save or delete we traverse the object graph tree. Going up - * to Assoc Ones the depth decreases and going down to Assoc Manys the depth - * increases. - *

- *

- * The depth is used for ordering batching statements. The lowest depth get - * executed first during save. - *

- */ - public int depth(int diff); + /** + * Return the batchSize specifically set for this transaction or 0. + *

+ * Returning 0 implies to use the system wide default batch size. + *

+ */ + public int getBatchSize(); - /** - * Return true if this transaction was created explicitly via - * Ebean.beginTransaction(). - */ - public boolean isExplicit(); + /** + * Modify and return the current 'depth' of the transaction. + *

+ * As we cascade save or delete we traverse the object graph tree. Going up + * to Assoc Ones the depth decreases and going down to Assoc Manys the depth + * increases. + *

+ *

+ * The depth is used for ordering batching statements. The lowest depth get + * executed first during save. + *

+ */ + public int depth(int diff); - /** - * Get the object that holds the event details. - *

- * This information is used maintain the table state, cache and text - * indexes. On commit the Table modifications this generates is broadcast - * around the cluster (if you have a cluster). - *

- */ - public TransactionEvent getEvent(); + /** + * Return true if this transaction was created explicitly via + * Ebean.beginTransaction(). + */ + public boolean isExplicit(); - /** - * Whether persistCascade is on for save and delete. - */ - public boolean isPersistCascade(); + /** + * Get the object that holds the event details. + *

+ * This information is used maintain the table state, cache and text + * indexes. On commit the Table modifications this generates is broadcast + * around the cluster (if you have a cluster). + *

+ */ + public TransactionEvent getEvent(); - /** - * Return true if this request should be batched. Conversely returns false - * if this request should be executed immediately. - */ - public boolean isBatchThisRequest(); + /** + * Whether persistCascade is on for save and delete. + */ + public boolean isPersistCascade(); - /** - * Return the queue used to batch up persist requests. - */ - public BatchControl getBatchControl(); + /** + * Return true if this request should be batched. Conversely returns false + * if this request should be executed immediately. + */ + public boolean isBatchThisRequest(); - /** - * Set the queue used to batch up persist requests. There should only be one - * PersistQueue set per transaction. - */ - public void setBatchControl(BatchControl control); + /** + * Return the queue used to batch up persist requests. + */ + public BatchControl getBatchControl(); - /** - * Return the persistence context associated with this transaction. - *

- * You may wish to hold onto this and set it against another transaction - * later. This is along the lines of 'extended persistence context' - * behaviour. - *

- */ - public PersistenceContext getPersistenceContext(); + /** + * Set the queue used to batch up persist requests. There should only be one + * PersistQueue set per transaction. + */ + public void setBatchControl(BatchControl control); - /** - * Set the persistence context to this transaction. - *

- * This could be considered similar to 'EJB3 Extended Persistence Context'. - * In that you can get the PersistenceContext from a transaction, hold onto - * it, and then set it back later to a second transaction. In general there - * is one PersistenceContext per Transaction. The getPersistenceContext() - * and setPersistenceContext() enable a developer to reuse a single - * PersistenceContext with multiple transactions. - *

- */ - public void setPersistenceContext(PersistenceContext context); + /** + * Return the persistence context associated with this transaction. + *

+ * You may wish to hold onto this and set it against another transaction + * later. This is along the lines of 'extended persistence context' + * behaviour. + *

+ */ + public PersistenceContext getPersistenceContext(); - /** - * Return the underlying Connection for internal use. - *

- * If the connection is made public from Transaction and the user code calls - * that method we can no longer trust the query only status of a - * Transaction. - *

- */ - public Connection getInternalConnection(); + /** + * Set the persistence context to this transaction. + *

+ * This could be considered similar to 'EJB3 Extended Persistence Context'. + * In that you can get the PersistenceContext from a transaction, hold onto + * it, and then set it back later to a second transaction. In general there + * is one PersistenceContext per Transaction. The getPersistenceContext() + * and setPersistenceContext() enable a developer to reuse a single + * PersistenceContext with multiple transactions. + *

+ */ + public void setPersistenceContext(PersistenceContext context); - /** - * Return true if the manyToMany intersection should be persisted for this particular relationship direction. - */ + /** + * Return the underlying Connection for internal use. + *

+ * If the connection is made public from Transaction and the user code calls + * that method we can no longer trust the query only status of a + * Transaction. + *

+ */ + public Connection getInternalConnection(); + + /** + * Return true if the manyToMany intersection should be persisted for this particular relationship direction. + */ public boolean isSaveAssocManyIntersection(String intersectionTable, String beanName); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java index 81431d375..635797e6d 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java @@ -653,6 +653,15 @@ public final class DefaultServer implements SpiEbeanServer { return (T) ref; } + @Override + public void register(TransactionCallback transactionCallback) { + Transaction transaction = currentTransaction(); + if (transaction == null) { + throw new PersistenceException("Not currently active transaction when trying to register transactionCallback"); + } + transaction.register(transactionCallback); + } + /** * Creates a new Transaction that is NOT stored in TransactionThreadLocal. Use * this when you want a thread to have a second independent transaction. diff --git a/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java b/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java index 0a3705e77..a06ce88f3 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java +++ b/src/main/java/com/avaje/ebeaninternal/server/transaction/JdbcTransaction.java @@ -13,6 +13,7 @@ import java.util.Map; import javax.persistence.PersistenceException; import javax.persistence.RollbackException; +import com.avaje.ebean.TransactionCallback; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -129,6 +130,8 @@ public class JdbcTransaction implements SpiTransaction { protected Map userObjects; + protected List callbackList; + /** * Create a new JdbcTransaction. */ @@ -171,6 +174,63 @@ public class JdbcTransaction implements SpiTransaction { return logPrefix; } + @Override + public void register(TransactionCallback callback) { + if (callbackList == null) { + callbackList = new ArrayList(4); + } + callbackList.add(callback); + } + + protected void firePreRollback() { + if (callbackList != null) { + for (TransactionCallback callback : callbackList) { + try { + callback.preRollback(); + } catch (Exception e) { + logger.error("Error executing preRollback callback", e); + } + } + } + } + + protected void firePostRollback() { + if (callbackList != null) { + for (TransactionCallback callback : callbackList) { + try { + callback.postRollback(); + } catch (Exception e) { + logger.error("Error executing postRollback callback", e); + } + } + } + } + + protected void firePreCommit() { + if (callbackList != null) { + for (TransactionCallback callback : callbackList) { + try { + callback.preCommit(); + } catch (Exception e) { + logger.error("Error executing preCommit callback", e); + } + } + } + } + + protected void firePostCommit() { + if (callbackList != null) { + for (TransactionCallback callback : callbackList) { + try { + callback.postCommit(); + } catch (Exception e) { + logger.error("Error executing postCommit callback", e); + } + } + } + } + + public List getDerivedRelationship(Object bean) { if (derivedRelMap == null) { return null; @@ -603,6 +663,9 @@ public class JdbcTransaction implements SpiTransaction { if (!isActive()) { throw new IllegalStateException(illegalStateMessage); } + + firePreCommit(); + try { if (queryOnly) { // can rollback or just close for performance @@ -620,6 +683,7 @@ public class JdbcTransaction implements SpiTransaction { } finally { // these will not throw an exception + firePostCommit(); deactivate(); notifyCommit(); } @@ -653,6 +717,7 @@ public class JdbcTransaction implements SpiTransaction { if (!isActive()) { throw new IllegalStateException(illegalStateMessage); } + firePreRollback(); try { performRollback(); @@ -661,6 +726,7 @@ public class JdbcTransaction implements SpiTransaction { } finally { // these will not throw an exception + firePostRollback(); deactivate(); notifyRollback(cause); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/transaction/TransactionManager.java b/src/main/java/com/avaje/ebeaninternal/server/transaction/TransactionManager.java index 1c267ee74..9f89c4fa6 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/transaction/TransactionManager.java +++ b/src/main/java/com/avaje/ebeaninternal/server/transaction/TransactionManager.java @@ -427,10 +427,9 @@ public class TransactionManager { for (TransactionEventListener listener : transactionEventListeners) { listener.postTransactionCommit(transaction); } - - } catch (Exception ex) { - String m = "NotifyOfCommit failed. L2 Cache potentially not notified."; - logger.error(m, ex); + + } catch (Exception ex) { + logger.error("NotifyOfCommit failed. L2 Cache potentially not notified.", ex); } } diff --git a/src/test/java/com/avaje/tests/transaction/TestTransactionCallback.java b/src/test/java/com/avaje/tests/transaction/TestTransactionCallback.java new file mode 100644 index 000000000..e8f3ba85c --- /dev/null +++ b/src/test/java/com/avaje/tests/transaction/TestTransactionCallback.java @@ -0,0 +1,81 @@ +package com.avaje.tests.transaction; + +import com.avaje.ebean.BaseTestCase; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.EbeanServer; +import com.avaje.ebean.TransactionCallbackAdapter; +import org.junit.Test; + +import javax.persistence.PersistenceException; + +import static org.junit.Assert.assertEquals; + +public class TestTransactionCallback extends BaseTestCase { + + int countPreCommit; + int countPostCommit; + int countPreRollback; + int countPostRollback; + + @Test(expected = PersistenceException.class) + public void test_noActiveTransaction() { + + Ebean.register(new MyCallback()); + } + + @Test + public void test_commitAndRollback() { + + + Ebean.beginTransaction(); + Ebean.register(new MyCallback()); + Ebean.commitTransaction(); + + assertEquals(1, countPreCommit); + assertEquals(1, countPostCommit); + assertEquals(0, countPreRollback); + assertEquals(0, countPostRollback); + + Ebean.beginTransaction(); + Ebean.register(new MyCallback()); + Ebean.rollbackTransaction(); + + assertEquals(1, countPreCommit); + assertEquals(1, countPostCommit); + assertEquals(1, countPreRollback); + assertEquals(1, countPostRollback); + + + } + + @Test(expected = PersistenceException.class) + public void test_withEbeanserver() { + + EbeanServer server = Ebean.getServer(null); + server.register(new MyCallback()); + } + + + class MyCallback extends TransactionCallbackAdapter { + + @Override + public void preCommit() { + countPreCommit++; + } + + @Override + public void postCommit() { + countPostCommit++; + } + + @Override + public void preRollback() { + countPreRollback++; + } + + @Override + public void postRollback() { + countPostRollback++; + } + } +}