Book Home Java Enterprise in a Nutshell Search this book

7.6. Implementing Entity Beans

An entity bean represents data that is stored in a database or some other persistent storage. Entity beans are persistent across client sessions and the lifetime of the server. Each entity bean of a given type has a unique identity that can look up the same bean from multiple clients. No matter when or where you get a reference to an entity bean with a given identity, the bean should reflect the current state of the persistent data it represents. Multiple clients can access an entity bean at the same time. The EJB container manages these concurrent transactions for the entity bean, ensuring that client transactions are properly isolated from each other. Note that support for entity beans is not strictly required by the EJB 1.0 specification. This has been changed in the EJB 1.1 specification, which makes entity bean support mandatory in EJB-compliant application servers.

An entity bean can be passivated by its container, but the meaning of being passivated is slightly different. A container passivates an entity bean (calling its ejbPassivate() method in the process) when it wants to disassociate the bean from the persistent data entity it has been representing. After being passivated, the bean may be put into the container's pool of entity beans to associate with another client-requested entity or it may be removed from the server altogether.

At a fundamental level, entity beans are implemented similarly to session beans. You need to provide a home interface, a remote interface, and a bean implementation. An entity bean, however, requires some additional methods in its home interface and bean implementation, to support the management of its persistent state and to allow clients to look up the entity bean from persistent storage. Entity beans must also provide a class that serves as its primary key, or index, into its persistent storage.

There are two ways persistent storage for an entity bean can be managed: by the EJB container or by the bean itself. In the first case, called a container-managed entity bean, the bean leaves the database calls to the container. The deployment tools provided with the EJB server are responsible for generating these database calls in the classes it uses to deploy your bean. In the second case, called bean-managed entity beans, you provide the database calls for managing your bean's persistent storage as part of your bean implementation.

If you can rely on the EJB container to handle your entity bean's persistence, this can be a huge benefit, since it saves you from having to add JDBC code to your beans. But the automated persistence support in EJB is limited, and there are times when you'll need to manage persistence directly in your bean implementation. We discuss the pros and cons of each of these scenarios a bit later in this section.

7.6.1. Primary Keys

If you develop an entity bean, you must provide the EJB container with a class that serves as the primary key for the bean. A primary key includes all of the information needed to uniquely identify an item in persistent storage. The primary key for a person's records in a database, for example, might be a first and last name, or a social security number (for U.S. citizens), or some other identification number. If you're developing an EJB object that represents a bank account, you might make the primary key an object that holds the account number, which is a unique identifier for an Account object. An EJB container typically creates unique identifiers for all session and entity beans, so that it can internally track individual beans. The primary key used for entity beans is a more public unique identifier, in that clients can see the primary key for an entity bean, and the primary key is used directly by the bean implementation to load/update its state from persistent storage.

