All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ratpack.jdbctx.Transaction Maven / Gradle / Ivy

There is a newer version: 2.0.0-rc-1
Show newest version
/*
 * Copyright 2016 the original author or authors.
 *
 * 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 ratpack.jdbctx;

import ratpack.exec.Execution;
import ratpack.exec.Operation;
import ratpack.exec.Promise;
import ratpack.func.Factory;
import ratpack.jdbctx.internal.BoundTransaction;
import ratpack.jdbctx.internal.DefaultTransaction;
import ratpack.jdbctx.internal.TransactionalDataSource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.util.Optional;

/**
 * A JDBC transaction coordinator.
 * 

* An instance of this type represents a potential transaction or an active transaction at any given time. * The {@link #begin()} method must be called on a transaction to actually initiate a transaction. *

* This type is effectively an asynchronous adapter to the JDBC {@link Connection} class's transactional methods such as * {@link Connection#commit()}, {@link Connection#setSavepoint()}, {@link 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 {@link #current()} and {@link #connection()}). *

* Transaction objects are reusable, but cannot be used concurrently. * *

{@code
 * 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 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 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"));
 *   }
 * }
 * }
* * @since 1.5 */ public interface Transaction { /** * Decorates the given data source to be {@link 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 {@code dataSource} will be returned. *

* All other methods/functions always delegate to the given {@code dataSource}. * * @param dataSource the data source to delegate to * @return a connection aware data source */ static DataSource dataSource(DataSource dataSource) { return new TransactionalDataSource(dataSource); } /** * The current execution bound transaction, if any. *

* When a transaction is active (i.e. {@link #begin()} has been called), the instance is bound to the current execution. * This behaviour can be disabled via {@link #autoBind(boolean)}. * * @return the current execution bound transaction, if any. */ static Optional current() { return Execution.currentOpt().flatMap(e -> e.maybeGet(Transaction.class)); } /** * The connection of the current transaction if it is active. * * @return the connection of the current transaction if it is active */ static Optional connection() { return current().flatMap(Transaction::getConnection); } /** * Creates a new transaction. *

* This method always creates a new transaction. * It is more typical to use {@link #get(Factory)} to use the existing transaction, or create a new one if none exists. * * @param connectionFactory the connection factory * @return the newly created transaction */ static Transaction create(Factory connectionFactory) { return new DefaultTransaction(connectionFactory); } /** * Returns the current transaction if present, otherwise a newly created transaction. * * @param connectionFactory the connection factory * @return the current transaction if present, otherwise a newly created transaction */ static Transaction get(Factory connectionFactory) { Optional current = current(); if (current.isPresent()) { return current.get(); } else { return create(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 {@link #current()} transaction if there is one, * or it will {@link #create(Factory)} a new one. *

* This differs to {@link #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 {@code Transaction} object that is used throughout the application. * * @param connectionFactory the connection factory * @return a transaction object that delegates to the current transaction, or creates a new one, for each method */ static Transaction bound(Factory connectionFactory) { return new BoundTransaction(connectionFactory); } /** * Returns the current transaction if present * * @return the current transaction if present * @throws TransactionException if there is no bound transaction */ static Transaction join() throws TransactionException { return current().orElseThrow(() -> new TransactionException("There is no bound transaction to join")); } /** * 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. * * @return {@code this} * @throws TransactionException if a different transaction is bound to the execution */ default Transaction bind() throws TransactionException { Execution execution = Execution.current(); execution.maybeGet(Transaction.class).ifPresent(t -> { if (t != this) { throw new TransactionException("A transaction is already bound"); } }); execution.add(Transaction.class, this); return this; } /** * Unbinds this transaction from the current execution. *

* If the transaction is not bound, this method is effectively a noop and returns false. * * @return whether this transaction was actually bound * @throws TransactionException if a different transaction is bound to the execution * @see #bind() */ default boolean unbind() { Execution execution = Execution.current(); Optional transaction = execution.maybeGet(Transaction.class); if (transaction.isPresent() && transaction.get() == this) { execution.remove(Transaction.class); return true; } else { return false; } } /** * Whether this transaction is active. * * @return whether this transaction is active */ default boolean isActive() { return getConnection().isPresent(); } /** * The underlying connection for the transaction. *

* The optional will be empty if the transaction is not active. * * @return the underlying connection for the transaction */ Optional getConnection(); /** * 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 {@link #commit()} or {@link #rollback()}. * Generally, it is more convenient to use {@link #wrap(Promise)} or {@link #wrap(Operation)} which manages this. * * @return an operation to start a transaction or create a savepoint */ Operation begin(); /** * Initiates a transaction rollback. *

* If the transaction is not active, the operation will fail with {@link TransactionException}. *

* If the save point stack is empty (i.e. there are no nested transactions), * A {@link Connection#rollback()} is issued and the underlying connection is closed. * The transaction will no longer be active and will be {@link #unbind() 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 {@link #bind() bound} (if auto-binding). * * @return an operation that rolls back the transaction or to the most recent savepoint */ Operation rollback(); /** * Commits the transaction, or pops the most recent savepoint off the stack. *

* If the transaction is not active, the operation will fail with {@link TransactionException}. *

* If the save point stack is empty (i.e. there are no nested transactions), * A {@link Connection#commit()} ()} is issued and the underlying connection is closed. * The transaction will no longer be active and will be {@link #unbind() 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 {@link #bind() bound} (if auto-binding). * * @return an operation that commits the transaction or pops the most recent save point */ Operation commit(); /** * Sets the auto binding behaviour of the transaction. *

* An auto-binding transaction will implicitly call {@link #bind()} when becoming active, * and {@link #unbind()} when closing. *

* It generally only helps to disable auto binding if multiple connections are used within the same execution. *

* Defaults to {@code true}. * * @param autoBind whether to enable auto-binding * @return {@code this} */ Transaction autoBind(boolean autoBind); /** * Whether this transaction is auto-binding. * @return whether this transaction is auto-binding */ boolean isAutoBind(); /** * Decorates the given promise in a transaction boundary. *

* The decoration effectively calls {@link #begin()} before yielding the promise. * If it fails, {@link #rollback()} will be issued. * If it succeeds, {@link #commit()} will be issued. * * @param promise the promise to yield in a transaction * @param the type of promised value * @return a promise that will yield within a transaction */ Promise wrap(Promise promise); /** * Executes the given factory and yields the resultant promise in a transaction. * * @param promiseFactory the factory of the promise to yield in a transaction * @param the type of promised value * @return a promise that will yield within a transaction */ default Promise wrap(Factory> promiseFactory) { return wrap(Promise.flatten(promiseFactory)); } /** * Decorates the given operation in a transaction boundary. *

* The decoration effectively calls {@link #begin()} before yielding the operation. * If it fails, {@link #rollback()} will be issued. * If it succeeds, {@link #commit()} will be issued. * * @param operation the operation to yield in a transaction * @return a operation that will yield within a transaction */ Operation wrap(Operation operation); }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy