net.java.ao.Transaction Maven / Gradle / Ivy
Show all versions of activeobjects-core Show documentation
/*
* Copyright 2007 Daniel Spiewak
*
* 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.
*/
package net.java.ao;
import java.sql.Connection;
import java.sql.SQLException;
import static net.java.ao.sql.SqlUtils.closeQuietly;
/**
* Allows for the syntactically simple use of database transactions within the
* ActiveObjects API. This class's syntax is modeled after the transaction
* do ... end
syntax provided by Rails's ActiveRecord ORM. The intention
* is to provide the simplest possible encapsulation around the transaction
* functionality. As such, AO transactions lack some of the power of the
* underlying database transaction function (such as arbitrary save-points).
*
* The design behind Transaction
is modeled after the
* following code snippet:
*
* new Transaction<Object>(manager) {
* public Object run() {
* Account a = getEntityManager().get(Account.class, 1);
* Account b = getEntityManager().get(Account.class, 2);
*
* a.setBalance(a.getBalance() - 1000);
* a.save();
*
* b.setBalance(b.getBalance() + 1000);
* b.save();
*
* return null;
* }
* }.execute();
*
* The transaction will be committed only after the run()
* method returns. Thus, a.save()
doesn't immediately modify
* the database values, only upon the committal of the transaction. If any
* conflicts are detected, JDBC will automatically throw an {@link SQLException}.
* Transaction
catches this exception and rolls back the
* transaction, ensuring data integrity. Once the transaction is rolled back, the
* exception is rethrown from the execute()
method.
*
* In cases where the transaction generates data which must be returned, this
* can be accomplished by returning from the {@link #run()} method against the
* parameterized type. Thus if a transaction to create an account is utilized:
*
* Account result = new Transaction<Account>(manager) {
* public Account run() throws SQLException {
* Account back = getEntityManager().create(Account.class);
*
* back.setBalance(0);
* back.save():
*
* return back;
* }
* }.execute();
*
* The value returned from run()
will be passed back up the call
* stack to execute()
, which will return the value to the caller.
* Thus in this example, result
will be precisely the back
* instance from within the transaction. This feature allows data to escape the
* scope of the transaction, thereby achieving a greater usefulness.
*
* The JDBC transaction type used is {@link Connection#TRANSACTION_SERIALIZABLE}.
*
* @author Daniel Spiewak
* @see java.sql.Connection
*/
public abstract class Transaction {
private final EntityManager manager;
private enum TransactionState {
START,
RUNNING,
COMMITTED
}
/**
* Creates a new Transaction
using the specified
* {@link EntityManager} instance. If the specified instance is null
,
* an exception will be thrown.
*
* @param manager The EntityManager
instance against which the
* transaction should run.
* @throws IllegalArgumentException If the {@link EntityManager} instance is null
.
*/
public Transaction(EntityManager manager) {
if (manager == null) {
throw new IllegalArgumentException("EntityManager instance cannot be null");
}
this.manager = manager;
}
protected final EntityManager getEntityManager() {
return manager;
}
/**
* Executes the transaction defined within the overridden {@link #run()}
* method. If the transaction fails for any reason (such as a conflict), it will
* be rolled back and an exception thrown. The value returned from the
* run()
method will be returned from execute()
.
*
* Custom JDBC code can be executed within a transaction. However, one
* should be a bit careful with the mutable state of the {@link Connection}
* instance obtained from getEntityManager().getProvider().getConnection()
.
* This is because it is this exact instance which is used in all database
* operations for that transaction. Thus it is technically possible to commit a
* transaction prematurely, disable the transaction entirely, or otherwise really
* mess up the internals of the implementation. You do not have to
* call setAutoCommit(boolean)
on the {@link Connection}
* instance retrieved from the {@link DatabaseProvider}. The connection is
* already initialized and within an open transaction by the time it gets to your
* custom code within the transaction.
*
* @return The value (if any) returned from the transaction run()
* @throws SQLException If the transaction failed for any reason and was rolled back.
* @see #run()
*/
public T execute() throws SQLException {
final DatabaseProvider provider = manager.getProvider();
TransactionState state = TransactionState.START;
Connection c = null;
try {
c = provider.startTransaction();
state = TransactionState.RUNNING;
final T back = run();
provider.commitTransaction(c);
state = TransactionState.COMMITTED;
return back;
} finally {
if (state == TransactionState.RUNNING && c != null) {
provider.rollbackTransaction(c);
}
provider.setCloseable(c, true);
closeQuietly(c);
}
}
/**
* Called internally by {@link #execute()} to actually perform the actions
* within the transaction. Any SQLException(s)
should be
* allowed to propogate back up to the calling method, which will ensure
* that the transaction is rolled back and the proper resources disposed. If
* the transaction generates a value which must be passed back to the calling
* method, this value may be returned as long as it is of the parameterized
* type. If no value is generated, null
is an acceptable return
* value.
*
* Be aware that any operations performed within a transaction
* (even if indirectly invoked by the run()
method) will use
* the exact same {@link Connection} instance. This is to ensure
* integrity of the transaction's operations while at the same time allowing
* custom JDBC code and queries within the transaction.
*
* @return Any value which must be passed back to the calling point (outside
* the transaction), or null
.
* @throws SQLException If something has gone wrong within the transaction and
* it requires a roll-back.
*/
protected abstract T run() throws SQLException;
}