ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


EJB Inheritance, Part 1

by Emmanuel Proulx
09/04/2002

Java is an object-oriented language, which means it follows the principles of object-oriented programming, such as encapsulation, inheritance, and polymorphism. (Inheritance and polymorphism pretty much go together, so they are often simply referred to as inheritance, which is what I will do in these articles.) These are great principles and can be used to represent relationships between objects in very powerful ways.

Entity beans are objects that represent data coming from a persistent store, such as a database. The key word here is objects. Entity beans encapsulate the data and business logic. But what about the two other principles, inheritance and polymorphism?

The bad news is that entity beans do not easily enable the use of these principles. The good news is that if you follow certain restrictions and tricks, it can be done. This series of articles describes some techniques to put inheritance and polymorphism back into entity beans.

Why EJB Inheritance?

Most systems today use direct SQL access to flat relational databases. Since entity beans are often mapped to relational databases, why use inheritance? Let's not forget that SQL and relational database technology date back to the days of COBOL. Object-oriented programming was invented later, with some specific goals in mind:

Source Code

Download the source code for this article. This zip file contains a WebLogic Server domain, EJB source code, and a PointBase database. Install this under C:\inherit. The admin login is:

username: inherit 
password: password

These goals do not apply to pure data, but rather to the behavior of objects. This means EJB inheritance is likely to be visible in the code, but not in the database. We can live with that, as long as the mapping between the data and the objects is done in a smart way. The advantages of EJB inheritance should be obvious when we see an example.

Recurrent Traveler Miles -- Requirements

Imagine a company named Recurrent Traveler Miles (RTM). This company has a customer retention plan similar to the airlines' "frequent flier miles" systems. Every time you spend a specified amount of money, you obtain a point or "reward mile." When you accumulate enough points, you earn some sort of prize, typically a free flight.

In this system, there are three kinds of customers: Regular, Gold, and Platinum. The differences between these three customers are the rules used for attributing and redeeming points. Those rules are listed here:

Table 1. Customer types and requirements

Customer

Purchase amount to get one point

Number of points to get a free trip

Regular

$20

Same region: 1500
Further away: 3500
Same continent: 10500
Out-of-continent: 25000

Gold

$18

Same region: 1300
Further away: 3100
Same continent: 10000
Out-of-continent: 22000

Platinum

Note: Platinum customers get to choose a charity. RTM will give 1% of all purchases to that charity.

$15

Same region: 1200
Further away: 2850
Same continent: 9500
Out-of-continent: 20000

These numbers are all arbitrary, so don't get mad if they don't make any sense. It's just an example.

With these requirements it's easy to do a single CMP entity bean, CustomerEJB, and put all of this business logic in a single place. Let's see what kind of code we obtain by doing this. Here's one method:

Example 1. Single entity bean method

public int redeemPoints(int zone) {
  int pts = 0;
  if(getType().equals("REGULAR")) {
    switch(zone) {
      case 1: pts=1500; break;
      case 2: pts=3500; break;
      case 3: pts=10500; break;
      case 4: pts=25000; break;
      default: return 0;
    }
  } else if (getType().equals("GOLD")) {
    switch(zone) {
      case 1: pts=1300; break;
      case 2: pts=3100; break;
      case 3: pts=10000; break;
      case 4: pts=22000; break;
      default: return 0;
    }
  } else if (getType().equals("PLATINUM")) {
    switch(zone) {
      case 1: pts=1200; break;
      case 2: pts=2850; break;
      case 3: pts=9500; break;
      case 4: pts=20000; break;
      default: return 0;
    }
  }
  if(getPoints() < pts) return 0;
  setPoints(getPoints()-pts);
  return pts;
}

How ugly. I could break apart this method into small pieces, it's true, but it wouldn't change the fact that this is bad design. The sure sign that object-orientation is missing here is that big if() else if() else if() block. There has to be a better way.

The Object-Oriented Way

Figure 1 shows how the system should be designed. There are three objects here, just like real life: Base (Regular), Gold, and Platinum customers. They are CMP entity beans.


Figure 1. Object-oriented approach

