Isolation for Entities with Bean Managed Persistence

If an Entity EJB is using Bean Managed Persistence, the developer must take care to ensure that the correct isolation level is propogated down to the database. This section covers the issues to be considered when coding the different container callbacks.

findByPrimaryKey

A common trick with BMP Entities is to have findByPrimaryKey simply return the supplied key without performing a database operation. For example:

public MyPKClass findByPrimaryKey(MyPKClass pk) {
    return pk;
}

This has the advantage of eliminating a trip to the database to check that the row still exists, relying on detecting if the row has been deleted during ejbLoad(). Another side effect of this operation is that because no database call is made, then no lock is taken in the database potentially allowing other transactions to modify the data underneath this entity. This is generally acceptable as the finder is simply establishing the existance of this entity and not accessing any data outside the primary key; however, for applications where Serializable isolation is a critical requirement, this method should be treated like any other finder and perform a read-only or write-intent select as appropriate.

Other Finders

Other finders, however, select entities based on fields outside the primary key or even from other entities. In these circumstances, care must be taken to use the appropriate isolation in the database and to indicate the read or write intent of the transaction.

If the desired isolation level is Read-Committed, a normal SELECT query can be used returning a READ_ONLY ResultSet; for example, a finder that returns orders shipping to California could be written:

/**
 * Finder which returns orders for a state.
 * This implementation assumes data will only be read.
 */
public Set findOrdersShippingToState(String state) {
    c = ds.getConnection();
    ps = c.prepareStatement(
        "SELECT O.ORDER_ID "+
        "FROM ORDER_DATA O "+
        "INNER JOIN ADDRESS A ON O.SHIP_TO = A.ADDRESS_ID "+
        "WHERE A.STATE = ?"
        );
    ps.setString(1, state);
    rs = ps.executeQuery();
    while (rs.next()) {
        results.add(new OrderPK(rs.getInt(1)));
    }
    return results;
}

If a higher level of isolation is required, then the DataSource used should be set to the appropriate level and, if the data may be updated, the query should indicate its intent to update. In many circumstances, the Bean Provider does not know whether the client will, in the same transaction, go on to update data and so should defensively assume that this could happen and take the appropriate locks every time. Alternatively, two different finders could be provided, one which takes update-intent locks and one which does not.

The finder above could be re-written using an updatable ResultSet as:

/**
 * Finder which returns orders for a state.
 * This implementation assumes data will be updated
 * later in the same transaction.
 */
public Set findOrdersShippingToStateForUpdate(String state) {
    c = ds.getConnection();
    ps = c.prepareStatement(
        "SELECT O.ORDER_ID "+
        "FROM ORDER_DATA O "+
        "INNER JOIN ADDRESS A ON O.SHIP_TO = A.ADDRESS_ID "+
        "WHERE A.STATE = ?",
        ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE
        );
    ps.setString(1, state);
    rs = ps.executeQuery();
    while (rs.next()) {
        results.add(new OrderPK(rs.getInt(1)));
    }
    return results;
}

ejbLoad()

The ejbLoad() callback is used by the container to synchronize the entity's state from the database. As with finders, it is important that the isolation level of the transaction and the read or write intent of the transaction is passed down to the database. Unfortunately, as this is a container callback, communicating the read or write intent from the client application is difficult (and non-portable).

If the EJB allows clients to interact directly with business methods, the safest option is to assume nothing about the intent of the transaction and if a Serializable isolation level may be an option then always use a write-intent (SELECT FOR UPDATE) query to load the data.

However, if the EJB's component interface is not intended for direct client use (for example, it is hidden behind a session façade) then the read or write intent can be established prior to ejbLoad() being called. For example, a different finder could be used for read- and write-intent transactions in order to establish the appropriate database locks; the query in ejbLoad() would then just be a simple read-only version.

As an example, assume we wished to modify all orders being shipped to California, a business method in the Session façade could be written like:

/**
 * Method to update all orders shipping to a state
 */
public void updateAllOrdersByState(String state) {
    Collection orders = orderHome.findOrdersShippingToStateForUpdate(state);
    for (Iterator i = orders.iterator(); i.hasNext(); ) {
        OrderLocal order = (OrderLocal) i.next();
        order.doUpdate(...);
    }
}

Using the ForUpdate finder ensures that all rows in the database are write-intent locked so that the read/modify/write performed in the iterator loop never needs to take an exclusive lock (as it was already taken by the finder). This, of course, assumes that the finder and the updates all occur in the same transaction.