home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


CONTENTS

Chapter 10. Bean-Managed Persistence

From the developer's point of view, bean-managed persistence (BMP) requires more effort than container-managed persistence, because you must explicitly write the persistence logic into the bean class. In order to write the persistence-handling code into the bean class, you must know what type of database is being used and the how the bean class's fields map to that database.

Given that container-managed persistence saves a lot of work, why would anyone bother with bean-managed persistence? The main advantage of BMP is that it gives you more flexibility in how state is managed between the bean instance and the database. Entity beans that use data from a combination of different databases or other resources such as legacy systems can benefit from BMP. Essentially, bean-managed persistence is the alternative to container-managed persistence when the container tools are inadequate for mapping the bean instance's state to the backend databases or resources.

The primary disadvantage of BMP is obvious: more work is required to define the bean. You have to understand the structure of the database or resource and the APIs that access them, and you must develop the logic to create, update, and remove data associated with entities. This requires diligence in using the EJB callback methods (e.g., ejbLoad() and ejbStore()) appropriately. In addition, you must explicitly develop the find methods defined in the bean's home interfaces.

The select methods used in EJB 2.0 container-managed persistence are not supported in bean-managed persistence.

Another disadvantage of BMP is that it ties the bean to a specific database type and structure. Any changes in the database or in the structure of data require changes to the bean instance's definition, and these changes may not be trivial. A bean-managed entity is not as database independent as a container-managed entity, but it can better accommodate a complex or unusual set of data.

To help you understand how BMP works, we will create a new Ship EJB that is similar to the one used in Chapter 7 and Chapter 9. For BMP, we need to implement the ejbCreate(), ejbLoad(), ejbStore(), and ejbRemove() methods to handle synchronizing the bean's state with the database.

EJB 1.1 readers will notice that some of the information in this chapter is repeated from Chapter 9, which covers CMP 1.1. However, for EJB 2.0 readers, who will probably skip Chapter 9, most of this material will be new. EJB 1.1 readers are encouraged to skim familiar material as it is encountered.

10.1 The Remote Interface

We will need a remote interface for the Ship EJB. This interface is basically the same as any other remote or local interface. It defines the business methods used by clients to interact with the bean:

package com.titan.ship;

import javax.ejb.EJBObject;
import java.rmi.RemoteException;

public interface ShipRemote extends javax.ejb.EJBObject {
    public String getName() throws RemoteException;
    public void setName(String name) throws RemoteException;
    public void setCapacity(int cap) throws RemoteException;
    public int getCapacity() throws RemoteException;
    public double getTonnage() throws RemoteException;
    public void setTonnage(double tons) throws RemoteException;
}

We will not develop a local interface for the bean-managed Ship bean in this chapter; however, in EJB 2.0, bean-managed entity beans can have either local or remote component interfaces, just like container-managed entity beans.

10.1.1 Set and Get Methods

The ShipRemote definition uses a series of accessor methods whose names begin with " set" and "get." This is not a required signature pattern, but it is the naming convention used by most Java developers when obtaining and changing the values of object attributes or fields. These methods are often referred to as setters and getters (a.k.a. mutators and accessors), and the attributes they manipulate are called properties.[1] Properties should be defined independently of the anticipated storage structure of the data. In other words, you should design the remote interface to model the business concepts, not the underlying data. Just because there's a capacity property doesn't mean that there has to be a capacity field in the bean or the database; the getCapacity() method could conceivably compute the capacity from a list of cabins by looking up the ship's model and configuration, or with some other algorithm.

Defining entity properties according to the business concept and not the underlying data is not always possible, but you should try to employ this strategy whenever you can. The reason is twofold. First, the underlying data doesn't always clearly define the business purpose or concept being modeled by the entity bean. Remote interfaces are often used by developers who know the business but not the database configuration. It is important to them that the entity bean reflect the business concept. Second, defining the properties of the entity bean independently of the data allows the bean and data to evolve separately. This is important because it allows a database implementation to change over time; it also allows for new behavior to be added to the entity bean as needed. If the bean's definition is independent of the data source, the impact of these developments is limited.

10.2 The Remote Home Interface

Entity beans' home interfaces (local and remote) are used to create, locate, and remove objects from EJB systems. Each entity bean has its own remote or local home interface. The home interface defines two basic kinds of methods: zero or more create methods, and one or more find methods.[2] In this example, the create methods act like remote constructors and define how new Ship EJBs are created. (In our home interface, we provide only a single create method.) The find method is used to locate a specific Ship or Ships.The following code contains the complete definition of the ShipHomeRemote interface:

package com.titan.ship;