Note that there are really three sets of objects to manipulate: local interfaces, bean classes, and local home interfaces. We see here the bean classes. The same design is found in the local interfaces, but not in the local home interfaces. We'll see why later.

Overview of the Steps

Related Reading

Enterprise JavaBeans
By Richard Monson-Haefel

The following method enables using inheritance with CMP entity beans, straightforwardly and reliably. The steps are summarized here, and they will be explained afterwards.

  1. Create a base bean class implementing EntityBean. Write bean subclasses extending base bean class.
  2. Create a base local interface, extending EJBLocalObject. Create sub-interfaces extending base local interface.
  3. Write a home interface for base class, extending EJBLocalHome. Write home interfaces for subclass, extending EJBLocalHome as usual. Don't extend home interface of base class.
  4. In each subclass' create or postCreate method, call corresponding base class's create or postCreate method.
  5. For each finder of base class, write a corresponding home method that will invoke finder in all subclasses. For each ejbSelect of the base class, write a regular method that will invoke ejbSelect in all subclasses.
  6. Optional: Copy home method signatures to subclasses' home interfaces. Override home methods as required.
  7. Set up vendor-specific table mapping.

Let's now explore each step more thoroughly.

1. Bean Classes

Write the base bean class BaseCustomerBean. This abstract class implements EntityBean. Then write GoldCustomerBean and PlatinumCustomerBean. The code is provided below for BaseCustomerBean and PlatinumCustomerBean:

Example 2. BaseCustomerBean

//Removed for readability: package, imports.
public abstract class BaseCustomerBean implements EntityBean {
  // Removed for readability: constructor, entity 
  // context, activate, passivate, load, store, 
  // create, postCreate, remove. 
  public abstract String getCustomerID();
  public abstract void setCustomerID(String newCustomerID);
  public abstract String getName();
  public abstract void setName(String newName);
  public abstract int getPoints();
  public abstract void setPoints(int number);
  public int addPoints(int purchaseAmount) {
    int pts = 0;
    pts = purchaseAmount/getPointFactor();
    setPoints(getPoints() + pts);
    return pts;
  }
  public int redeemPoints(int zone) {
    int pts = getZoneThreshold(zone);
    if(getPoints() < pts) return 0;
    setPoints(getPoints()-pts);
    return pts;
  }
  protected int getPointFactor() {
    return 20;
  }
  protected int getZoneThreshold(int zone) {
    switch(zone) {
      case 1: return 1500;
      case 2: return 3500;
      case 3: return 10500;
      case 4: return 25000;
      default: return Integer.MAX_VALUE;
    }
  }
}

Example 3. PlatinumCustomerBean

//Removed for readability: package, imports.
public abstract class PlatinumCustomerBean extends basecustomer.BaseCustomerBean
{
  // Removed for readability: constructor, entity
  // context, activate, passivate, load, store, 
  // create, postCreate, remove. 
  public abstract String getCharity();
  public abstract void setCharity(String charity);
  protected int getPointFactor() {
    return 15;
  }
  protected int getZoneThreshold(int zone) {
    switch(zone) {
      case 1: return 1200;
      case 2: return 2850;
      case 3: return 9500;
      case 4: return 20000;
      default: return Integer.MAX_VALUE;
    }
  }
}

As expected, the code for PlatinumCustomerBean is much shorter than for BaseCustomerBean. Can you see the reuse going on? Note how almost all of the base class fields and business logic are kept in the subclass. Note also how "shared" business logic was put in the base class, and class-specific business logic was put in separate methods, overridden in the subclasses.

Other techniques exist to provide inheritance-like behavior, like aggregation (the Strategy pattern). But these don't rid us of the dreaded if() else if() else if() block.

2. Local Interfaces

The local interfaces have a similar design. The local interface BaseCustomer extends EJBLocalObject, and the sub-interfaces extend BaseCustomer. Here's the code:

Example 4. Local interface for BaseCustomer

//Removed for readability: package, imports.
public interface BaseCustomer extends EJBLocalObject {
  public String getCustomerID();
  public String getName();
  public void setName(String newName);
  public int getPoints();
  public int addPoints(int purchaseAmount);
  public int redeemPoints(int zone);
}

