Chapter 6. Database API

Transaction API Requirements

Introduction

The purpose of this document is to establish the requirements for an API used to specify that a block of code is a transaction. A transaction is defined as a set of operations that transforms data from one consistent state to another [2]. Transactions are used for a number of purposes, including support for concurrent access to a database. The transaction API is a part of a database input/output API that provides applications the capability to specify that several statements of code are one atomic unit of work: a transaction.

This overview of transactions follows the description given in [2]. Transactions provide several important properties, referred to as the ACID properties:

  • Atomicity: A transaction is either successful or unsuccessful. Either all of the operations that make up a transaction take effect or none take effect. A successful transaction is said to commit. An unsuccessful transaction is said to abort. Any operations performed by an aborted transaction are undone (rolled back) so that its effects are not visible.

  • Consistency: A transaction transforms distributed data from one consistent state to another. The application program is responsible for ensuring consistency.

  • Isolation: Each transaction appears to run independently of other transactions that are running concurrently. The effects of a transaction are not visible to others until the transaction completes (commits or aborts). The transactions appear to be serialized, with two or more transactions acting as though one completed before the other began, even though they are executed concurrently.

  • Durability: Also known as permanence, this property ensures that once completed, the effects of a transaction are permanent. A subsequent failure (such as abnormal program termination, communications failure, or hardware failure) does not cause the effects to be undone. Transactions are often used to control and moderate access to a centralized database. Typical uses for transaction processing systems include database-oriented applications such as airline reservation systems and automatic bank-teller machines.

The process of reserving a seat on an airline flight can be used to illustrate the role that transaction properties play in accessing data. For example, a customer requests a seat on an airline flight, and a reservation agent queries the database for the number of seats left on that flight. The answer to the query indicates that one seat is available, and the agent can then reserve that seat.

The isolation property ensures that other transactions that make the same request at the same time cannot reserve the same seat. That seat must be reserved by the transaction until the entire transaction is either completed or canceled--information is not allowed to change while a running transaction depends on it. To provide atomicity, the request for an available seat and the reservation of that seat must both be part of the same transaction. This ensures that if a seat is marked as unavailable, there is a customer reservation associated with that seat. (If the customer decides not to reserve the seat, the transaction is canceled, and the seat is marked as available once again.) To provide durability, the centralized database must maintain a permanent record of the reservation even if the database resides on a computer that fails and restarts after the seat is reserved. To provide consistency, the application must update the information in the database correctly to indicate that the seat is unavailable and identify the customer who reserved the seat.

Vision Statement

Transactions provide a way to guarantee the reliability of data processing and are a critical requirement for supporting collaborative commerce. Enterprise applications require a level of robustness and concurrency that transactions provide. Without transactions, it is impossible to maintain data integrity. The consequence is that information critical to a business's function can become corrupted into inconsistent states. This can result in software malfunction, such as lost personnel records or incorrect order fulfillment that compromise an enterprise's objectives. The purpose of the transaction API is to provide developers with methods to ensure that the data of an enterprise application is always kept in a consistent state.

System/Application Overview

Flat Transactions

A transaction is an atomic unit of work executed by applications taking the role of a client to a database server. In the simplest case, several application servers talk to a single database. The example below includes two SQL commands into a single transaction and uses the Java Transaction API [1]. Either both commands succeed, or neither of them do.

// Obtain a JNDI Context
ctx = new InitialContext(env);
// Obtain a UserTransaction Object
UserTransaction tx = (UserTransaction)
    ctx.lookup("javax.transaction.UserTransaction");
/*
 * Begin a transaction
 */
tx.begin();
try {
    /*
     * Body of transaction code.
     */

    //Issue SQL commands to change a reservation
        #sql { DELETE FROM SALES WHERE ITEM_NAME='old_reservation'};
        #sql { INSERT INTO SALES(ITEM_NAME) VALUES ('new_reservation')};
    } catch (SQLException e) {
    /*
     * Handle the error and rollback the transaction.
     */
    tx.rollback();
    }