import javax.ejb.EJBHome;
import javax.ejb.CreateException;
import javax.ejb.FinderException;
import java.rmi.RemoteException;
import java.util.Collection;

public interface ShipHomeRemote extends javax.ejb.EJBHome {

    public ShipRemote create(Integer id, String name, int capacity, double tonnage)
        throws RemoteException,CreateException;
    public ShipRemote create(Integer id, String name)
        throws RemoteException,CreateException;
    public ShipRemote findByPrimaryKey(Integer primaryKey)
        throws FinderException, RemoteException;

    public Collection findByCapacity(int capacity)
        throws FinderException, RemoteException;
}

Enterprise JavaBeans specifies that create methods in the home interface must throw the javax.ejb.CreateException. This provides the EJB container with a common exception for communicating problems experienced during the create process.

The RemoteException is thrown by all remote interfaces and is used to report network problems that occurred while processing invocations between a remote client and the EJB container system.

10.3 The Primary Key

In bean-managed persistence, a primary key can be a serializable object defined specifically for the bean by the bean developer. The primary key defines attributes we can use to locate a specific bean in the database. For the ShipBean we need only one attribute, id, but in other cases a primary key may have several attributes, which together uniquely identify a bean's data.

We will examine primary keys in detail in Chapter 11; for now, we specify that the Ship EJB uses a simple single-value primary key of type java.lang.Integer. The actual persistence field in the bean class is an Integer named id.

10.4 The ShipBean

The ShipBean defined for this chapter uses JDBC to synchronize the bean's state to the database. In reality, an entity bean this simple could easily be deployed as a CMP bean. The purpose of this chapter, however, is to illustrate exactly where the resource-access code goes for BMP and how to implement it. When learning about bean-managed persistence, you should focus on when and where the resource is accessed in order to synchronize the bean with the database. The fact that we are using JDBC and synchronizing the bean state against a relational database is not important. The bean could just as easily be persisted to some legacy system, to an ERP application, or to some other resource that is not supported by your vendor's version of CMP, such as LDAP or a hierarchical database.

Here is the complete definition of the ShipBean:

package com.titan.ship;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.ejb.EntityContext;
import java.rmi.RemoteException;
import java.sql.SQLException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.DriverManager;
import java.sql.ResultSet;
import javax.sql.DataSource;
import javax.ejb.CreateException;
import javax.ejb.EJBException;
import javax.ejb.FinderException;
import javax.ejb.ObjectNotFoundException;
import java.util.Collection;
import java.util.Properties;
import java.util.Vector;
import java.util.Collection;

public class ShipBean implements javax.ejb.EntityBean {
    public Integer id;
    public String name;
    public int capacity;
    public double tonnage;

    public EntityContext context;
       