Example 5. Local interface for PlatinumCustomer

//Removed for readability: package, imports.
public interface PlatinumCustomer extends basecustomer.BaseCustomer
{
  public String getCharity();
  public void setCharity(String newCharity);
}

Note the new accessor method in PlatinumCustomer, to handle the charity's name. We could have added new business methods, too.

3. Local Home Interfaces

If there's a single place where inheritance clashes with entity EJBs, it's the home interfaces. They can't extend each other! Why is that? Because of the way the specification is written. Let's take, for example, the case of a create method. The specification says the return type of all create methods must be the business interface (either the local or remote interface). In a theoretical base home interface, the returned type would be the base local interface. If you try to write a home sub-interface, the return type would be wrong.

We'll have to go through some workarounds in order to impose inheritance-like behavior for the methods of the home interface. There are three kinds of methods that can be found in home interfaces:

We will later see how these methods can be conceived so that they emulate inheritance. But for now, write the home interfaces using the regular technique; they should extend EJBLocalHome.

4. create & postCreate Methods

Emulating inheritance in create and postCreate methods is relatively easy. Simply put, all you'll have to do is to call the right super.ejbCreate() and super.ejbPostCreate() in the subclasses. Here's an example based on the RTM system:

Example 6. BaseCustomerBean

//...
public abstract class BaseCustomerBean implements EntityBean {
  //...
  public String ejbCreate(String customerID, String name)
    throws CreateException
  {
    setCustomerID(customerID);
    setName(name);
    setPoints(0);
    return null;
  }
  public void ejbPostCreate(String customerID, String name)
{
  }
  //...
}

Example 7. PlatinumCustomerBean

//...
public abstract class PlatinumCustomerBean extends basecustomer.BaseCustomerBean
{
  //...
  public String ejbCreate(String customerID, String name, String charity)
    throws CreateException
  {
    super.ejbCreate(customerID,name);
    setCharity(charity);
    return null;
  }
  public void ejbPostCreate(String customerID, String name, String charity) {
    super.ejbPostCreate(customerID,name);
  }
  //...
}

Here, the subclass' ejbCreate() calls the superclass' ejbCreate() to initialize most of the fields, and then it initializes the remaining fields. This works because creation is handled by your code. ejbPostCreate() works the same way. Even though the base class' ejbPostCreate() is empty, it is good practice to call super.ejbPostCreate() just in case this changes in the future.

Note that our create methods in the subclasses override the ones in the base class. But subclasses can create new create methods, with different parameters. This works fine too, as long as fields are initialized properly.

5. Finders & ejbSelects

Working out inheritance in finders and ejbSelect methods is not so simple. Finders are written by the container, using EJB-QL provided in the deployment descriptor. There's no hook for us to exploit.

In reality, a finder exists to find objects of a single type. This is highlighted in our example by the fact that each finder bares the name of its returned object; for example, findAllGoldCustomers(). Finders cannot find objects from the base type and all of its sub-types; they were not made for that. Queries are not polymorphic.

A solution is to use what I call locate methods -- essentially "smart finders," which know how to handle the base class' and subclasses' finders in order to collect the results properly. Locate methods have to know about subclasses, special finders that may be used depending on the table mapping, etc. Some other things about locate methods:

Locate methods are not natural in the world of inheritance and polymorphism, but they are necessary. I am working on ways to make the task of authoring locate methods easier. I plan to report my findings in future articles.

Let's see some examples of finders and locate methods.

First, the home interfaces:

Example 8. BaseCustomerHome

//...
public interface BaseCustomerHome extends EJBLocalHome {
//...
  public BaseCustomer findByPrimaryKey(String customerID)
    throws FinderException;

  public Collection findAllBaseCustomers()
    throws FinderException;

  public BaseCustomer locate(String customerID)
    throws FinderException;

  public Collection locateAll()
    throws FinderException;
}

Example 9. PlatinumCustomerHome

//...
public interface PlatinumCustomerHome extends EJBLocalHome {

//...
  public PlatinumCustomer findByPrimaryKey(String customerID)
    throws FinderException;