If we were to develop an entity-bean version of our ProfileBean (which we'll do shortly), the primary key class might look something like the following:

public class ProfilePK implements java.io.Serializable {
  public String mName;
  public ProfilePK() {
    mName = null;
  }
  public ProfilePK(String name) {
    mName = name;
  }
}

Since there is a one-to-one correspondence between named users and their profiles, we just use the name of the user as our primary key for an entity ProfileBean.

The primary key class for an entity bean must be derived from java.io.Serializable. If any of the persistence of the entity bean is container-managed, the primary key class must also obey the following:

The primary key for our ProfileBean is really just a wrapper around a String field that holds a name. We've done this to support the option of using container-managed persistence for the bean. We have to use the ProfilePK class as the primary key, not just a String, because the EJB container needs to be able to introspect on the primary key and match its fields with the corresponding fields on the bean class.

7.6.2. Finder Methods

Since entity beans are persistent and can be accessed by multiple clients, clients have to be able to find them as well as create them. To this end, an entity bean's home interface can provide findXXX() methods, and the bean implementation has to have corresponding ejbFindXXX() methods that take the same arguments and have the same return types. The findXXX() methods on the home interface can have any name, as long as the method name begins with find. A bank account bean, for example, might define a findByName() method that accepts a string that is the name of the person whose accounts are desired.

Each findXXX() method on the home interface must return either an instance of the bean's remote interface or a collection of these objects. In the EJB 1.0 specification, only Enumeration objects can return collections of entity beans, but the EJB 1.1 specification allows EJB implementations to also use Java 2 Collection types as return types for findXXX() methods. In our bank account example, the findByName() method can return multiple accounts (e.g., if a person has both checking and savings accounts), so it should be declared as returning an Enumeration.

The home interface for an entity-based ProfileBean is shown in Example 7-8. It provides two finder methods: findByPrimaryKey() finds a profile by its primary key (which encapsulates the user's name), and findByEntryValue() finds profiles that have a particular attribute value set. The first finder method returns a single Profile object, since there is only a single Profile for each user. The second finder method returns a collection of Profile objects (as an Enumeration), as multiple user profiles might have a given attribute value. The findByPrimaryKey() method is a standard finder method defined by the EJB specification; its only argument is always the primary key type for the entity bean.

Example 7-8. Home Interface for an Entity ProfileBean

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

public interface ProfileHome extends EJBHome {
  public Profile create() throws RemoteException;
  public Profile create(String name)
    throws RemoteException, DuplicateProfileException;

  public Profile findByPrimaryKey(ProfilePK key)
    throws RemoteException, FinderException;
  public Enumeration findByEntryValue(String key, String value)
    throws RemoteException, FinderException;
}

A client can use the findXXX() methods on the home interface to determine if a bean (or beans) with a given identity already exists in persistent storage. If a findXXX() method finds an appropriate bean (or beans), a single primary key (or set of keys) is initialized to represent the unique identity of the bean(s) that matched the client query, and these key(s) are returned to the client. If the identified bean cannot be found in persistent storage, a javax.ejb.FinderException is thrown. All findXXX() methods on the bean's home interface must declare that they can throw FinderException and RemoteException (since the method is an RMI remote method).

The EJB container intercepts the client's invocation of the finder method and invokes the corresponding ejbFindXXX() method on an instance of the entity bean on the server. An entity bean of the appropriate type is pulled from the container's pool and its ejbFindXXX() method is called with the client's arguments. The ejbFindXXX() method on the bean should do the necessary queries to persistent storage to determine if the requested data exists there, then create primary key instances and initialize them with the results of the query. The primary key objects are the return value of the ejbFindXXX() method. The EJB container is responsible for taking the key(s) returned by the ejbFindXXX() method and converting them to remote objects, whose stubs are returned to the client that invoked the finder method.

It's important to note that the entity bean that executes the ejbFindXXX() method doesn't necessarily represent the entities being looked up by the client. The container uses the bean to call the method, takes the primary key or keys returned, and then uses them to either create new beans or reinitialize existing beans.

An entity bean implementation must at a minimum provide an ejbFindByPrimaryKey() method that accepts a primary key object as its argument. The implementation must also provide additional findXXX() methods to match any other ejbfindXXX() methods on the home interface. Each ejbFind method must have the same arguments and return types as the corresponding find method.

7.6.3. Entity Bean Implementation

I've already mentioned a few additional requirements on entity bean implementations, but here is a list of all the additional methods an entity bean either must implement or has the option to implement:

public priKeyType ejbFindByPrimaryKey(priKeyType) throws FinderException

The only required finder method on an entity bean. Both the argument and the return type must be the bean's primary key type.

public void ejbPostCreate()

If needed, an entity bean can optionally provide an ejbPostCreate() method for each ejbCreate() method it provides, taking the same arguments. The container calls the ejbPostCreate() method after the bean's ejbCreate() method has been called and after the container has initialized the transaction context for the bean.

public void ejbLoad()

Called by the container to cause the bean instance to load its state from persistent storage. The container can call this bean method any time after the bean has been created, to do an initial load from persistent storage or to refresh the bean's state from the database.

public void ejbStore()

Called by the container to cause the bean to write its current runtime state to persistent storage. This method can be called any time after a bean is created.

public void setEntityContext(EntityContext ctx)

The container calls this method after a new instance of the bean has been constructed, but before any of its ejbCreate() methods are called. The bean is responsible for storing the context object.

public void unsetEntityContext(EntityContext ctx)

The container calls this method before the entity bean is destroyed.

Most of these methods, like ejbLoad() and ejbStore(), are invoked by the EJB container to notify the bean about persistent store management.

In addition to these entity-specific methods on bean implementations, the semantics of some of the other standard methods are slightly different for entity beans. Each ejbCreate() method, for example, should not only assign any state data passed in as variables, but also create a record in persistent storage for the new entity bean. The signatures of ejbCreate() methods on entity beans can be different too. For an entity bean that manages its own persistence (a bean-managed entity bean), the ejbCreate() methods return the primary key type for the bean. For a container-managed entity bean, the ejbCreate() methods return void, the same as for session beans. The ejbRemove() method is called by the container when the bean is to be removed from the server. The bean should also remove its state from the persistent storage in its ejbRemove() implementation, since a request by a client to remove() an entity bean is really a request to remove the record from persistent storage as well.

7.6.3.1. A persistent ProfileBean

The major drawback in our stateful session ProfileBean is that the profile data it represents isn't persistent. A profile is created by a client and updated through remote method calls, but once the ProfileBean is removed by the server or the server crashes/restarts, the accumulated profile data is lost. What we really want is a bean whose state is persistent stored in a relational database or some other persistent storage, that can be reloaded at a later time, when the user reenters a profiled application. An entity EJB object provides this functionality, and an EJB container that supports entity beans provides your bean with facilities that make it easier to manage persistent state. It's also possible to have the container manage the persistence of the bean for you, if that's desired.

Let's look at the implementation for the entity bean version of our ProfileBean, shown in Example 7-9. We've already seen the home interface and remote interface for this entity bean in earlier examples. The purpose of the bean is the same as our stateful session version: it represents a profile for a named application user, maintaining a list of name/value pairs for various attributes and options. The difference is that this ProfileBean represents a profile entity that exists as data in persistent storage (a database, in this case). The most obvious differences in the actual code are the JDBC calls peppered throughout the class, where the bean manages its persistent data. There are a few extra methods defined as well. Most of them are required by the EJB specification for entity beans and a few are utility methods used by the JDBC code to connect to the database and make updates.

Example 7-9. An Entity ProfileBean with Bean-Managed Persistence

import javax.ejb.*;
import java.rmi.RemoteException;
import java.util.Properties;
import java.util.Enumeration;
import java.util.Vector;
import java.sql.*;

public class ProfileBean implements EntityBean {
  // Entries in the profile (name/value pairs)
  public Properties mEntries;

  // Store context (nonpersistent)
  private transient EntityContext mContext = null;

  // Entity bean methods

  // During activation, create our entry lookup table
  public void ejbActivate() {
    mEntries = new Properties();
    System.out.println("ProfileBean activated.");
  }

   // Load bean from persistent store. In this case, we're managing the dbase
   // storage, so we store our profile entries as independent records in a
   // separate "PROFILE_ENTRY" table.
  public void ejbLoad() throws RemoteException {
    try {
      // Get primary key from context, use it to load our data
      ProfilePK key = (ProfilePK)mContext.getPrimaryKey();
      loadFromDB(key);
    }
    catch (Exception e) {
      System.out.println("Failed to load ProfileBean: ");
      e.printStackTrace();
      throw new RemoteException("ejbLoad failed: ", e);
    }
    System.out.println("ProfileBean load finished.");
  }

  protected void loadFromDB(ProfilePK key) throws FinderException {
    boolean found = false;
    try {
      // Get a connection and select our profile record
      Connection conn = newConnection();
      Statement s = conn.createStatement();
      s.executeQuery("select name from profile where name = `" + key.mName +
                     "`");
      ResultSet rs = s.getResultSet();
      if (rs.next()) {
        found = true;
        // We found a profile record, so look up the entries
        s.executeQuery("select key, value from profile_entry where name = `"
                       + key.mName + "`");
        rs = s.getResultSet();
        while (rs.next()) {
          String pKey = rs.getString(1);
          String pValue = rs.getString(2);
          mEntries.put(pKey, pValue);
        }
      }
    }
    catch (SQLException e) {
      throw new FinderException("Failed to load profile entries from DB: " +
                                e.toString());
    }
    if (!found) {
      // No profile record found, throw a FinderException
      throw new FinderException("No profile found for " + key.mName);
    }
  }

  // Get connection (BEA/WebLogic-specific version)
  private Connection newConnection() throws SQLException {
    // Make sure that the JDBC driver is loaded
    try {
      Class.forName("weblogic.jdbc.oci.Driver");
    }
    catch (ClassNotFoundException cnfe) {
      System.out.println("Failed to load JDBC drivers.");
    }
    // Get the connection from the pool that we specified in the 
    // WebLogic server properties file
    return DriverManager.getConnection("jdbc:weblogic:jts:myPool");
  }

   // Store bean to persistent store. Properties are stored as records in the
   // PROFILE_ENTRY table.
  public void ejbStore() throws RemoteException {
    // Get our primary key from our context
    ProfilePK key = (ProfilePK)mContext.getPrimaryKey();
    try {
      Connection conn = newConnection();
      // Clear out old profile entries
      Statement s = conn.createStatement();
      s.executeUpdate("delete from PROFILE_ENTRY where name = `" + key.mName
                      + "`");
      Enumeration pKeys = mEntries.propertyNames();
      // Add each entry to the PROFILE_ENTRY table
      while (pKeys.hasMoreElements()) {
        String pKey = (String)pKeys.nextElement();
        String pValue = mEntries.getProperty(pKey);
        s.executeUpdate("insert into PROFILE_ENTRY (name,key,value) values "
                        + "(`" + key.mName + "`, `" + pKey + "`, `"
                        + pValue + "`)");
      }
      // Close the statement and the connection, just to be tidy...
      s.close();
      conn.close();
    }
    catch (Exception e) {
      // Store operation failed, toss a RemoteException
      throw new RemoteException("ejbStore failed: ", e);
    }
    System.out.println("ProfileBean store finished.");
  }

  // Remove this named profile from the database
  public void ejbRemove() {
    // Get this profile's name
    ProfilePK key = (ProfilePK)mContext.getPrimaryKey();
    try {
      Connection conn = newConnection();
      // Clear out any profile entries
      Statement s = conn.createStatement();
      s.executeUpdate("delete from profile_entry where name = `" + key.mName
                      + "`");
      // Clear out the profile itself
      s.executeUpdate("delete from profile where name = `" + key.mName 
                      + "`");

      s.close();
      conn.close();
      System.out.println("ProfileBean removed.");
    }
    catch (SQLException se) {
      System.out.println("Error removing profile for " + key.mName);
      se.printStackTrace();
    }
  }

  // When we're passivated, release our entries.
  public void ejbPassivate() {
    mEntries = null;
    System.out.println("ProfileBean passivated.");
  }

  // Get context from container
  public void setEntityContext(EntityContext context) {
    mContext = context;
    System.out.println("ProfileBean context set.");
  }

  // Container is removing our context...
  public void unsetEntityContext() throws RemoteException {
    mContext = null;
    System.out.println("ProfileBean context unset.");
  }

  // Since we're managing persistence here in the bean, we need to
  // implement the finder methods
  public ProfilePK ejbFindByPrimaryKey(ProfilePK key)
    throws FinderException, RemoteException {
    loadFromDB(key);
    return key;
  }

  public Enumeration ejbFindByEntryValue(String key, String value)
    throws RemoteException, FinderException {
    Vector userList = new Vector();
    // Get a new connection from the EJB server
    try {
      Connection conn = newConnection();
      Statement s = conn.createStatement();
      // Issue a query for matching profile entries, grabbing just the name
      s.executeQuery("select distinct(name) from profile_entry where " +
                     " key = `" + key + "` and value = `" + value + "`");
      // Convert the results in primary keys and return an enumeration
      ResultSet results = s.getResultSet();
      while (results.next()) {
        String name = results.getString(1);
        userList.addElement(new ProfilePK(name));
      }
    }
    catch (SQLException se) {
      // Failed to do database lookup
      throw new FinderException();
    }
    return userList.elements();
  }

  // Create method (corresponds to each create() method on the
  // home interface, ProfileHome).  Nothing to initialize in this case
  public ProfilePK ejbCreate() {
    System.out.println("Nameless ProfileBean created.");
    return new ProfilePK();
  }

  // Create method with name of profile owner.
  public ProfilePK ejbCreate(String name) throws DuplicateProfileException {
    try {
      Connection conn = newConnection();
      Statement s = conn.createStatement();
      s.executeUpdate("insert into profile (name) values (`" + name + "`)");
      s.close();
      conn.close();
    }
    catch (SQLException se) {
      System.out.println("Error creating profile, assuming duplicate.");
      throw new DuplicateProfileException("SQL error creating profile for " +
                                          name + ": " + se.toString());
    }
      
    System.out.println("ProfileBean created for " + name + ".");
    return new ProfilePK(name);
  }

  // Post-creation notification.  Nothing to do here, but we need
  // to provide an implementation.
  public void ejbPostCreate() {
    System.out.println("ProfileBean post-create called.");
  }

  // Post-creation notification.  Nothing to do here, what we need
  // to provide an implementation.
  public void ejbPostCreate(String name) {
    System.out.println("ProfileBean post-create called for " + name + ".");
  }
   
  // Business methods
  public String getName() {
    ProfilePK key = (ProfilePK)mContext.getPrimaryKey();
    return key.mName;
  }

  public String getEntry(String key) {
    return mEntries.getProperty(key);
  }

  public void setEntry(String key, String value) {
    mEntries.put(key, value);
  }
}

The structure of the entity ProfileBean is similar to the stateful session bean version in Example 7-6. A Properties object holds the profile entries for the user, and the getEntry() and setEntry() remote method implementations access this Properties object for the client. You might notice that there is no data member on the entity ProfileBean to hold the name of the user. We can do this here because we're not using the EJB container to manage the bean's persistence for us, so we're relying on the fact that the name is found in the primary key object, and the primary key is stored for us in the EntityContext the container gives us through the setEntityContext() method. If we were using container-managed persistence, however, we'd have to have a field on the bean for the name, so that the container would know how to set it. The getName() remote method on ProfileBean shows how we retrieve the username for the profile using the getPrimaryKey() method on the EntityContext.

We've also removed the setName() remote method from the entity ProfileBean, since we don't want to allow the client to change the name of an existing, active entity bean. The Profile remote interface for this bean, not shown here, is similar to the Profile interface in Example 7-5, but does not have a setName() method. Since the Profile is now a persistent entity bean and the name is the primary key, or identifying attribute, of the bean, the name of the bean can only be set when the bean is created. While the entity bean is active, it is associated with a profile entity for a specific user, and the client can only read the name associated with the profile.

In the ProfileBean code, you'll notice many of the EJB-required methods, including ejbActivate(), ejbPassivate(), ejbCreate(), and ejbRemove(). The ejbActivate() and ejbPassivate() methods handle the movement of the bean out of and into the EJB server's entity bean pool, respectively.

The ejbCreate() methods on the ProfileBean create a new profile entity in the database. There is a matching ejbCreate() method for each create() method on our ProfileHome interface from Example 7-8. The EJB container is responsible for intercepting the generated primary key object, converting it to a remote Profile object, and returning a remote Profile stub to the client that called the create() method on the ProfileHome interface. The ejbRemove() method on our ProfileBean deletes all the records for this profile entity from the database.

The ProfileBean also contains methods specific to entity beans. For each ejbCreate() method, it has a corresponding ejbPostCreate() method, which is called by the container after the ejbCreate() method has returned, and the container has initialized the bean's transaction context. There's nothing more for us to do in our ProfileBean at this point, so we just print a message to standard output in each ejbPostCreate() method.

There is an ejbFindXXX() method in our entity ProfileBean that corresponds to each findXXX() method in ProfileHome. The ejbFindByPrimaryKey() method simply takes the primary key passed in as an argument and attempts to load the data for the entity from the database. If successful, it returns the primary key back to the container, where it is converted to a remote Profile object to be returned to the client. Note that it's not necessary for us to actually load all the profile data here in the finder method; we need to verify only that the named entity exists in the database and either return the primary key to signal success or throw an exception. The container takes the returned primary key and assigns it to one of the beans in its pool (possibly the same one it called the finder method on, but not necessarily). Since we already have the loadFromDB() method used in ejbLoad(), it is a simple matter to reuse it here in the finder method. If the performance hit for loading the profile data twice is too great, we'd have to rewrite the finder method to simply check the PROFILE table for a record matching the name in the primary key.

The ejbFindByEntryValue() method takes a key and value String arguments and attempts to find any and all profile entities with a matching key/value pair in the PROFILE_ENTRY table. Each name that has such a record is converted to a primary key object and returned to the container in an Enumeration. The container converts each primary key object into a remote Profile object and returns the set to the client. If we encounter a database problem along the way, we throw a FinderException.

7.6.4. The Entity Context

The EJB container provides context information to an entity bean in the form of an EntityContext object. The container sets this object using the bean's setEntityContext() method and removes it when the bean is being removed by calling the bean's unsetEntityContext() method. Like SessionContext, EntityContext provides the bean with access to its corresponding remotely exported object through the getEJBObject() method. The EntityContext, in addition, gives an entity bean access to its primary key through getPrimaryKey(). The declared return type of this method is Object, but the object returned is of the bean's primary key type. Note that the data accessed through the EntityContext might be changed by the EJB container during the bean's lifetime, as explained in the next section. For this reason, you shouldn't store the EJB remote object reference or primary key in data variables in the bean object, since they might not be valid for the entire lifetime of the bean. Our entity ProfileBean, for example, stores the EntityContext reference in an instance variable, where it can access the context data as needed during its lifetime.

7.6.5. Life Cycle of an Entity Bean

Before the first client asks for an entity bean by calling a create() or findXXX() method on its home interface, an EJB container might decide to create a pool of entity beans to handle client requests for beans. This potentially reduces the amount of time it takes for a client to receive a entity bean remote reference after it makes a request for an entity bean. To add a bean to its pool, the container creates an instance of your bean implementation class and sets its context using the setEntityContext() method. At this point, the entity bean hasn't been associated with a particular data entity, so it doesn't have a corresponding remote object.

When a client calls a create() method on the bean's home interface, the container picks a bean out of the pool and calls its corresponding ejbCreate() method. If the efbCreate() method is successful, it returns one or more primary key objects to the container. For each primary key, the container picks an entity bean out of its pool to be assigned to the entity represented by the key. Next, the container assigns the bean's identity by setting the properties in its EntityContext object (e.g., its primary key and remote object values). If the bean has an ejbPostCreate() method, that gets called after the bean's entity identity has been set. The ejbCreate() method should create the entity in persistent storage, if the bean is managing its own persistence.

Alternately, the client might call a findXXX() method on the home interface. The container picks one of the pooled entity beans and calls the corresponding ejbFindXXX() method on it. If the finder method finds one or more matching entities in persistent storage, the container uses pooled entity beans to represent these entities. It picks entity beans out of the pool and calls their ejbActivate() methods. Before calling ejbActivate(), the container sets the bean's context by assigning the corresponding primary key and remote object reference in its context.

After an entity bean has been activated (either by being created through one of its ejbCreate() methods or by being found and having its ejbActivate() method called), it is associated with a specific entity in persistent storage, and with a specific remote object that has been exported to a remote client or clients. At any point after this, the container can call the bean's ejbLoad() or ejbStore() method to force the bean to read or write its state from/to persistent storage. The bean's business methods can also be invoked by clients when it is in this state.

At some point, the container may decide to put the bean back into its internal pool. This might happen after all remote references to the bean have been released or after a certain period of inactivity with the bean. The container might also do this as a reaction to client loading issues (e.g., time-sharing pooled beans between client requests). When the container wants to remove the association between the bean and the remote object, but doesn't want the object's state removed from persistent store, it calls the bean's ejbPassivate() method. The bean can release any resources it allocated while in the active state, but it doesn't have to update persistent storage for the entity it represents, as this was done the last time its ejbStore() method was invoked by the container.

The bean can also lose its association with an entity when the client decides to remove the entity. The client does this either by calling a remove() method on the bean's home interface or calling the remove() method directly on an EJB object. When one of these things happens, the container calls the bean's ejbRemove() method, and the bean should delete the data in persistent storage pertaining to the entity it represents. After the ejbRemove() method completes, the container puts the bean back into its internal pool.

7.6.6. Handles on Entity Beans

Every bean's remote interface extends the EJBObject interface. This interface allows the client to obtain a serializable handle on the enterprise bean. This handle is a persistent reference to the bean that can be serialized and then stored in local storage on the bean or emailed as an attachment to other users, for example. Later, a client can deserialize the handle object and continue interacting with the bean it references. The handle contains all of the information needed to reestablish a remote reference to the enterprise bean it represents. Since this is only useful for beans that are still valid when the handle is reconstituted, it is usually only applicable to entity beans.

The handle for a bean can be obtained using the getHandle() method on a remote bean object reference:

Profile profile = ...;
Handle pHandle = profile.getHandle();

getHandle() returns a javax.ejb.Handle object. The Handle interface itself does not extend java.io.Serializable, but any class that implements it is required by the EJB specification to extend Serializable. Typically, the Handle implementation is provided by the EJB container, which enforces this restriction. So you can always assume that the Handle for an EJB object can be stored in serialized format, if needed:

ObjectOutputStream oout = ...;
oout.writeObject(pHandle);

Later, you can read the object back from its serialized state and obtain a reference to the remote bean object, using the getEJBObject() method on the handle:

ObjectInputStream oin = ...;
Handle pHandleIn = (Handle)oin.readObject();
Profile profileIn = (Profile)pHandleIn.getEJBObject();
profileIn.getEntry("favoriteColor");

7.6.7. Container-Managed Persistence

In our entity-based ProfileBean, shown in Example 7-9, the persistent state of the profile entity is managed by the bean itself. There's JDBC code in the ProfileBean implementation that loads, stores, and removes the entity's database entries. This is called bean-managed persistence : the EJB container calls the appropriate methods on your entity bean, but your bean implementation is responsible for connecting to the database and making all of the necessary queries and updates to reflect the life cycle of the data entity.

As I mentioned earlier, the EJB specification provides another option: container-managed persistence. In this case, you define data members on your entity bean implementation that hold the state of the entity and tell the EJB container how to map these data members to persistent storage. If the persistent storage is a database, you tell the container which columns in which tables hold the various data members of your entity. With container-managed persistence, the container is responsible for loading, updating, and removing the entity data from persistent storage, based on the mapping you provide. The container also implements all the finder methods required by the bean's home interface.

If you want to take advantage of container-managed persistence, you have to indicate this to the EJB container when you deploy the bean. You also provide the data mapping at deployment time. To see this in action, let's use a simple entity bean that represents a person using just a first and last name:

import javax.ejb.*;
import java.rmi.RemoteException;
import jen.ejb.NoSuchPersonException;
import java.util.Properties;

public class PersonBean implements EntityBean {
  // First name of person
  public String mFirstName = "";
  // Last name
  public String mLastName = "";

  // Store context (nonpersistent)
  private transient EntityContext mContext = null;

  // No need for us to activate anything in this bean, but we need to
  // provide an implementation.
  public void ejbActivate() {
    System.out.println("ProfileBean activated.");
  }

  // Load bean from persistent store. Container is doing this for us, so
  // nothing to do here.
  public void ejbLoad() throws RemoteException {}

  // Store bean to persistent store.  Container is doing this, so nothing
  // to do here, either.
  public void ejbStore() throws RemoteException {}
  
  // Nothing to do on a remove.
  public void ejbRemove() {}

  // No state to store on passivation (it's all in persistenct storage).
  public void ejbPassivate() {}

  // Get context from container.
  public void setEntityContext(EntityContext context) {
    mContext = context;
  }

  // Container is removing our context.
  public void unsetEntityContext() throws RemoteException {
    mContext = null;
  }

  // Create method (corresponds to each create() method on the
  // home interface).  Nothing to initialize in this case.
  public void ejbCreate() {
    System.out.println("Nameless PersonBean created.");
  }

  // Postcreation notification.  Nothing to do here, but we need
  // to provide an implementation.
  public void ejbPostCreate() {
    System.out.println("PersonBean post-create called.");
  }
   
  // Create method with name of person.
  public void ejbCreate(String fname, String lname) 
    throws NoSuchPersonException {
    mFirstName = fname;
    mLastName = lname;
  }

  // Postcreation notification.  Nothing to do here, but we need
  // to provide an implementation.
  public void ejbPostCreate(String fname, String lname) {}
   
  // Business methods 
  public String getFirstName() {
    return mFirstName;
  }

  public String getLastName() {
    return mLastName;
  }
}

We're going to focus on the bean implementation here; I leave it to you to sort out the home and remote interfaces for this bean. The PersonBean has two data members, representing the first and last name of the person. In comparison to our entity ProfileBean from earlier, this bean is much simpler, since the ejbRemove(), ejbLoad(), and ejbStore() methods are empty. We're going to let the container handle the loading and storing of the bean's data and the removal of any entities from the database, so we don't need to do anything about these operations in our bean implementation.

In order for an EJB container to manage the persistence of this bean, we need to tell it what data members should be stored persistently and where to store them in the database. We'll see some examples of container-managed data mappings in the section on deploying enterprise beans, but to give you a sense of how this works, here's the relevant section from a deployment descriptor file for BEA's WebLogic EJB server:

...
(persistentStoreProperties
  persistentStoreType    jdbc 
  (jdbc
    tableName            PERSON
		. . .
    (attributeMap
      mFirstName         FIRST_NAME
      mLastName          LAST_NAME
    )
  )
)
...
containerManagedFields    [mFirstName mLastName]
primaryKeyClassName       PersonPK
...

In this part of the bean's deployment descriptor, we're telling the container that the mFirstName and mLastName members of the PersonBean are stored persistently, and that they should be stored in the FIRST_NAME and LAST_NAME columns of the PERSON table. We also have to tell the container which JDBC connection pool to use to connect to the database, but I've omitted those details for now.

We also need to tell the container the primary key class for our entity bean. In this case, it's the PersonPK class, which looks like this:

public class PersonPK implements java.io.Serializable {
  public String mFirstName;
  public String mLastName;

  public PersonPK() {
    mFirstName = null;
    mLastName = null;
  }
  public PersonPK(String fname, String lname) {
    mFirstName = fname;
    mLastName = lname;
  }
}

Since we're using container-managed persistence, the primary key class for our bean has to include members that match the corresponding members on the bean class. This allows the bean to map the key fields to bean fields automatically and to generate the default finder methods for the bean.

If you choose container-managed persistence for your bean, the EJB container generates all the ejbFindXXX() methods required for the finder methods on the home interface. It automatically generates an ejbFindByPrimaryKey() method, based on the data-mapping information you provide at deployment time. For any other ejbFindXXX() methods, you need to provide the container with a recipe for implementing the methods. The EJB 1.0 specification doesn't provide a standard format for specifying these additional finder methods for your bean, nor does it provide a means for you to specify some of the finder methods yourself in the bean implementation and leave the rest for the EJB container to implement. Some EJB providers allow you to provide code segments at deployment time for the finder methods, while other providers define a descriptive scripting language that allows you to describe the logic of the method implementation to the EJB container. In either case, the container takes this information and creates implementations for the finder methods. These finder methods are located within one of the support classes it generates, usually in a generated subclass of your home interface.

As an example, suppose we want to have a finder method for our PersonBean class called findFreds() that finds all the people whose first name starts with "Fred." BEA's WebLogic server specifies a syntax for describing the logic of a finder method in the bean's deployment descriptor file. The segment of the descriptor that describes this finder method for a WebLogic server might look like this:

(finderDescriptors
  "findFreds()" "(like mFirstName Fred%)"
)

If you are implementing an entity bean with many complicated finder methods, or if you are concerned with your bean being easily portable between EJB server providers, you may want to shy away from container-managed persistence and stick with managing the persistent data yourself. With some EJB providers, you may find that the format they provide for describing finder methods is too limited for your purposes. And deploying the bean in different EJB servers means porting the descriptions of your finder methods from server to server, which defeats the purpose of writing to a distributed component standard.

If your EJB object is using container-managed persistence, the container is handling the loading and storing of persistent data. You still can provide ejbLoad() and ejbStore() methods on your bean implementation, however. The ejbLoad() method is called just after the container has loaded the specified data fields from persistent storage into your data members, and ejbStore() is called just before the container writes your data members to persistent storage. If there is any conversion or bookkeeping you need to handle, you can do that in these methods.

Container-managed beans also rely on the container to create and remove the entities they represent from persistent storage. The bean can still provide ejbCreate() and ejbRemove() methods, however. The appropriate creation method is called just before the container creates the required records for the new entity in the database. The bean can use these methods to initialize any data members the container accesses while creating the records. The container also invokes the bean's ejbRemove() method just before the container removes the necessary records from persistent storage. This lets you do any cleanup before the entity is removed.

7.6.7.1. Handling complex data structures

Each EJB container is limited to some degree in the way that data on your bean implementation can be mapped into persistent data fields. There is no standard format defined for the data mapping the container supports, so it's possible a particular EJB provider won't support whatever complicated mapping you require for your bean. For the most part, however, you can expect EJB providers to limit the format to a single persistent data field being mapped to a single data member on your bean implementation. If the data structures on your bean are too complicated for you to provide an explicit mapping to persistent data fields, you have to decide how to deal with this.

In our entity ProfileBean example, we've stored the profile entries in a Properties object. We don't know at deployment time how many entries there will be, so we can't enumerate a mapping to database fields. We really want each entry in the Properties object to be stored in the PROFILE_ENTRY table in our database, along with the name of the owner of the entry, which is exactly how we implemented our bean-managed implementation in Example 7-9.

One option is to give up on container-managed persistence and manage it yourself in the bean implementation. Another is to make each entry in the profile its own bean and store the entries in a list on the profile bean. This would probably turn out to be too expensive in terms of interactions with the container and memory, however. Each entry in the profile would need to be managed separately by the container, with all of the relevant lifecycle notifications.

Another option is to serialize your data structures into individual data members on your bean and allow the container to read/write the serialized bytes to database fields as binary data. In our entity ProfileBean example, rather than using the PROFILE and PROFILE_ENTRY tables we used in the bean-managed version, we can define a single table to hold the name of the profile owner, along with the serialized bytecodes for the Properties object that represents the profile entries. We can then use the ejbStore() method on our bean to convert the Properties object to an array of bytes:

public void ejbStore() throws RemoteException {
  try {
    ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
    ObjectOutputStream objOut = new ObjectOutputStream(byteOut);
    objOut.writeObject(mEntries);
    mEntriesBytes = byteOut.toByteArray();
  }
  catch (Exception e) {
    throw new RemoteException("ejbStore failed: ", e);
  }
}

After the container calls our ejbStore() method, it can write the mEntriesBytes data member on our bean to a raw data field in the database (e.g., a LONG BINARY field in a SQL database). On the reading end, we can use the ejbLoad() method to convert the bytes loaded by the container to the mEntriesBytes data member into a Properties object:

public void ejbLoad() throws RemoteException {
  try {
    ByteArrayInputStream byteIn = new ByteArrayInputStream(mEntriesBytes);
    ObjectInputStream objIn = new ObjectInputStream(byteIn);
    mEntries = (Properties)objIn.readObject();
  }
  catch (Exception e) {
    throw new RemoteException("ejbLoad failed: ", e);
  }
}

This workaround allows us to deploy our entity ProfileBean with container-managed persistence, but it makes our database records unusable for other, non-Java applications. There's no way, for example, to check on a user's profile entries using a simple SQL query.



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.

This HTML Help has been published using the chm2web software.