Home > Articles > Programming > Java

  • Print
  • + Share This
This chapter is from the book

This chapter is from the book

Implementing a BMP Entity Bean

Implementing an Entity bean involves providing an implementation for the methods of the javax.ejb.EntityBean, corresponding methods for each method in the local-home interface, and a method for each method in the local interface.

Implementing javax.ejb.EntityBean

The setEntityContext() method is a good place to perform JNDI lookups, for example to acquire a JDBC DataSource reference. Listing 6.4 shows how this is done for the JobBean code.

Listing 6.4—JobBean.setEntityContext() Method

  1: package data;
  2: 
  3: import javax.ejb.*;
  4: import javax.naming.*;
  5: import javax.sql.*;
  6: // imports omitted
  7: 
  8: public class JobBean implements EntityBean
  9: {
 10:   public void setEntityContext(EntityContext ctx) {
 11:     this.ctx = ctx;
 12:     InitialContext ic = null;
 13:     try {
 14:       ic = new InitialContext();
 15:       dataSource = (DataSource)ic.lookup("java:comp/env/jdbc/Agency");
 16:      skillHome = (SkillLocalHome)
         ic.lookup("java:comp/env/ejb/SkillLocal");
 17:      locationHome = (LocationLocalHome)
         ic.lookup("java:comp/env/ejb/LocationLocal");
 18:      customerHome = (CustomerLocalHome)
         ic.lookup("java:comp/env/ejb/CustomerLocal");
 19:    }
 20:    catch (NamingException ex) {
 21:      error("Error looking up depended EJB or resource",ex);
 22:      return;
 23:    }
 24:  }	
 25:
 26:   private Context ctx;
 27:   private DataSource dataSource
 28: 
 29:   // code omitted
 30: }

The unsetEntityContext() method (not shown) usually just sets these fields to null.

The ejbLoad() and ejbStore() methods are responsible for synchronizing the bean's state with the persistent data store. Listing 6.5 shows these methods for JobBean.

