public interface Transaction
An instance of this type represents a potential transaction or an active transaction at any given time.
The begin()
method must be called on a transaction to actually initiate a transaction.
This type is effectively an asynchronous adapter to the JDBC Connection
class's transactional methods such as
Connection.commit()
, Connection.setSavepoint()
, Connection.rollback()
etc.
It also (optionally) manages an execution global binding, analogous to thread local globals with synchronous frameworks such as Spring's transaction management.
This allows implicit use of the “current” transaction's connection (see current()
and connection()
).
Transaction objects are reusable, but cannot be used concurrently.
import org.h2.jdbcx.JdbcDataSource;
import org.junit.Assert;
import ratpack.exec.Blocking;
import ratpack.exec.Operation;
import ratpack.func.Block;
import ratpack.jdbctx.Transaction;
import ratpack.test.exec.ExecHarness;
import ratpack.util.Exceptions;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Example {
private static DataSource txDs;
private static Transaction tx;
public static void main(String[] args) throws Exception {
JdbcDataSource ds = new JdbcDataSource();
ds.setURL("jdbc:h2:mem:transactionExamples;DB_CLOSE_DELAY=-1");
txDs = Transaction.dataSource(ds);
tx = Transaction.create(ds::getConnection);
try (Connection connection = txDs.getConnection()) {
try (Statement statement = connection.createStatement()) {
statement.executeUpdate("CREATE TABLE tbl (value VARCHAR(50)) ");
}
}
List<Block> examples = Arrays.asList(
Example::singleTransactionExample,
Example::singleTransactionRollbackExample,
Example::nestedTransactionExample,
Example::nestedTransactionRollbackExample,
() -> manualTransactionExample(true),
() -> manualTransactionExample(false)
);
try (ExecHarness harness = ExecHarness.harness()) {
for (Block example : examples) {
harness.execute(Operation.of(example));
reset();
}
}
}
private static void reset() throws SQLException {
try (Connection connection = txDs.getConnection()) {
connection.createStatement().execute("DELETE FROM tbl");
}
}
private static Operation insert(String value) {
return Blocking.op(() -> {
try (Connection connection = txDs.getConnection()) {
connection.createStatement().execute("INSERT INTO tbl (value) VALUES (" + value + ")");
}
});
}
private static Block assertValues(String... expected) {
return () ->
Blocking.get(() -> {
try (Connection connection = txDs.getConnection()) {
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("SELECT value FROM tbl;");
List<String> actual = new ArrayList<>();
while (resultSet.next()) {
actual.add(resultSet.getString(1));
}
return actual;
}
})
.then(actual -> Assert.assertEquals(Arrays.asList(expected), actual));
}
// BEGIN EXAMPLES
private static void singleTransactionExample() {
tx.wrap(insert("1")).then(assertValues("1"));
}
private static void singleTransactionRollbackExample() {
RuntimeException exception = new RuntimeException("1");
tx.wrap(
insert("1")
.next(() -> {
throw exception;
})
)
.onError(e -> {
Assert.assertSame(e, exception);
Operation.of(assertValues()).then();
})
.then(() -> {
throw new IllegalStateException("operation should have failed");
});
}
private static void nestedTransactionExample() {
tx.wrap(
insert("1")
.next(tx.wrap(insert("2")))
)
.then(assertValues("1", "2"));
}
private static void nestedTransactionRollbackExample() {
RuntimeException exception = new RuntimeException("1");
tx.wrap(
insert("1")
.next(
tx.wrap(
insert("2")
.next(() -> {
throw exception;
})
)
// recover from the error, and insert something else
.mapError(e -> insert("3").then())
)
)
.then(assertValues("1", "3"));
}
private static void manualTransactionExample(boolean fail) {
tx.begin()
.next(insert("1"))
.next(() -> {
if (fail) {
throw new RuntimeException("!");
}
})
.onError(e ->
tx.rollback().then(() -> {
throw Exceptions.toException(e);
})
)
.next(tx.commit())
.onError(e -> assertValues().map(Operation::of).then())
.then(assertValues("1"));
}
}
Modifier and Type | Method and Description |
---|---|
Transaction |
autoBind(boolean autoBind)
Sets the auto binding behaviour of the transaction.
|
Operation |
begin()
Starts a transaction.
|
default Transaction |
bind()
Binds this transaction to the current execution.
|
static Transaction |
bound(Factory<? extends java.sql.Connection> connectionFactory)
Creates a transaction implementation that delegates to the execution bound transaction.
|
Operation |
commit()
Commits the transaction, or pops the most recent savepoint off the stack.
|
static java.util.Optional<java.sql.Connection> |
connection()
The connection of the current transaction if it is active.
|
static Transaction |
create(Factory<? extends java.sql.Connection> connectionFactory)
Creates a new transaction.
|
static java.util.Optional<Transaction> |
current()
The current execution bound transaction, if any.
|
static javax.sql.DataSource |
dataSource(javax.sql.DataSource dataSource)
Decorates the given data source to be
Transaction aware. |
static Transaction |
get(Factory<? extends java.sql.Connection> connectionFactory)
Returns the current transaction if present, otherwise a newly created transaction.
|
java.util.Optional<java.sql.Connection> |
getConnection()
The underlying connection for the transaction.
|
default boolean |
isActive()
Whether this transaction is active.
|
boolean |
isAutoBind()
Whether this transaction is auto-binding.
|
static Transaction |
join()
Returns the current transaction if present
|
Operation |
rollback()
Initiates a transaction rollback.
|
default boolean |
unbind()
Unbinds this transaction from the current execution.
|
default <T> Promise<T> |
wrap(Factory<? extends Promise<T>> promiseFactory)
Executes the given factory and yields the resultant promise in a transaction.
|
Operation |
wrap(Operation operation)
Decorates the given operation in a transaction boundary.
|
<T> Promise<T> |
wrap(Promise<T> promise)
Decorates the given promise in a transaction boundary.
|
static javax.sql.DataSource dataSource(javax.sql.DataSource dataSource)
Transaction
aware.
This method can be used to create a data source that implicitly uses the connection of the active transaction if there is one. This is a typical pattern in an application that interacts with a single database. More complex applications may require more explicit connection assignment.
If a connection is requested while there is a current active transaction, its connection will be returned. The returned connection is effectively un-closeable. It will be closed when the overarching transaction is completed.
If a connection is requested while there is NOT a current active transaction, a connection from dataSource
will be returned.
All other methods/functions always delegate to the given dataSource
.
dataSource
- the data source to delegate tostatic java.util.Optional<Transaction> current()
When a transaction is active (i.e. begin()
has been called), the instance is bound to the current execution.
This behaviour can be disabled via autoBind(boolean)
.
static java.util.Optional<java.sql.Connection> connection()
static Transaction create(Factory<? extends java.sql.Connection> connectionFactory)
This method always creates a new transaction.
It is more typical to use get(Factory)
to use the existing transaction, or create a new one if none exists.
connectionFactory
- the connection factorystatic Transaction get(Factory<? extends java.sql.Connection> connectionFactory)
connectionFactory
- the connection factorystatic Transaction bound(Factory<? extends java.sql.Connection> connectionFactory)
This transaction can be used as an application wide singleton.
When any transaction method is called,
it will delegate to the current()
transaction if there is one,
or it will create(Factory)
a new one.
This differs to get(Factory)
in that this method returns a dynamically delegating transaction,
instead of an actual transaction.
Typically, this method can be used to create a single Transaction
object that is used throughout the application.
connectionFactory
- the connection factorystatic Transaction join() throws TransactionException
TransactionException
- if there is no bound transactiondefault Transaction bind() throws TransactionException
The instance is added to the current execution's registry.
It is typically not necessary to call this directly. Transactions default to “auto binding”. That is, this method is called implicitly when the transaction starts.
this
TransactionException
- if a different transaction is bound to the executiondefault boolean unbind()
If the transaction is not bound, this method is effectively a noop and returns false.
TransactionException
- if a different transaction is bound to the executionbind()
default boolean isActive()
java.util.Optional<java.sql.Connection> getConnection()
The optional will be empty if the transaction is not active.
Operation begin()
If the transaction is not active, a new connection will be acquired. If a transaction has already begun, creates a new savepoint and adds it to an internal stack.
A call to this method MUST be paired with a subsequent call to either commit()
or rollback()
.
Generally, it is more convenient to use wrap(Promise)
or wrap(Operation)
which manages this.
Operation rollback()
If the transaction is not active, the operation will fail with TransactionException
.
If the save point stack is empty (i.e. there are no nested transactions),
A Connection.rollback()
is issued and the underlying connection is closed.
The transaction will no longer be active and will be unbound
(if auto-binding).
If the save point stack is NOT empty, the most recent savepoint is rolled back.
The underlying connection will not be closed, and the transaction will remain active and bound
(if auto-binding).
Operation commit()
If the transaction is not active, the operation will fail with TransactionException
.
If the save point stack is empty (i.e. there are no nested transactions),
A Connection.commit()
()} is issued and the underlying connection is closed.
The transaction will no longer be active and will be unbound
(if auto-binding).
If the save point stack is NOT empty, the most recent savepoint is popped from the stack.
The underlying connection will not be closed, and the transaction will remain active and bound
(if auto-binding).
Transaction autoBind(boolean autoBind)
An auto-binding transaction will implicitly call bind()
when becoming active,
and unbind()
when closing.
It generally only helps to disable auto binding if multiple connections are used within the same execution.
Defaults to true
.
autoBind
- whether to enable auto-bindingthis
boolean isAutoBind()
<T> Promise<T> wrap(Promise<T> promise)
The decoration effectively calls begin()
before yielding the promise.
If it fails, rollback()
will be issued.
If it succeeds, commit()
will be issued.
T
- the type of promised valuepromise
- the promise to yield in a transactiondefault <T> Promise<T> wrap(Factory<? extends Promise<T>> promiseFactory)
T
- the type of promised valuepromiseFactory
- the factory of the promise to yield in a transactionOperation wrap(Operation operation)
The decoration effectively calls begin()
before yielding the operation.
If it fails, rollback()
will be issued.
If it succeeds, commit()
will be issued.
operation
- the operation to yield in a transaction