    public Integer ejbCreate(Integer id, String name, int capacity, double tonnage) 
        throws CreateException {
        if ((id.intValue() < 1) || (name == null))
            throw new CreateException("Invalid Parameters");
        this.id = id;
        this.name = name;
        this.capacity = capacity;
        this.tonnage = tonnage;
        
        Connection con = null;
        PreparedStatement ps = null;
        try {
            con = this.getConnection();
            ps = con.prepareStatement(
                "insert into Ship (id, name, capacity, tonnage) " +
                "values (?,?,?,?)");
            ps.setInt(1, id.intValue());
            ps.setString(2, name);
            ps.setInt(3, capacity);
            ps.setDouble(4, tonnage);
            if (ps.executeUpdate() != 1) {
                throw new CreateException ("Failed to add Ship to database");
            }
            return id;
        }
        catch (SQLException se) {
            throw new EJBException (se);
        }
        finally {
            try {         
                if (ps != null) ps.close(); 
                if (con!= null) con.close(); 
            } catch(SQLException se) {
                se.printStackTrace();
            }
        }
    }
    public void ejbPostCreate(Integer id, String name, int capacity, 
        double tonnage) {
        // Do something useful with the primary key.
    }
    public Integer ejbCreate(Integer id, String name) throws CreateException {
        return ejbCreate(id,name,0,0);
    }
    public void ejbPostCreate(Integer id, String name) {
        // Do something useful with the EJBObject reference.
    }
    public Integer ejbFindByPrimaryKey(Integer primaryKey) throws FinderException {
        Connection con = null;
        PreparedStatement ps = null;
        ResultSet result = null;
        try {
            con = this.getConnection();
            ps = con.prepareStatement("select id from Ship where id = ?");
            ps.setInt(1, primaryKey.intValue());
            result = ps.executeQuery();
            // Does the ship ID exist in the database?
            if (!result.next()) {
                throw new ObjectNotFoundException("Cannot find Ship with id = "+id);
            }
        } catch (SQLException se) {
            throw new EJBException(se);
        }
        finally {
            try {
                if (result != null) result.close();
                if (ps != null) ps.close(); 
                if (con!= null) con.close(); 
            } catch(SQLException se){
                se.printStackTrace();
            }
        }
        return primaryKey;
    }
    public Collection ejbFindByCapacity(int capacity) throws FinderException {
        Connection con = null;
        PreparedStatement ps = null;
        ResultSet result = null;        
        try {
            con = this.getConnection();
            ps = con.prepareStatement("select id from Ship where capacity = ?");
            ps.setInt(1,capacity);
            result = ps.executeQuery();
            Vector keys = new Vector();
            while(result.next()) {
                keys.addElement(result.getObject("id"));
            }
            return keys;

        }
        catch (SQLException se) {
            throw new EJBException (se);
        }
        finally {
            try {
                if (result != null) result.close();
                if (ps != null) ps.close(); 
                if (con!= null) con.close(); 
            } catch(SQLException se) {
                se.printStackTrace();
            }
        }      
    }
    public void setEntityContext(EntityContext ctx) {
        context = ctx;
    }
    public void unsetEntityContext() {
        context = null;
    }
    public void ejbActivate() {}
    public void ejbPassivate() {}
    public void ejbLoad() {

        Integer primaryKey = (Integer)context.getPrimaryKey();
        Connection con = null;
        PreparedStatement ps = null;
        ResultSet result = null;
        try {
            con = this.getConnection();
            ps = con.prepareStatement("select name, capacity, 
                tonnage from Ship where id = ?");
            ps.setInt(1, primaryKey.intValue());
            result = ps.executeQuery();
            if (result.next()){
                id =primaryKey;
                name = result.getString("name");
                capacity = result.getInt("capacity");
                tonnage = result.getDouble("tonnage");
            } else {
                throw new EJBException();
            }
        } catch (SQLException se) {
            throw new EJBException(se);
        }
        finally {
            try {
                if (result != null) result.close();
                if (ps != null) ps.close(); 
                if (con!= null) con.close(); 
            } catch(SQLException se) {
                se.printStackTrace();
            }
        }      
    }
    public void ejbStore() {
        Connection con = null;
        PreparedStatement ps = null;
        try {
            con = this.getConnection();
            ps = con.prepareStatement(
                "update Ship set name = ?, capacity = ?, " +
                "tonnage = ? where id = ?");
            ps.setString(1,name);
            ps.setInt(2,capacity);
            ps.setDouble(3,tonnage);
            ps.setInt(4,id.intValue());
            if (ps.executeUpdate() != 1) {
                throw new EJBException("ejbStore");
            }
        }
        catch (SQLException se) {
            throw new EJBException (se);
        }
        finally {
            try {
                if (ps != null) ps.close(); 
                if (con!= null) con.close(); 
            } catch(SQLException se) {
                se.printStackTrace();
            }
        }
    }
    public void ejbRemove() {
        Connection con = null;
        PreparedStatement ps = null;
        try {
            con = this.getConnection();
            ps = con.prepareStatement("delete from Ship where id = ?");
            ps.setInt(1, id.intValue());
            if (ps.executeUpdate() != 1) {
                throw new EJBException("ejbRemove");
            }
        }
        catch (SQLException se) {
            throw new EJBException (se);
        }
        finally {
            try {
                if (ps != null) ps.close(); 
                if (con!= null) con.close(); 
            } catch(SQLException se) {
                se.printStackTrace();
            }
        }
    }
    public String getName() {
        return name;
    }
    public void setName(String n) {
        name = n;
    }
    public void setCapacity(int cap) {
        capacity = cap;
    }
    public int getCapacity() {
        return capacity;
    }
    public double getTonnage() {
        return tonnage;
    }
    public void setTonnage(double tons) {
        tonnage = tons;
    }
    private Connection getConnection() throws SQLException {
         // Implementations shown below.
   }
}

10.5 Obtaining a Resource Connection

In order for a BMP entity bean to work, it must have access to the database or resource to which it will persist itself. To get access to the database, the bean usually obtains a resource factory from the JNDI ENC. The JNDI ENC is covered in detail in Chapter 12, but an overview here will be helpful since this is the first time it is actually used in this book. The first step in accessing the database is to request a connection from a DataSource, which we obtain from the JNDI environment naming context:

private Connection getConnection() throws SQLException {
    try {
        Context jndiCntx = new InitialContext();
        DataSource ds = (DataSource)jndiCntx.lookup("java:comp/env/jdbc/titanDB");
        return ds.getConnection();
    }
    catch (NamingException ne) {
        throw new EJBException(ne);
    }
}

In EJB, every enterprise bean has access to its JNDI environment naming context (ENC), which is part of the bean-container contract. The bean's deployment descriptor maps resources such as the JDBC DataSource, JavaMail, and Java Message Service to a context (name) in the ENC. This provides a portable model for accessing these types of resources. Here's the relevant portion of the deployment descriptor that describes the JDBC resource:

<enterprise-beans>
    <entity>
        <ejb-name>ShipEJB</ejb-name>
        ...
        <resource-ref>
            <description>DataSource for the Titan database</description>
            <res-ref-name>jdbc/titanDB</res-ref-name>
            <res-type>javax.sql.DataSource</res-type>
            <res-auth>Container</res-auth>
        </resource-ref>
        ...
    </entity>
    ...
</enterprise-beans>

The <resource-ref> tag is used for any resource (e.g., JDBC, JMS, JavaMail) that is accessed from the ENC. It describes the JNDI name of the resource (<res-ref-name>), the factory type (<res-type>), and whether authentication is performed explicitly by the bean or automatically by the container (<res-auth>). In this example, we are declaring that the JNDI name jdbc/titanDB refers to a javax.sql.DataSource resource manager and that authentication to the database is handled automatically by the container. The JNDI name specified in the <res-ref-name> tag is always relative to the standard JNDI ENC context name, java:comp/env.

When the bean is deployed, the deployer maps the information in the <resource-ref> tag to a live database. This is done in a vendor-specific manner, but the end result is the same. When a database connection is requested using the JNDI name java:comp/jdbc/titanDB, a DataSource for the Titan database is returned. Consult your vendor's documentation for details on how to map the DataSource to the database at deployment time.

The getConnection() method provides us with a simple and consistent mechanism for obtaining a database connection for our ShipBean class. Now that we have a mechanism for obtaining a database connection, we can use it to insert, update, delete, and find Ship EJBs in the database.

10.6 Exception Handling

Exception handling is particularly relevant to this discussion because, unlike in container-managed persistence, in bean-managed persistence the bean developer is responsible for throwing the correct exceptions at the right moments. For this reason, we'll take a moment to discuss the different types of exceptions in BMP. This discussion will be useful when we get into the details of database access and implementing the callback methods.

Bean-managed beans throw three types of exceptions:

Application exceptions

Application exceptions include standard EJB application exceptions and custom application exceptions. The standard EJB application exceptions are CreateException, FinderException, ObjectNotFoundException, DuplicateKeyException, and RemoveException. These exceptions are thrown from the appropriate methods to indicate that a business logic error has occurred. Custom exceptions are exceptions developed for specific business problems. We cover developing custom exceptions in Chapter 12.

Runtime exceptions

Runtime exceptions are thrown from the virtual machine itself and indicate that a fairly serious programming error has occurred. Examples include the NullPointerException and IndexOutOfBoundsException. These exceptions are handled by the container automatically and should not be handled inside a bean method.

You will notice that all the callback methods (ejbLoad(), ejbStore(), ejbActivate(), ejbPassivate(), and ejbRemove()) throw an EJBException when a serious problem occurs. All EJB callback methods declare the EJBException and RemoteException in their throws clauses. Any exception thrown from one of the callback methods must be an EJBException or a subclass. The RemoteException type is included in the method signature to support backward compatibility with EJB 1.0 beans. Its use has been deprecated since EJB 1.1. RemoteExceptions should never be thrown by callback methods of EJB 1.1 or EJB 2.0 beans.

Subsystem exceptions

Checked exceptions thrown by other subsystems should be wrapped in an EJBException or application exception and rethrown from the method. Several examples of this can be found in the previous example, in which an SQLException that was thrown from JDBC was caught and rethrown as an EJBException. Checked exceptions from other subsystems, such as those thrown from JNDI, JavaMail, and JMS, should be handled in the same fashion. The EJBException is a subtype of the RuntimeException, so it doesn't need to be declared in the method's throws clause. If the exception thrown by the subsystem is not serious, you can opt to throw an application exception, but this is not recommended unless you are sure of the cause and the effects of the exception on the subsystem. In most cases, throwing an EJBException is preferred.

Exceptions have an impact on transactions and are fundamental to transaction processing. They are examined in greater detail in Chapter 14.

10.7 The ejbCreate( ) Method

ejbCreate() methods are called by the container when a client invokes the corresponding create() method on the bean's home. With bean-managed persistence, the ejbCreate() methods are responsible for adding new entities to the database. This means that the BMP version of ejbCreate() will be much more complicated than the equivalent methods in container-managed entities; with container-managed beans, ejbCreate() doesn't have to do much more than initialize a few fields. Another difference between bean-managed and container-managed persistence is that the EJB specification states that ejbCreate() methods in bean-managed persistence must return the primary key of the newly created entity. By contrast, in container-managed beans ejbCreate() is required to return null.

The following code contains the ejbCreate() method of the ShipBean. Its return type is the Ship EJB's primary key, Integer. The method uses the JDBC API to insert a new record into the database based on the information passed as parameters:

public Integer ejbCreate(Integer id, String name, int capacity, double tonnage) 
    throws CreateException {
    if ((id.intValue() < 1) || (name == null))
        throw new CreateException("Invalid Parameters");
    this.id = id;
    this.name = name;
    this.capacity = capacity;
    this.tonnage = tonnage;
        
    Connection con = null;
    PreparedStatement ps = null;
    try {
        con = this.getConnection();
        ps = con.prepareStatement(
            "insert into Ship (id, name, capacity, tonnage) " +
            "values (?,?,?,?)");
        ps.setInt(1, id.intValue());
        ps.setString(2, name);
        ps.setInt(3, capacity);
        ps.setDouble(4, tonnage);
        if (ps.executeUpdate() != 1) {
            throw new CreateException ("Failed to add Ship to database");
        }
        return id;
    }
    catch (SQLException se) {
        throw new EJBException (se);
    }
    finally {
        try {         
            if (ps != null) ps.close(); 
            if (con!= null) con.close(); 
        } catch(SQLException se) {
            se.printStackTrace();
        }
    }
}

At the beginning of the method, we verify that the parameters are correct and throw a CreateException if the id is less than 1 or the name is null. This shows how you would typically use a CreateException to report an application-logic error.

The ShipBean instance fields are initialized using the parameters passed to ejbCreate() by setting the instance fields of the ShipBean. We will use these values to manually insert the data into the SHIP table in our database.

To perform the database insert, we use a JDBC PreparedStatement for SQL requests, which makes it easier to see the parameters being used (we could also have used a stored procedure through a JDBC CallableStatement or a simple JDBC Statement object). We insert the new bean into the database using a SQL INSERT statement and the values passed into ejbCreate() parameters. If the insert is successful (i.e., no exceptions are thrown), we create a primary key and return it to the container.

If the insert operation is unsuccessful we throw a new CreateException, which illustrates this exception's use in a more ambiguous situation. Failure to insert the record could be construed as an application error or a system failure. In this situation, the JDBC subsystem hasn't thrown an exception, so we shouldn't interpret the inability to insert a record as a failure of the subsystem. Therefore, we throw a CreateException instead of an EJBException. Throwing a CreateException allows the application to recover from the error, a transactional concept that is covered in more detail in Chapter 14.

When the insert operation is successful, the primary key is returned to the EJB container from the ejbCreate() method. In this case we simply return the same Integer object passed into the method, but in many cases a new key might be derived from the method arguments. This is especially true when using compound primary keys, which are discussed in Chapter 11. Behind the scenes, the container uses the primary key and the ShipBean instance that returned it to provide the client with a reference to the new Ship entity. Conceptually, this means that the ShipBean instance and primary key are assigned to a newly constructed EJB object, and the EJB object stub is returned to the client.

Our home interface requires us to provide a second ejbCreate() method with different parameters. We can save work and write more bulletproof code by making the second method call the first:

public Integer ejbCreate(Integer id, String name) throws CreateException {
    return ejbCreate(id,name,0,0);
}

10.8 The ejbLoad( ) and ejbStore( ) Methods

Throughout the life of an entity, its data will be changed by client applications. In the ShipBean, we provide accessor methods to change the name, capacity, and tonnage of the Ship EJB after it has been created. Invoking any of these accessor methods changes the state of the ShipBean instance, and these changes must be reflected in the database.

In container-managed persistence, synchronization between the entity bean and the database takes place automatically; the container handles it for you. With bean-managed persistence, you are responsible for synchronization: the entity bean must read from and write to the database directly. The container works closely with the BMP entities by advising them when to synchronize their state through the use of two callback methods: ejbStore() and ejbLoad().

The ejbStore() method is called when the container decides that it is a good time to write the entity bean's data to the database. The container makes these decisions based on all the activities it is managing, including transactions, concurrency, and resource management. Vendor implementations may differ slightly as to when the ejbStore() method is called, but this is not the bean developer's concern. In most cases, the ejbStore() method will be called after one or more business methods have been invoked or at the end of a transaction.

Here is the ejbStore() method for the ShipBean:

public void ejbStore() {
    Connection con = null;
    PreparedStatement ps = null;
    try {
        con = this.getConnection();
        ps = con.prepareStatement(
            "update Ship set name = ?, capacity = ?, " +
            "tonnage = ? where id = ?");
        ps.setString(1,name);
        ps.setInt(2,capacity);
        ps.setDouble(3,tonnage);
        ps.setInt(4,id.intValue());
        if (ps.executeUpdate() != 1) {
            throw new EJBException("ejbStore");
        }
    }
    catch (SQLException se) {
        throw new EJBException (se);
    }
    finally {
        try {
            if (ps != null) ps.close(); 
            if (con!= null) con.close(); 
        } catch(SQLException se) {
            se.printStackTrace();
        }
    }
}

Except for the fact that we are doing an update instead of an insert, this method is similar to the ejbCreate() method we examined earlier. We use a JDBC PreparedStatement to execute the SQL UPDATE command, and we use the entity bean's persistence fields as parameters to the request. This method synchronizes the database with the state of the bean.

EJB also provides an ejbLoad() method that synchronizes the state of the entity with the database. This method is usually called at the start of a new transaction or business-method invocation. The idea is to make sure that the bean always represents the most current data in the database, which could be changed by other beans or other non-EJB applications.

Here is the ejbLoad() method for a bean-managed ShipBean class:

public void ejbLoad() {

    Integer primaryKey = (Integer)context.getPrimaryKey();
    Connection con = null;
    PreparedStatement ps = null;
    ResultSet result = null;
    try {
        con = this.getConnection();
        ps = con.prepareStatement(
            "select name, capacity, tonnage from Ship where id = ?");
        ps.setInt(1, primaryKey.intValue());
        result = ps.executeQuery();
        if (result.next()){
            id = primaryKey;
            name = result.getString("name");
            capacity = result.getInt("capacity");
            tonnage = result.getDouble("tonnage");
        } else {
            throw new EJBException();
        }
    } catch (SQLException se) {
        throw new EJBException(se);
    }
    finally {
        try {
            if (result != null) result.close();
            if (ps != null) ps.close(); 
            if (con!= null) con.close(); 
        } catch(SQLException se) {
            se.printStackTrace();
        }
    }      
}

To execute the ejbLoad() method, we need a primary key. To get the primary key, we query the bean's EntityContext. Note that we don't get the primary key directly from the ShipBean's id field, because we cannot guarantee that this field is always valid—the ejbLoad() method might be populating the bean instance's state for the first time, in which case the fields would all be set to their default values. This situation would occur following bean activation. We can guarantee that the EntityContext for the ShipBean is valid because the EJB specification requires that the bean instance's EntityContext reference be valid before the ejbLoad() method can be invoked.

At this point you may want to jump to Chapter 11 and read Section 11.4 to get a better understanding of the EntityContext's purpose and usefulness in entity beans.

10.9 The ejbRemove( ) Method

In addition to handling their own inserts and updates, bean-managed entities must handle their own deletions. When a client application invokes the remove method on the EJB home or EJB object, that method invocation is delegated to the bean-managed entity by calling ejbRemove(). It is the bean developer's responsibility to implement an ejbRemove() method that deletes the entity's data from the database. Here's the ejbRemove() method for our bean-managed ShipBean:

public void ejbRemove() {
    Connection con = null;
    PreparedStatement ps = null;
    try {
        con = this.getConnection();
        ps = con.prepareStatement("delete from Ship where id = ?");
        ps.setInt(1, id.intValue());
        if (ps.executeUpdate() != 1) {
            throw new EJBException("ejbRemove");
        }
    }
    catch (SQLException se) {
        throw new EJBException (se);
    }
    finally {
        try {
            if (ps != null) ps.close(); 
            if (con!= null) con.close(); 
        } catch(SQLException se) {
            se.printStackTrace();
        }
    }
}

10.10 The ejbFind( ) Methods

In bean-managed persistence, the find methods in the remote or local home interface must match the ejbFind() methods in the actual bean class. In other words, for each method named find<SUFFIX>() in a home interface, there must be a corresponding ejbFind<SUFFIX>() method in the entity bean class with the same arguments and exceptions. When a find method is invoked on an EJB home, the container delegates the find() method to a corresponding ejbFind() method on the bean instance. The bean-managed entity is responsible for locating records that match the find requests. There are two find methods in ShipHomeRemote:

public interface ShipHomeRemote extends javax.ejb.EJBHome {