  public Collection findAllPlatinumCustomers()
    throws FinderException;
}

Note that the finders are implemented by the container based on these query definitions:

Example 10. Query definitions for BaseCustomer

<query>
  <query-method>
    <method-name>findAllBaseCustomers</method-name>
    <method-params>
    </method-params>
  </query-method>
  <ejb-ql>
    <![CDATA[SELECT OBJECT(c) FROM BaseCustomerEJB AS c</XMLCDATA>
  </ejb-ql>
</query>

Example 11. Query definitions for PlatinumCustomer

<query>
  <query-method>
    <method-name>findAllPlatinumCustomers</method-name>
    <method-params>
    </method-params>
  </query-method>
  <ejb-ql>
    <![CDATA[SELECT OBJECT(c) FROM PlatinumCustomerEJB AS c</XMLCDATA>
  </ejb-ql>
</query>

The GoldCustomer bean has a similar implementation.

Now the tricky part: how to implement the locate methods in the base class:

Example 12. Implementing the locate method for a single customer

  public BaseCustomer ejbHomeLocate(String customerID) throws
FinderException {
    //At this point, all base EJB classes must know their EJB subclasses...
    Context jndiCtx;
    BaseCustomerHome bHome;
    GoldCustomerHome gHome;
    PlatinumCustomerHome pHome;
    try {
      jndiCtx = new InitialContext();
      bHome = (BaseCustomerHome)ctx.getEJBLocalHome();
      gHome = (GoldCustomerHome)jndiCtx.lookup("GoldCustomerEJB");
      pHome = (PlatinumCustomerHome)jndiCtx.lookup("PlatinumCustomerEJB");
    } catch (NamingException e) {
      throw new FinderException("Cannot obtain JNDI initial context.");
    }
    BaseCustomer b=null;
    try {
      b = bHome.findByPrimaryKey(customerID);
      return b;
    } catch (FinderException e) {
    }
    try {
      b = gHome.findByPrimaryKey(customerID);
      return b;
    } catch (FinderException e) {
    }
    try {
      b = pHome.findByPrimaryKey(customerID);
      return b;
    } catch (FinderException e) {
    }
    throw new FinderException("Cannot find any bean with that key.");
  }

Example 13. Implementing the locate method for all customers

  public Collection ejbHomeLocateAll() throws FinderException
{
    Context jndiCtx;
    BaseCustomerHome bHome;
    GoldCustomerHome gHome;
    PlatinumCustomerHome pHome;
    try {
      jndiCtx = new InitialContext();
      bHome = (BaseCustomerHome)ctx.getEJBLocalHome();
      gHome = (GoldCustomerHome)jndiCtx.lookup("GoldCustomerEJB");
      pHome = (PlatinumCustomerHome)jndiCtx.lookup("PlatinumCustomerEJB");
    } catch (NamingException e) {
      throw new FinderException("Cannot obtain JNDI initial context.");
    }
    //At this point, all base EJB classes must know their EJB subclasses...
    ArrayList l = new ArrayList();
    l.addAll(bHome.findAllBaseCustomers());
    l.addAll(gHome.findAllGoldCustomers());
    l.addAll(pHome.findAllPlatinumCustomers());
    return l;
  }

The logic here is as follows:

Note that, depending on your table mapping, the code for finders can be quite different. My next article will be dedicated to the various techniques for table mapping.

I know this code is ugly, but it is the only way I have found so far to emulate inheritance in finder methods. If you find a better solution, let me know.

But what about ejbSelect methods? The same principle exists with ejbSelect methods; they must be wrapped with other methods in the bean implementation. I decided to give them the prefix ejbLocate and call them ejbLocate methods. Note that they are not exposed through the home interfaces, just like regular ejbSelect methods.

6. Home Methods

Note that since home interfaces are not inherited, if you want to allow someone to invoke a home method on a subclass you will have to manually copy the home method's signature from the base class home interface to the subclass home interface.

When a home method returns an EJB object in the hierarchy, write the method signature so that it returns Object. Then the home method can be polymorphic (you can override them in subclasses). The only issue is that you'll have to cast the result to one of the local interfaces on the client side.

Home method implementation requires a similar approach to that for create and finder methods. Business logic of the superclass can be used by calling super.ejbHomeSomething(). The result can then be used or augmented in the subclass' home method. I didn't write an example, but you can refer to the create and finder methods above if you need inspiration.

7. Table Mapping

We're all done with the coding part. Up until now, our code followed the EJB 2.0 standard. Now comes the vendor-specific part. Once our entity beans are written, we must map them to database tables. I have chosen to map each bean to a different, unrelated table. Figure 2 gives a diagram of the database schema:


Figure 2. Database schema

Note the following:

This design is known as "horizontal mapping." There's a one-to-one correspondence between the fields in the database tables and the fields in the entity beans.

Since this part is vendor-specific, I didn't quote any code here. If you want to look at an implementation, look at the one I wrote using WebLogic Server 7 (with the provided PointBase database). You can download the complete RTM example as a zip file.

In the next article, we'll talk about more options for mapping entity beans to database tables.

Other Considerations

Primary Keys

In our example, the primary key was a String. This example relies on the rule that all keys must be unique across objects of all types. You can't have a GoldCustomer and a PlatinumCustomer with the same key. This rule is essential for the locate methods to work properly.

In the case where the key is compound, a primary key class must be written. This class can be shared among all related classes in a hierarchy, but sometimes an entity bean subclass has a different set of fields. In that case, multiple key classes can be provided.

Primary keys can also inherit from one another. This means a key for an entity bean subclass can inherit from the key of its base class. Note that primary key subclasses are specializations of their superclasses. This means, by subclassing, you can't remove or modify fields of the primary key base class. You can only add fields. This will very rarely be useful.

If you use different keys for the entity beans of your hierarchy, you must modify your system as follows:

EJB Container Providers

Not all EJB containers behave the same. Some vendors prohibit entity beans from extending other classes. This is due to the different ways the specification can be interpreted.

The example I provided works fine under WebLogic Server 7, and should be easily adaptable to other EJB containers. I didn't have the time to test the example under other products. If you're using another product, please let us know if the example works, and what changes need to be done to make it work.

In the future ...

The EJB specification (2.0, and even 2.1 public draft) mentions a few things regarding inheritance:

In the future, the EJB specification may be changed to allow inheritance in entity beans (see specification, appendix A). In order to do that, a couple of things will have to change:

What will the EJB committee come up with?

In This Series

EJB Free and Open Source Tools Summary
What's the best platform for J2EE development? Emmanuel Proulx finds himself answering that question time after time. In this article, he explores several free-as-in-speech and free-as-in-beer EJB 2.0 tools and gives his suggestions for choosing an application server.

EJB Inheritance, Part 4
This series has demonstrated all sorts of ways to handle inheritance in beans. With web and message services, though, how do you handle inheritance with remotely-invoked beans? The EJB 2.0 specification allows it; Emmanuel Proulx demonstrates how.

EJB Inheritance, Part 3
Session beans can take advantage of inheritance, just like entity beans. Indeed, implementing session bean inheritance is nowhere near as hard as it is with entity beans. Part 3 of this series shows the proper technique for implementing inheritance in session beans and addresses the use of factories.

EJB Inheritance, Part 2
Part two of this series on inheritance with Entity Java Beans focuses on the various options for table mapping.

Reality Check

Entity bean inheritance isn't going to make your life that much easier. It may make the code smaller, with less repetition, and more maintainable. But there is some work involved (especially for finders and table mapping), which may negate these benefits.

Before you go ahead and merge all of your entity beans into a hierarchy, ask yourself what kind of improvement and effort are involved. If the repeated code is small, it may take less effort to isolate it into a utility class. That's what I would do if I had to refactor a COBOL program (*wink*).

But if you have many objects that are based on each other, using inheritance may result in the most maintainable system.

Emmanuel Proulx is an expert in J2EE and Enterprise JavaBeans, and is a certified WebLogic Server 7.0 engineer. He works in the fields of telecommunications and web development.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.