/*
 * Complete processing prior to commit.
 */
tx.commit();

/*
 * Transaction completed.
 */

This is a simple flat transaction. The semantics of a flat transaction are straightforward to understand. A flat transaction takes several SQL statements and reduces them to a single unit of work.

Nested Transactions

Consider a developer building a software component for managing users and groups. Amy provides an API in his component for adding and removing a user from a group. These methods take several SQL statements to keep the database consistent, so the developer uses a transaction. The developer writes the necessary code and unit tests and finds that the system works.

/*
 *  Adds a user to this group
 *  @param user The user to be added to the group.
 */
public add(User user) throws GroupException{
// Obtain a JNDI Context
ctx = new InitialContext(env);
// Obtain a UserTransaction Object
UserTransaction tx = (UserTransaction)
    ctx.lookup("javax.transaction.UserTransaction");
tx.begin();
try {
    #sql { INSERT INTO GROUP_USER_MAP(USER_ID  ...) };
    #sql { INSERT INTO GROUP_PARTY_MAP (USER_ID ...) };
    // More SQL ...
    } catch (SQLException e) {
    tx.rollback();
    throw new GroupException("Group creation failed.");
    }
tx.commit();

Ken finds Amy's group management component useful and decides to use it in his software. He builds a user registration page that includes several steps

  1. Create a user.

  2. Create a user profile.

  3. Add user to groups based on personalized profile.

Ken's registration routine requires that all of these steps succeed to keep the database consistent, so he specifies that they are a transaction. However, Christine's group component also contains a transaction.

// Obtain a JNDI Context
ctx = new InitialContext(env);
// Obtain a UserTransaction Object
UserTransaction tx = (UserTransaction)
    ctx.lookup("javax.transaction.UserTransaction");
tx.begin();
try {
    User user = new User("John", "Doe");
    UserProfile profile = new UserProfile(user, UserType.STAR_TREK_FAN);
    GroupManager.findGroup("Star Trek Fans").add(user);
} catch (SQLException e) {
    tx.rollback();
}
tx.commit();

When Ken tests this code, he finds that the attempt to add a user fails with a NotSupportedException. Oracle does not support nested transactions [4]. Few database vendors in Q1 2001 provide such support. The only route to nested transactions is through specialized transaction software and TP monitors.

This situation needs resolution. Amy wants to specify that her code is a transaction. She wants to guarantee that no matter what client is using the code, all of the statements run, or none do. There are four options.

  1. Transaction by pre-condition

    A pre-condition is an explicit assumption that is part of a method's contract. If Amy specifies that the code must be run within a transaction, then the only supported way to use the code is if a transaction is started before calling the method.

    Amy could enforce this precondition by checking it in the code itself.

    // Obtain a UserTransaction Object
    UserTransaction tx = (UserTransaction)
        ctx.lookup("javax.transaction.UserTransaction");
    if (tx.getStatus() == javax.transaction.Status.STATUS_ACTIVE) {
        throw new GroupException("Must be run as a transaction");
    }

    There are some problems with this approach:

    • Java does not provide a good way for checking and enforcing pre-conditions.

    • The pre-condition places a requirement on the client of the method to comply with the pre-condition.

    • In order to guarantee the contract, the pre-condition requires the implementor of the method to check if a transaction is open or not.

    The pre-condition requires a fair amount of code to be written to ensure the contract. One could formalize this precondition as part of a coding standard. For instance, it could be required that every request is considered a transaction. With such a coding standard, much of the work described can be taken care of at the architectural level.

  2. Pseudo-nested transaction

    A pseudo-nested transaction allows for the use of a transaction within a transaction, but without providing true nested transaction functionality. A pseudo-nested transaction is typically implemented by providing a wrapper around the begin() and commit() methods. The first time begin() is called, the transaction is started. Each subsequent time begin() is called, a count of the number of the transaction depth is incremented. Every time commit()is called, that counter is decremented. When that count reaches 0, the transaction is ended.

    If an error is encountered with a pseudo-nested transaction, the rollback() method is executed. This flags the transaction to be aborted. However, because this is a pseudo-transaction, there are no clear semantics on how to handle subsequent begin() or commit() commands. Generally a coding standard is required to solve this problem, such as requiring that all components that contain a transaction throw a SQLException to signify the failure of the transaction. Any author of a new transaction must catch such that exception type and then rollback the transaction.

    Pseudo-nested transactions provide a means of components maintaining a transaction contract in a modular way. However, this comes at the expense of confusing language semantics that are difficult to explain and can lead to confusing situations.

  3. Nested subtransaction

    A nested subtransaction commits with respect to the parent transaction [2]. That is, even though the subtransaction commits, the permanence of its effects depends on the parent transaction committing. If the parent transaction aborts, the results of the nested transaction are backed out. However, if the nested transaction aborts, the parent transaction is not aborted.

    This option resolves the semantic problems of pseudo-nested transactions. It is clear exactly what the commit() and rollback() commands accomplish. The modularity of the component is preserved. This kind of transaction does require that the component author signal an exception to the caller so that the caller can decide to abort the top-level transaction or work around the problem. Unfortunately, it is not possible to achieve this functionality through Oracle or most other database vendors. This option is therefore ruled out.

  4. Nested top-level transaction

    A nested top-level transaction commits or aborts independently of the enclosing transaction. That is, after it is created, it is completely independent of the transaction that created it.

    In many cases, subtransactions are preferred to top-level nested transactions because they provide a means of associating a transaction with a larger body of work. A nested top-level transaction, once committed, cannot be rolled back, even if the enclosing transaction aborts. Unfortunately, as with nested subtransactions is not possible to achieve this functionality through Oracle or most other database vendors. This option is therefore ruled out.

In summary, providing support for nested subtransactions with or without nested top-level transactions is preferable to transactions as architectural pre-conditions or pseudo-nested transactions.

Distributed Transactions and Two-Phase Commit Protocol

Transactions are often described within a framework of local transactions. A local transaction is a transaction to one resource manager, also called a data source.

In contrast, a distributed transaction is a transaction between multiple data sources. For instance, a commerce application might initiate a distributed transaction between a company's data source and an external order fulfillment data source.

Distributed transactions are a necessary tool for performing transactional operations across multiple data stores. This is important if an operation, such as buying a product, requires coordinating multiple vendors and only committing a transaction if success is registered from all associated transaction managers.

Alternatively, if one of the resource managers had signaled failure in the prepare or commit phases, the transaction coordinator would have ensured that the transactions on all resource managers were rolled back.

The Java Transaction API provides a specification for managing distributed transactions according to the XA specification defined by X/Open. Oracle provides an implementation of these features which are also a requirement of the J2EE 1.3 specification. The transaction API does not need to manage any of the logistics of distributed transactions, but must interoperate with code that requires it so that enterprise clients can use the API in tandem with their other systems.

Use-cases and User-scenarios

  1. Use Case 1: Room Reservation Joe User wants to reserve a conference room. He finds that there is a vacancy available. He leaves his computer without confirming the reservation. Jill User goes to reserve a conference room, and finds the same room, and starts to confirm the reservation. Joe comes back, and tries to confirm the reservation while the transaction processor is working to fulfill Jill's transactions. The transaction processor waits for Jill's transaction, which includes the same resource as the transaction Joe is trying to start, to finish, before starting Joe's transactions. Since Jill confirmed her reservation, Joe's transaction cannot be completed and he is informed that the room is already been reserved.

  2. Use Case 2: Distributed Transaction

    1. Bill buys a tennis racket through a commerce store front maintained by Enterprise Tennis.

    2. The commerce store front application opens a distributed transaction through a transaction manager.

    3. Database commands are issued to the Enterprise Tennis order database.

    4. Database commands are issued to Fred's Fulfillment to ship a tennis racket.

    5. The transaction manager completes the prepare phase and initiates the commit phase.

    6. The resource managers for Enterprise Tennis and Fred's Fulfillment vote to commit.

    7. The databases all commit and the transaction manager signals success.

Competitive Analysis

Pseudo-transactions are provided by Persistence [6] and the Red Hat Web Application Framework (ACS) 4 Database API. Experience with the Red Hat Web Application Framework (ACS) 4 database API has shown that pseudo-trans can be difficult to support because of confusing rollback() semantics.

Nested subtransactions and nested top-level transactions are provided by Encina [2].

The Java Transaction API defines a specification for a transaction API. J2EE vendors, such as BEA and Oracle, implement it.

Requirements

  • 10.0 Auditing Requirements

    Every transaction needs to have information available for auditing. Such information can be used for generating reports and analysis of transaction usage.

    10.0.5 Transaction log

    A log of all transactions and the results of their processing must be kept to support administration.

    • Start time

    • Stop time

    • Result and error if applicable

    • Participating Resource Managers

    10.0.10 Transaction Identification

    Creates a unique transaction identifier when a client application initiates a transaction.

    10.0.15 Transaction Naming

    Supports an optional transaction name describing the business process that the transaction represents. The transaction name makes statistics and error messages more meaningful.

  • 20.0 Flat Transactions

    Flat transaction semantics require a standard set of commands and situations to be supported

    20.0.5 BEGIN

    Indicates that a transaction is starting.

    20.0.10 COMMIT

    Indicates that the transaction should commit its results.

    20.0.15 ROLLBACK

    Indicates that the transaction has been aborted.

    20.0.20 ON COMMIT

    Provides a way to run code after a transaction commits, but not if it aborts.

    20.0.25 ON ABORT

    Provides a way to run after a transaction aborts, but not if it commits.

    20.0.30 CATCH ABORT

    Provides a way to catch an aborting transaction, to handle the error, and optionally issue a commit.

    20.0.35 Transaction Status

    Provides a way to check the status of a current transaction.

    20.0.40 Isolation Level

    Provides a way to check the level of isolation within a transaction.

    20.0.45 Transaction timeouts

    Provides a way to cancel any running transactions if their running time exceeds a time limit.

  • 30.0 Components and Transactions

    Component code never manages the transaction context explicitly. This means that a component's methods are not allowed to open, commit, or rollback a transaction on their own. A component's method is capable of indicating what kind of transactional status it requires. The API must provide a standard way for methods to comply with the conditions specified below.

    30.0.5 Required

    This condition specifies that a component's method is must be invoked within a transactional context. The method will throw an exception otherwise if it is invoked outside of a transactional context.

    30.0.15 NotSupported

    This condition specifies that a component's method cannot run within a transactional context. The method will throw an exception if it is invoked inside of a It will throw an exception

    30.0.20 Supports

    This condition specifies that the method can run within a transactional context, but doesn't need to.

    30.0.25 EJB Compatibility

    The design and implementation of these conditions must be compatible with the EJB specification. It is acceptable to consider the implementation as a form of a bean-managed transaction.

  • 40.0 Distributed Transactions

    The transaction API must support distributed transactions.

    40.0.5 Two-phase Commit Protocol

    The standard two-phase commit protocol is supported.

    40.0.10 Compatible with XA implementing resource managers.

    Any XA implementing resource manager is supported.

  • 50.0 General API Requirements

    50.0.5 Layered above Oracle

    The transaction API must sit above Oracle and involve abstracted operations, such as Transaction.start(). We cannot rely on the JDBC command for turning auto-commit mode off, or sending special strings to Oracle.

    50.0.10 Scalable

    The API must allow any number of transactions to involve the same resources at a given time. There should be no increased bottleneck over directly communicating with the transaction managers.

    50.0.15 Engineering Standards

    The transaction API's documentation must identify how developers are to write code that use transactions.

Implementation Notes

The requirements for the transaction API are mostly met by the Java Transaction API specification and implemented by Oracle. However, a wrapper does need to be provided to provide some functionality, such as auditing. The Weblogic Transaction API has some good examples of how this can be achieved. There will be some kind of dispatching mechanism used to manage requests to the application server. This dispatcher might be used to start and end transactional contexts.