    public ShipRemote findByPrimaryKey(Integer primaryKey) 
        throws FinderException, RemoteException;
    public Collection findByCapacity(int capacity) 
        throws FinderException, RemoteException;
}

Here are the signatures of the corresponding ejbFind() methods in the ShipBean:

public class ShipBean extends javax.ejb.EntityBean {

    public Integer ejbFindByPrimaryKey(Integer primaryKey) 
        throws FinderException {}
    public Collection ejbFindByCapacity(int capacity) 
        throws FinderException {}
}

Aside from the names, there's a significant difference between these two groups of methods. The find methods in the home interface returns either an EJB object implementing the bean's remote interface—in this case, ShipRemote—or a collection of EJB objects in the form of a java.util.Enumeration or java.util.Collection. The ejbFind() methods in the bean class, on the other hand, return either a primary key for the appropriate bean—in this case, Integer—or a collection of primary keys. The methods that return a single value (whether a remote/local interface or a primary key) are used whenever you need to look up a single reference to a bean. If you are looking up a group of references (for example, all ships with a certain capacity), you have to use the method that returns either the Collection or Enumeration type. In either case, the container intercepts the primary keys and converts them into remote references for the client.

The EJB 2.0 specification recommends that EJB 2.0 bean-managed persistence beans use the Collection type instead of the Enumeration type. This recommendation is made so that BMP beans are more consistent with EJB 2.0 CMP beans, which use only the Collection type.

It shouldn't come as a surprise that the type returned—whether it's a primary key or a remote (or local, in EJB 2.0) interface—must be appropriate for the type of bean you're defining. For example, you shouldn't put find methods in a Ship EJB to look up and return Cabin EJB objects. If you need to return collections of a different bean type, use a business method in the remote interface, not a find method from one of the home interfaces.

In EJB 2.0, the EJB container takes care of returning the proper (local or remote) interface to the client. For example, the Ship EJB may define a local and a remote home interface, both of which have a findByPrimaryKey() method. When findByPrimary() is invoked on the local or remote interface, it will be delegated to the ejbFindByPrimary() key method. After the ejbFindByPrimaryKey() method executes and returns the primary key, the EJB container takes care of returning a ShipRemote or ShipLocal reference to the client, depending on which home interface (local or remote) was used. The EJB container also handles this for multi-entity find methods, returning a collection of remote references for remote home interfaces or local references for local home interfaces.

Both find methods defined in the ShipBean class throw an EJBException if a failure in the request occurs when an SQL exception condition is encountered. findByPrimaryKey() throws an ObjectNotFoundException if no records in the database match the id argument. This is exception should always be thrown by single-entity find methods if no entity is found.

The findByCapacity() method returns an empty collection if no SHIP records with a matching capacity are found; multi-entity find methods do not throw ObjectNotFoundExceptions if no entities are found.

It is mandatory for all entity remote and local home interfaces to include the findByPrimaryKey() method. This method returns the remote interface type (in this case, Ship). It declares one parameter, the primary key for that bean type. With local home interfaces, the return type of any single-entity finder method is always the bean's local interface. With remote home interfaces, the return type of any single-entity find method is the remote interface. You cannot deploy an entity bean that doesn't include a findByPrimaryKey() method in its home interfaces.

Following the rules outlined earlier, we can define two ejbFind() methods in ShipBean that match the two find() methods defined in the ShipHome:

public Integer ejbFindByPrimaryKey(Integer primaryKey) throws FinderException, {
    Connection con = null;
    PreparedStatement ps = null;
    ResultSet result = null;
    try {
        con = this.getConnection();
        ps = con.prepareStatement("select id from Ship where id = ?");
        ps.setInt(1, primaryKey.intValue());
        result = ps.executeQuery();
        // Does the ship ID exist in the database?
        if (!result.next()) {
            throw new ObjectNotFoundException("Cannot find Ship with id = "+id);
        }
    } catch (SQLException se) {
        throw new EJBException(se);
    }
    finally {
        try {
            if (result != null) result.close();
            if (ps != null) ps.close(); 
            if (con!= null) con.close(); 
        } catch(SQLException se) {
            se.printStackTrace();
        }
    }
    return primaryKey;
}
public Collection ejbFindByCapacity(int capacity) throws FinderException {
    Connection con = null;
    PreparedStatement ps = null;
    ResultSet result = null;        
    try {
        con = this.getConnection();
        ps = con.prepareStatement("select id from Ship where capacity = ?");
        ps.setInt(1,capacity);
        result = ps.executeQuery();
        Vector keys = new Vector();
        while(result.next()) {
            keys.addElement(result.getObject("id"));
        }
        return keys;
    }
    catch (SQLException se) {
        throw new EJBException (se);
    }
    finally {
        try {
            if (result != null) result.close();
            if (ps != null) ps.close(); 
            if (con!= null) con.close(); 
        } catch(SQLException se) {
            se.printStackTrace();
        }
    }      
}

The mandatory findByPrimaryKey() method uses the primary key to locate the corresponding database record. Once it has verified that the record exists, it simply returns the primary key to the container, which then uses the key to activate a new instance and associate it with that primary key at the appropriate time. If no record is associated with the primary key, the method throws an ObjectNotFoundException.

The ejbFindByCapacity() method returns a collection of primary keys that match the criteria passed into the method. Again, we construct a prepared statement that we use to execute our SQL query. This time, however, we expect multiple results, so we use the java.sql.ResultSet to iterate through the results, creating a vector of primary keys for each SHIP_ID returned.

Find methods are not executed on bean instances that are currently supporting a client application. Only bean instances that are not currently assigned to an EJB object (i.e., instances in the instance pool) are supposed to service find requests, which means that the ejbFind() methods in the bean instance have somewhat limited use of the EntityContext. The EntityContext methods getPrimaryKey() and getEJBObject() will throw exceptions because the bean instance is in the pool and is not associated with a primary key or EJB object when the ejbFind() method is called.

Where do the objects returned by find methods originate? This seems like a simple enough question, but the answer is surprisingly complex. Remember that find methods aren't executed by bean instances that are actually supporting the client; rather, the container selects an idle bean instance from the instance pool to execute the method. The container is responsible for creating the EJB objects and local or remote references for the primary keys returned by the ejbFind() method in the bean class. As the client accesses these remote references, bean instances are swapped into the appropriate EJB objects, loaded with data, and made ready to service the client's requests.

10.11 The Deployment Descriptor

With a complete definition of the Ship EJB, including the remote interface, home interface, and primary key, we are ready to create a deployment descriptor. XML deployment descriptors for bean-managed entity beans are a little different from the descriptors we created for the container-managed entity beans in Chapter 6, Chapter 7, and Chapter 9. In this deployment descriptor, the <persistence-type> is Bean and there are no <container-managed> or <relationship-field> declarations. We also must declare the DataSource resource factory that we use to query and update the database.

Here is the deployment descriptor for EJB 2.0:

<!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise
JavaBeans 2.0//EN" "http://java.sun.com/dtd/ejb-jar_2_0.dtd">

<ejb-jar>
    <enterprise-beans>
        <entity>
            <description>
                This bean represents a cruise ship.
            </description>
            <ejb-name>ShipEJB</ejb-name>
            <home>com.titan.ship.ShipHomeRemote</home>
            <remote>com.titan.ship.ShipRemote</remote>
            <ejb-class>com.titan.ship.ShipBean</ejb-class>
            <persistence-type>Bean</persistence-type>
            <prim-key-class>java.lang.Integer</prim-key-class>
            <reentrant>False</reentrant>
            <security-identity><use-caller-identity/></security-identity>
            <resource-ref>
                <description>DataSource for the Titan database</description>
                <res-ref-name>jdbc/titanDB</res-ref-name>
                <res-type>javax.sql.DataSource</res-type>
                <res-auth>Container</res-auth>
            </resource-ref>
        </entity>
    </enterprise-beans>
 