Listing 6.5—JobBean's ejbLoad() and ejbStore() Methods

  1: package data;
  2: 
  3: import javax.ejb.*;
  4: import java.sql.*;
  5: // imports omitted
  6: 
  7: public class JobBean implements EntityBean
  8: {
  9:   public void ejbLoad(){
 10:     JobPK key = (JobPK)ctx.getPrimaryKey();
 11:     Connection con = null;
 12:     PreparedStatement stmt = null;
 13:     ResultSet rs = null;
 14:     try {
 15:       con = dataSource.getConnection();
 16:       stmt = con.prepareStatement(
         "SELECT description,location 
          FROM Job
          WHERE ref = ? AND customer = ?");
 17:       stmt.setString(1, key.getRef());
 18:       stmt.setString(2, key.getCustomer());
 19:       rs = stmt.executeQuery();
 20:       if (!rs.next()) {
 21:         error("No data found in ejbLoad for " + key, null);
 22:       }
 23:       this.ref = key.getRef();
 24:       this.customer = key.getCustomer();
 25:       this.customerObj = 
         customerHome.findByPrimaryKey(this.customer); // derived
 26:       this.description = rs.getString(1);
 27:       String locationName = rs.getString(2);
 28:       this.location = (locationName != null) ? 
         locationHome.findByPrimaryKey(locationName) : null;
 29:       // load skills
 30:       stmt = con.prepareStatement(
         "SELECT job, customer, skill 
          FROM JobSkill 
          WHERE job = ? AND customer = ? 
          ORDER BY skill");
 31:       stmt.setString(1, ref);
 32:       stmt.setString(2, customerObj.getLogin());
 33:       rs = stmt.executeQuery();
 34:       List skillNameList = new ArrayList();
 35:       while (rs.next()) {
 36:         skillNameList.add(rs.getString(3));
 37:       }
 38:       this.skills = skillHome.lookup(skillNameList);
 39:     }
 40:     catch (SQLException e) {
 41:       error("Error in ejbLoad for " + key, e);
 42:     }
 43:     catch (FinderException e) {
 44:       error("Error in ejbLoad (invalid customer or location) for " 
            + key, e);
 45:     }
 46:     finally {
 47:       closeConnection(con, stmt, rs);
 48:     }
 49:   }
 50: 
 51:   public void ejbStore(){
 52:     Connection con = null;
 53:     PreparedStatement stmt = null;
 54:     try {
 55:       con = dataSource.getConnection();
 56:       stmt = con.prepareStatement(
         "UPDATE Job 
          SET description = ?, location = ? 
          WHERE ref = ? AND customer = ?");
 57:       stmt.setString(1, description);
 58:       if (location != null) {
 59:         stmt.setString(2, location.getName());
 60:       } else {
 61:         stmt.setNull(2, java.sql.Types.VARCHAR);
 62:       }
 63:       stmt.setString(3, ref);
 64:       stmt.setString(4, customerObj.getLogin());
 65:       stmt.executeUpdate();
 66:       // delete all skills
 67:       stmt = con.prepareStatement(
         "DELETE FROM JobSkill 
          WHERE job = ? and customer = ?");
 68:       stmt.setString(1, ref);
 69:       stmt.setString(2, customerObj.getLogin());
 70:       stmt.executeUpdate();
 71:       // add back in all skills
 72:       for (Iterator iter = getSkills().iterator(); iter.hasNext(); ) {
 73:         SkillLocal skill = (SkillLocal)iter.next();
 74:         stmt = con.prepareStatement(
           "INSERT INTO JobSkill (job,customer,skill) 
            VALUES (?,?,?)");
 75:         stmt.setString(1, ref);
 76:         stmt.setString(2, customerObj.getLogin());
 77:         stmt.setString(3, skill.getName());
 78:         stmt.executeUpdate();
 79:       }
 80:     }
 81:     catch (SQLException e) {
 82:       error("Error in ejbStore for " + ref + "," + customer, e);
 83:     }
 84:     finally {
 85:       closeConnection(con, stmt, null);
 86:     }
 87:   }
 88:   // code omitted
 89: }

In the ejbLoad() method, the JobBean must load its state from both the Job and JobSkill tables, using the data in the JobSkill table to populate the skills field. In the ejbStore() method, the equivalent updates to the Job and JobSkill tables occur.

Of course, there is the chance that when the bean comes to save itself, the data could have been removed. This would happen if some user manually deleted the data; there is nothing in the EJB specification to require that an Entity bean "locks" the underlying data. In such a case, the bean should throw a javax.ejb.NoSuchEntityException; in turn, this will be returned to the client as some type of java.rmi.RemoteException. This was mentioned briefly yesterday, so look back to refresh your memory if needed. And remember, you will be learning more about exception handling and transactions on Day 8.

WARNING

To keep the case study as small and understandable as possible, the error handling in JobBean is slightly simplified. In Listing 6.5, the code will throw an EJBException (rather than NoSuchEntityException) from ejbLoad() if the data has been removed. In ejbStore(), it doesn't actually check to see if any rows were updated, so no exception would be thrown.

More complex beans can perform other processing within the ejbLoad() and ejbStore() methods. For example, the data might be stored in some denormalized form in a relational database, perhaps for performance reasons. The ejbStore() method would store the data in this de-normalized form, while the ejbLoad() methods would effectively be able to re-normalize the data on-the-fly. The client need not be aware of these persistence issues.

Another idea: these methods could be used to handle text more effectively. The EJB specification suggests compressing and decompressing text, but they could also perhaps do searches for keywords within the text, and then redundantly store these keywords separately, or the data might be converted into XML format.

As noted earlier today, there is usually very little or nothing to be done when an Entity bean is passivated or activated. Listing 6.6 shows this.

Listing 6.6—JobBean's ejbActivate() and ejbPassivate() Methods

  1: package data;
  2: 
  3: import javax.ejb.*;
  4: // imports omitted
  5: 
  6: public class JobBean implements EntityBean
  7: {
  8:   public void ejbPassivate(){
  9:     ref = null;
 10:     customer = null;
 11:     customerObj = null;
 12:     description = null;
 13:     location = null;
 14:   }
 15: 
 16:   public void ejbActivate(){
 17:   }
 18: 
 19:   // code omitted
 20: }

Implementing the Local-Home Interface Methods

The implementation of ejbCreate() and ejbPostCreate() for the JobBean is shown in Listing 6.7.

Listing 6.7—JobBean's ejbCreate() and ejbPostCreate() Methods

  1: package data;
  2: 
  3: import javax.ejb.*;
  4: import javax.sql.*;
  5: // imports omitted
  6: 
  7: public class JobBean implements EntityBean
  8: {
  9:   private String ref;
 10:   private String customer;
 11:   private String description;
 12:   private LocationLocal location;
 13:   private CustomerLocal customerObj; // derived
 14:   private List skills;  // vector field; list of SkillLocal ref's.
 15: 
 16:   public String ejbCreate (String ref, String customer) 
            throws CreateException {
 17:     // validate customer login is valid.
 18:     try {
 19:       customerObj = customerHome.findByPrimaryKey(customer);
 20:     } catch (FinderException ex) {
 21:       error("Invalid customer.", ex);
 22:     }
 23:     JobPK key = new JobPK(ref, customer);
 24:     try {
 25:       ejbFindByPrimaryKey(key);
 26:       throw new CreateException("Duplicate job name: " + key);
 27:     }
 28:     catch (FinderException ex) { }
 29:     Connection con = null;
 30:     PreparedStatement stmt = null;
 31:     try {
 32:       con = dataSource.getConnection();
 33:       stmt = con.prepareStatement(
         "INSERT INTO Job (ref,customer) 
          VALUES (?,?)");
 34:       stmt.setString(1, ref);
 35:       stmt.setString(2, customerObj.getLogin());
 36:       stmt.executeUpdate();
 37:     }
 38:     catch (SQLException e) {
 39:       error("Error creating job " + key, e);
 40:     }
 41:     finally {
 42:       closeConnection(con, stmt, null);
 43:     }
 44:     this.ref = ref;
 45:     this.customer = customer;
 46:     this.description = description;
 47:     this.location = null;
 48:     this.skills = new ArrayList();
 49:     return key;
 50:   }
 51: 
 52:   public void ejbPostCreate (String name, String description) {}
 53: }

This particular implementation validates that the customer exists (jobs are identified by customer and by a unique reference), and it also makes sure that the full primary key does not already exist in the database. If it does, the BMP bean throws a CreateException. If it doesn't (represented by the ejbFindByPrimaryKey() call throwing a FinderException), the method continues.

An alternative implementation would have been to place a unique index on the Job table within the RDBMS and then to catch the SQLException that might be thrown if a duplicate is attempted to be inserted.

WARNING

There is a race condition here. It's possible that another user could insert a record between the check for duplicates and the actual SQL INSERT. The ejbCreate() method is called within a transaction; changing the RDBMS isolation level (in a manner specified by the EJB container) would eliminate this risk, although deadlocks could then occur. Transactions, isolation levels, and deadlocks are discussed further on Day 8.

Note that the skills field is set to an empty ArrayList. This holds a list of SkillLocal references, this being the local interface to the Skill bean. Of course, for a newly created Job bean, this list is empty. The decision for the skills field to hold references to SkillLocal objects rather than, say, just Strings holding the skill names, was taken advisedly. If the skill name is used (that is, the primary key of a skill), finding information about the skill would require extra steps. Perhaps more compellingly, this is also the approach taken for CMP beans and container-managed relationships, discussed in detail tomorrow.

Also noteworthy is the customerObj field. The Job, when created, is passed just a String containing the customer's name. In other words, this is a primary key to a customer. The customerObj field contains a reference to the parent customer bean itself by way of its CustomerLocal reference.

Both the skills and the customerObj fields illustrate (for want of a better phrase) bean-managed relationships. For the skills field, this is a many-to-many relationship, from Job to Skill. For the customerObj field, this is a many-to-one relationship from Job to Customer.

As for the stateful Session beans that you learned about yesterday, the ejbCreate() and ejbPostCreate() methods both correspond to a single method called create() in the bean's local-home interface. The list of arguments must correspond. Again, as for Session beans, it is possible for there to be more than one create method with different sets of arguments, or indeed the createXXX() method naming convention can be used instead of overloading the method name of create().

The ejbRemove() method is the opposite of the ejbCreate() method; it removes a bean's data from the persistent data store. Its implementation for JobBean is shown in Listing 6.8.

Listing 6.8—JobBean's ejbRemove() Method

  1: package data;
  2: 
  3: import javax.ejb.*;
  4: import javax.naming.*;
  5: // imports omitted
  6: 
  7: public class JobBean implements EntityBean
  8: {
  9:   public void ejbRemove(){
 10:     JobPK key = (JobPK)ctx.getPrimaryKey();
 11:     Connection con = null;
 12:     PreparedStatement stmt1 = null;
 13:     PreparedStatement stmt2 = null;
 14:     try {
 15:       con = dataSource.getConnection();
 16:       stmt1 = con.prepareStatement(
         "DELETE FROM JobSkill 
          WHERE job = ? and customer = ?");
 17:       stmt1.setString(1, ref);
 18:       stmt1.setString(2, customerObj.getLogin());
 19:       stmt2 = con.prepareStatement(
         "DELETE FROM Job 
          WHERE ref = ? and customer = ?");
 20:       stmt2.setString(1, ref);
 21:       stmt2.setString(2, customerObj.getLogin());
 22:       stmt1.executeUpdate();
 23:       stmt2.executeUpdate();
 24:     }
 25:     catch (SQLException e) {
 26:       error("Error removing job " + key, e);
 27:     }
 28:     finally {
 29:       closeConnection(con, stmt1, null);
 30:       closeConnection(con, stmt2, null);
 31:     }
 32:     ref = null;
 33:     customer = null;
 34:     customerObj = null;
 35:     description = null;
 36:     location = null;
 37:   }
 38:   // code omitted
 39: }

Each of the finder methods of the local-home interface must have a corresponding method in the bean. By way of example, Listing 6.9 shows two of the (three) finder methods for the JobBean.

Listing 6.9—JobBean's Finder Methods

  1: package data;
  2: 
  3: import javax.ejb.*;
  4: import java.sql.*;
  5: import java.util.*;
  6: // imports omitted
  7: 
  8: public class JobBean implements EntityBean
  9: {
 10:   public JobPK ejbFindByPrimaryKey(JobPK key) throws FinderException {
 11:     Connection con = null;
 12:     PreparedStatement stmt = null;
 13:     ResultSet rs = null;
 14:     try {
 15:       con = dataSource.getConnection();
 16:       stmt = con.prepareStatement(
         "SELECT ref 
          FROM Job 
          WHERE ref = ? AND customer = ?");
 17:       stmt.setString(1, key.getRef());
 18:       stmt.setString(2, key.getCustomer());
 19:       rs = stmt.executeQuery();
 20:       if (!rs.next()) {
 21:         throw new FinderException("Unknown job: " + key);
 22:       }
 23:       return key;
 24:     }
 25:     catch (SQLException e) {
 26:       error("Error in findByPrimaryKey for " + key, e);
 27:     }
 28:     finally {
 29:       closeConnection(con, stmt, rs);
 30:     }
 31:     return null;
 32:   }
 33: 
 34:   public Collection ejbFindByCustomer(String customer) 
              throws FinderException {
 35:     Connection con = null;
 36:     PreparedStatement stmt = null;
 37:     ResultSet rs = null;
 38:     try {
 39:       con = dataSource.getConnection();
 40:       stmt = con.prepareStatement(
         "SELECT ref, customer 
          FROM Job 
          WHERE customer = ? 
          ORDER BY ref");
 41:       stmt.setString(1, customer);
 42:       rs = stmt.executeQuery();
 43:       Collection col = new ArrayList();
 44:       while (rs.next()) {
 45:         String nextRef = rs.getString(1);
 46:         String nextCustomer = rs.getString(2);
 47:         // validate customer exists
 48:         CustomerLocal nextCustomerObj = 
           customerHome.findByPrimaryKey(nextCustomer);
 49:         col.add(new JobPK(nextRef, nextCustomerObj.getLogin()));
 50:       }
 51:       return col;
 52:     }
 53:     catch (SQLException e) {
 54:       error("Error in findByCustomer: " + customer, e);
 55:     }
 56:     catch (FinderException e) {
 57:       error("Error in findByCustomer, invalid customer: " + customer, e);
 58:     }
 59:     finally {
 60:       closeConnection(con, stmt, rs);
 61:     }
 62:     return null;
 63:   }
 64:
 65:  // code omitted
 66: }

The implementation of the ejbFindByPrimaryKey() method might seem somewhat unusual; it receives a primary key, and then returns it. Of course, it has validated that an entity exists for the given primary key; if there were none, a javax.ejb.ObjectNotFoundException would be thrown. The implementation of ejbFindByCustomer() is straightforward enough.

The Job bean defines a home method, namely deleteByCustomer(), and the corresponding method in the JobBean class is ejbHomeDeleteByCustomer(), as shown in Listing 6.10.

Listing 6.10—JobBean.ejbHomeDeleteByCustomer() Home Method

  1: package data;
  2: 
  3: import javax.ejb.*;
  4: import java.sql.*;
  5: import java.util.*;
  6: // imports omitted
  7: 
  8: public class JobBean implements EntityBean
  9: {
 10:   public void ejbHomeDeleteByCustomer(String customer) {
 11:     Connection con = null;
 12:     PreparedStatement stmt2 = null;
 13:     PreparedStatement stmt1 = null;
 14:     try {
 15:       con = dataSource.getConnection();
 16:       stmt1 = con.prepareStatement(
         "DELETE FROM JobSkill 
          WHERE customer = ?");
 17:       stmt2 = con.prepareStatement(
         "DELETE FROM Job 
          WHERE customer = ?");
 18:       stmt1.setString(1, customer);
 19:       stmt2.setString(1, customer);
 20:       stmt1.executeUpdate();
 21:       stmt2.executeUpdate();
 22:     }
 23:     catch (SQLException e) {
 24:       error("Error removing all jobs for " + customer, e);
 25:     }
 26:     finally {
 27:       closeConnection(con, stmt1, null);
 28:       closeConnection(con, stmt2, null);
 29:     }
 30:   }
 31:  // code omitted
 32: }

Implementing the Local Interface Methods

Each of the methods in the local interface has a corresponding method in the bean itself. The corresponding methods for JobBean are shown in Listing 6.11.

Listing 6.11—Business Methods of JobBean Correspond to the Methods of the Local Interface

  1: package data;
  2: 
  3: import java.rmi.*;
  4: import javax.ejb.*;
  5: // imports omitted
  6: 
  7: public class JobBean implements EntityBean
  8: {
  9:   public String getRef() {
 10:     return ref;
 11:   }
 12:   public String getCustomer() {
 13:     return customer;
 14:   }
 15:   public CustomerLocal getCustomerObj() {
 16:     return customerObj;
 17:   }
 18:   public String getDescription() {
 19:     return description;
 20:   }
 21:   public void setDescription(String description) {
 22:     this.description = description;
 23:   }
 24:   public LocationLocal getLocation() {
 25:     return location;
 26:   }
 27:   public void setLocation(LocationLocal location) {
 28:     this.location = location;
 29:   }
 30:   /** returns (copy of) skills */
 31:   public Collection getSkills() {
 32:     return new ArrayList(skills);
 33:   }
 34:   public void setSkills(Collection skills) {
 35:     // just validate that the collection holds references to SkillLocal's
 36:     for (Iterator iter = getSkills().iterator(); iter.hasNext(); ) {
 37:       SkillLocal skill = (SkillLocal)iter.next();
 38:     }
 39:     // replace the list of skills with that defined.
 40:     this.skills = new ArrayList(skills);
 41:   }
 42:  // code omitted
 43: }

The getSkills() and setSkills() methods bear closer inspection. The getSkills() method returns a copy of the local skills field because, otherwise, the client could change the contents of the skills field without the JobBean knowing. This isn't an issue that would have arisen if the interface to JobBean was remote, because a copy would automatically have been created. Turning to the setSkills() method, this checks to make sure that the new collection of skills supplied is a list of SkillLocal references. This is analogous to the setLocation() method that was discussed; the Job Entity bean is enforcing referential integrity with the Skill Entity bean.

Generating IDs

Sometimes an Entity bean already has a field(or fields) that represent the primary key, but at other times, the set of fields required may just be too large. Alternatively, the obvious primary key may not be stable in the sense that its value could change over the lifetime of an entity—something prohibited by the EJB specification. For example, choosing a (lastname, firstname) as a means of identifying an employee may fail if a female employee gets married and chooses to adopt her husband's surname.

In these cases, it is common to introduce an artificial key, sometimes known as a surrogate key. You will be familiar with these if you have ever been allocated a customer number when shopping online. Your social security number, library card number, driver's license, and so on may well be just a pretty meaningless jumble of numbers and letters, but they are guaranteed to be unique and stable.

With BMP Entity beans, the responsibility for generating these ID values will yours—the bean provider. Whether numbers and letters or just numbers are used is up to you, although just numbers are often used in an ascending sequence (that is, 1, 2, 3, and so on). If you adopt this strategy, you could calculate the values by calling a stateless Session bean—a number fountain, if you will. A home method could perhaps encapsulate the lookup of this NumberFountainBean.

The implementation of such a NumberFountainBean can take many forms. There will need to be some persistent record of the maximum allocated number in the series, so a method such as getNextNumber("MyBean") could return a value by performing an appropriate piece of SQL against a table held in an RDBMS:

begin tran

update number_fountain
set  max_value = max_value + 1
where bean_name = "MyBean"

select max_value
from  number_fountain
where bean_name = "MyBean"

commit

One disadvantage with this approach is that the NumberFountainBean—or rather, the underlying database table—can become a bottleneck. A number of strategies have been developed to reduce this. One is to make the getNextNumber() method occur in a different transaction from the rest of the work. You will learn more about transactions on Day 8; for now, it is just necessary to know that while this will increase throughput, there is the risk of gaps occurring in the sequence.

If non-contiguous sequences are acceptable, even better throughput can be achieved by implementing a stateless Session bean that caches values in memory. Thus, rather than incrementing the maximum value by 1, it can increment by a larger number, perhaps by 100. Only every 100th call actually performs an SQL update, and the other 99 times, the number is allocated from memory. Of course, if the system crashes or power fails, there could quite a large gap.

A final enhancement that improves scalability further and also improves resilience is to arrange for there to be a number of beans, each with a range of values. For example, these might be allocated using a high-order byte/low-order bytes arrangement.

NOTE

Countries that allocate car license plates by state or by district are effectively using this approach.

One advantage of implementing a Session bean, such as NumberFountainBean, is that it isolates the dependency on the persistent data store that is holding the maximum value. Also, the SQL to determine the next available number is easily ported across RDBMS. On the other hand, many organizations use only a single RDBMS, so such portability is not needed. In these cases, the RDBMS may have built-in support for allocating monotonically increasing numeric values, and this can be used directly. For example, SEQUENCEs can be used in Oracle, while both Microsoft SQL Server and Sybase provide so-called identity columns and the @@identity global variable. So, for BMP Entity beans, another way to obtain the next value is to perform the SQL INSERT, obtaining the value from the back-end RDBMS. Note that most of these tools have the same scalability issues as the home-grown NumberFountainBean, and most also provide optimizations that can result in gaps in the series.

There is an alternative to using numbers for ID values, namely to generate a value that must be unique. On Windows PCs, you may well have seen strings in the format

{32F8CA14-087C-4908-B7C4-6757FE7E90AB}

In case you are wondering, this was found by delving into the Windows Registry and (apparently) represents the FormatGUID for .AVI files (whatever that means!). The point is that it is—to all intents and purposes—guaranteed to be unique. In the case of GUIDs, it is unique because it is based on the MAC address of the ethernet card of the PC, plus the time.

Clearly, other algorithms can be created, and a quick search on the Web should throw up some commercial products and free software from which to select. For example, one algorithm generates values unique to the millisecond, the machine, the object creating the ID, and the top-most method of the call stack.

Granularity Revisited

A recurrent theme when developing Entity beans is in selecting an appropriate granularity for the bean. Prior to EJB 2.0, Entity beans could only provide a remote interface, which meant that a relatively coarse grained interface was required to minimize the client-to-bean network traffic. Indeed, this is still the recommendation made for Session beans in EJB 2.0 that have a remote interface.

With EJB 2.0, Entity beans can have a local interface, meaning that the cost of interaction with the client becomes minimal. If the cost of interaction of the Entity bean to the persistent data store is not too high, fine-grained Entity beans are quite possible. This may be true, either because the EJB container can make optimize the database access in some way (true only for CMP Entity beans) or if the data store resides on the same computer as the EJB container and, ideally, within the same JVM process space.

NOTE

Running a persistent data store in the same process space as the EJB container is quite possible; a number of pure Java RDBMS—including Cloudscape, the database bundled with the J2EE RI—provide an "embedded mode."

Under BMP however, the advice is generally not to use fine-grained Entity beans, principally because the EJB container will be unable to perform any database access optimization. Choosing the granularity is then best determined by focusing on the identity and lifecycle on the candidate Entity beans. Hence, order and order-detail should be a single Order bean, but customer and order, while related, should be kept separate.

In the case study, you will find that the Job bean writes to both the Job table and also the JobSkill table (to record the skill(s) needed to perform the job).

Beware Those Finder Methods!

As you now have learned, Entity beans can be created, removed, and found through their home interface. While these all seem straightforward enough operations, there's danger lurking in the last of these; finder methods can be dangerous. Specifically, they can cripple the performance of your application if used incorrectly.

This probably doesn't seem obvious, but if you consider the interplay between the EJB container (implementing the local-home interface) and the bean itself, it becomes easier to see:

  • The local-home interface's findManyByXxx() method is called. For the purposes of this discussion, this finder method returns a Collection.

  • The local-home interface delegates to a pooled bean, calling its ejbFindManyByXxx() method. This performs an SQL SELECT statement (or the equivalent), and returns back a Collection of primary keys to the local-home interface.

  • The local-home interface instantiates a Collection the same size as was obtained from the bean and populates it with local proxies. Each local proxy is assigned a primary key.

So far so good, the client receives a Collection of proxies. Suppose now that the client iterates over this collection, calling some getter method getXxx().

  • The client calls getXxx() on the first proxy in its Collection. The proxy holds a primary key, but there is no corresponding bean actually associated with the proxy. Therefore, the EJB container activates a bean from the pool, calls its ejbLoad() lifecycle method, and then finally delegates the getXxx() business method. After that method has completed, the ejbStore() method is called.

  • This process continues for all of the proxies in the collection.

You can probably see the problem; the persistent data store will be hit numerous times. First, it will be hit as a result of the ejbFindManyByXxx() method; this will return a thin column of primary key values. Then, because ejbLoad() is called for each bean, the rest of the data for that row is returned. This is shown in Figure 6.8.

Figure 6.8 Finder methods can result in poor performance under BMP.

Consequently, if 20 primary keys were returned by the bean following the initial ejbFindManyByXxx(),the database will be hit in all 41 times—once for the initial SELECT and two times each for each of the beans.

There are a number of techniques that can eliminate this overhead, each with pros and cons:

  • The most obvious solution is to not use finder methods that return Collections of many beans. Instead, use stateless Session beans that perform a direct SQL SELECT query against the database, iterate over the ResultSet, and return back a Collection of serializable value objects that mirror the data contained in the actual entity. This technique is called the Fast-lane Reader.

  • Another technique that can be used is to alter the definition of the primary key class. As well as holding the key information that identifies the bean in the database, it also holds the rest of the bean's data as well. When the original finder bean returns the Collection of primary keys, the primary keys are held by the local proxies. When the beans are activated, they can obtain their state from the proxy by using entityContext.getLocalObject().getPrimaryKey(). This technique has been dubbed the fat key pattern, for obvious reasons.

  • Last, you may be able to remove the performance hit by porting the bean to use container managed persistence. Because under CMP the EJB container is responsible for all access to the persistent data store, many will obtain all the required information from the data store during the first findManyByXxx method call, and then eagerly instantiate beans using this information. You will be learning more about CMP tomorrow.

Incidentally, Figure 6.8 shows a graphic illustration of why Entity beans should, in general, define only a local interface—not a remote interface. If the client were remote rather than local, the total network calls for a finder method returning references to 20 beans would be double the original figure, namely 42! Moreover, every subsequent business method invocation (call to getYYY() for example) would inflict a further 20 network calls.

EJB Container Performance Tuning

Many organizations are wary of using Entity beans because of the performance costs that are associated with it. You have already seen the performance issues arising from using finder methods, but even ignoring this, any business method to an Entity bean will require two database calls—one resulting from the ejbLoad() that precedes the business method and one from the ejbStore() to save the bean's new state back to the data store.

Of course, these database calls may be unnecessary. If a bean hasn't been passivated since the last business call, the ejbLoad() need not do anything, provided that nothing has updated the data store through non-EJB mechanisms. Also, if the business method called did not change the state of the bean was, for example, a getter accessor method, the ejbStore() has nothing to do also.

Another scenario is where a bean is interacted with several times as part of a transaction. You will be learning more about transactions on Day 8, so for now, just appreciate that when a bean is modified through the course of a transaction, either all of its changes in state or none of them need to be persisted. In other words, there is only really the requirement to call ejbStore() once at the end of the transaction.

Taking these points together, the amount of network traffic from the EJB container to the persistent data store can be substantially reduced, down to the levels that might be expected in a hand-written J2SE client/server application. Although not part of the EJB specification, many EJB containers provide proprietary mechanisms to prevent unnecessary ejbLoad() or ejbStore() calls. Of course, the use of these mechanisms will make your bean harder to port to another EJB container, but you may well put up with the inconvenience for the performance gains realized. Indeed, if you are in the process of evaluating EJB containers, as many companies are, you may even have placed these features on your requirements list.

  • + Share This
  • 🔖 Save To Your Account