Interface Transaction


  • public interface Transaction
    A JDBC transaction coordinator.

    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 ratpack.exec.Blocking;
     import ratpack.exec.Operation;
     import ratpack.func.Block;
     import ratpack.jdbctx.Transaction;
     import ratpack.test.exec.ExecHarness;
     import ratpack.func.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;
    
     import static org.junit.jupiter.api.Assertions.assertEquals;
     import static org.junit.jupiter.api.Assertions.assertSame;
    
     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 -> 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 -> {
             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"));
       }
     }
     
    Since:
    1.5
    • Method Detail

      • dataSource

        static DataSource dataSource​(DataSource dataSource)
        Decorates the given data source to be 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.

        Parameters:
        dataSource - the data source to delegate to
        Returns:
        a connection aware data source
      • current

        static Optional<Transaction> current()
        The current execution bound transaction, if any.

        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).

        Returns:
        the current execution bound transaction, if any.
      • connection

        static Optional<Connection> connection()
        The connection of the current transaction if it is active.
        Returns:
        the connection of the current transaction if it is active
      • create

        static Transaction create​(Factory<? extends Connection> connectionFactory)
        Creates a new transaction.

        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.

        Parameters:
        connectionFactory - the connection factory
        Returns:
        the newly created transaction
      • get

        static Transaction get​(Factory<? extends Connection> connectionFactory)
        Returns the current transaction if present, otherwise a newly created transaction.
        Parameters:
        connectionFactory - the connection factory
        Returns:
        the current transaction if present, otherwise a newly created transaction
      • bound

        static Transaction bound​(Factory<? extends Connection> connectionFactory)
        Creates a transaction implementation that delegates to the execution bound transaction.

        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.

        Parameters:
        connectionFactory - the connection factory
        Returns:
        a transaction object that delegates to the current transaction, or creates a new one, for each method
      • bind

        default Transaction bind()
                          throws TransactionException
        Binds this transaction to the current execution.

        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.

        Returns:
        this
        Throws:
        TransactionException - if a different transaction is bound to the execution
      • unbind

        default boolean unbind()
        Unbinds this transaction from the current execution.

        If the transaction is not bound, this method is effectively a noop and returns false.

        Returns:
        whether this transaction was actually bound
        Throws:
        TransactionException - if a different transaction is bound to the execution
        See Also:
        bind()
      • isActive

        default boolean isActive()
        Whether this transaction is active.
        Returns:
        whether this transaction is active
      • getConnection

        Optional<Connection> getConnection()
        The underlying connection for the transaction.

        The optional will be empty if the transaction is not active.

        Returns:
        the underlying connection for the transaction
      • begin

        Operation begin()
        Starts a transaction.

        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.

        Returns:
        an operation to start a transaction or create a savepoint
      • rollback

        Operation rollback()
        Initiates a transaction 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).

        Returns:
        an operation that rolls back the transaction or to the most recent savepoint
      • commit

        Operation commit()
        Commits the transaction, or pops the most recent savepoint off the stack.

        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).

        Returns:
        an operation that commits the transaction or pops the most recent save point
      • autoBind

        Transaction autoBind​(boolean autoBind)
        Sets the auto binding behaviour of the transaction.

        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.

        Parameters:
        autoBind - whether to enable auto-binding
        Returns:
        this
      • isAutoBind

        boolean isAutoBind()
        Whether this transaction is auto-binding.
        Returns:
        whether this transaction is auto-binding
      • wrap

        <T> Promise<T> wrap​(Promise<T> promise)
        Decorates the given promise in a transaction boundary.

        The decoration effectively calls begin() before yielding the promise. If it fails, rollback() will be issued. If it succeeds, commit() will be issued.

        Type Parameters:
        T - the type of promised value
        Parameters:
        promise - the promise to yield in a transaction
        Returns:
        a promise that will yield within a transaction
      • wrap

        default <T> Promise<T> wrap​(Factory<? extends Promise<T>> promiseFactory)
        Executes the given factory and yields the resultant promise in a transaction.
        Type Parameters:
        T - the type of promised value
        Parameters:
        promiseFactory - the factory of the promise to yield in a transaction
        Returns:
        a promise that will yield within a transaction
      • wrap

        Operation wrap​(Operation operation)
        Decorates the given operation in a transaction boundary.

        The decoration effectively calls begin() before yielding the operation. If it fails, rollback() will be issued. If it succeeds, commit() will be issued.

        Parameters:
        operation - the operation to yield in a transaction
        Returns:
        a operation that will yield within a transaction