    <assembly-descriptor>
        <security-role>
            <description>
                This role represents everyone who is allowed full access 
                to the Ship EJB.
            </description>
            <role-name>everyone</role-name>
        </security-role>

        <method-permission>
            <role-name>everyone</role-name>
            <method>
                <ejb-name>ShipEJB</ejb-name>
                <method-name>*</method-name>
            </method>
        </method-permission>

        <container-transaction>
            <method>
                <ejb-name>ShipEJB</ejb-name>
                <method-name>*</method-name>
            </method>
            <trans-attribute>Required</trans-attribute>
        </container-transaction>
    </assembly-descriptor>
</ejb-jar>

The EJB 1.1 deployment descriptor is exactly the same except for two things. First, the <!DOCTYPE> element references EJB 1.1 instead of 2.0:

<!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise
JavaBeans 1.1//EN" "http://java.sun.com/j2ee/dtds/ejb-jar_1_1.dtd">

Second, the <security-identity> element:

<security-identity><use-caller-identity/></security-identity>

is specific to EJB 2.0 and is not found in the EJB 1.1 deployment descriptor.

figs/book.gifPlease refer to Workbook Exercise 10.1, A BMP Entity Bean. This workbook is available free, in PDF format, at http://www.oreilly.com/catalog/entjbeans3/workbooks.

[1]  Although EJB is different from its GUI counterpart, JavaBeans, the concepts of accessors and properties are similar. You can learn about this idiom by reading Developing Java Beans™ by Rob Englander (O'Reilly).

[2]   Chapter 15 explains when you should not define any create methods in the home interface.

CONTENTS