Book Home Enterprise JavaBeans Search this book

9.7. Entity Bean Relationships

Business systems frequently define relationships between entity beans. These relationships can be complex or simple. The concept of a cabin and its relationship to a ship embodies both simple and complex relationships. A cabin always belongs to a particular ship--a relationship that's obviously fairly simple. From the ship's perspective, the relationship is more complex: a ship has many cabins and must maintain a relationship to all of them.

This section explores how to write entity beans that use container-managed persistence and maintain relationships with other beans. This information will be most useful to EJB 1.0 developers. EJB 1.0 does not allow references to other beans to be container-managed. This means that a bean needs to manage persistence for references to other beans within its own code.

If you're using EJB 1.1, you can probably ignore this section, particularly if your server has robust support for the persistence of bean references. While EJB 1.1 allows bean references to be container-managed fields, a few EJB 1.1 servers may not be able to persist relationships between beans. This section will be useful to developers using these limited EJB 1.1 servers. EJB 1.1 developers using bean-managed persistence may also find the strategies in this section useful.

However, since this is predominately a EJB 1.0 problem, the code in this section has been left in the EJB 1.0 style. EJB 1.1 developers must make minor changes to the code for it to work in EJB 1.1 servers.

9.7.1. Simple Associations

In Titan Cruises, the business concept of a cabin models the real-world cabins that are in all of Titan's ships. Regardless of the ship, cabins all have certain attributes that we want to capture in the Cabin business concept: cabin name, deck level, and so forth. Important to this discussion is the cabin's relationship to its ship. How do we model and implement this simple relationship?

There are several alternatives, which can be grouped into two general categories: implementation-specific and non-implementation-specific. Both categories have their own strengths and weaknesses, which we will explore later in this section.

Let's start by defining the Cabin bean's remote interface. We add methods that allow the cabin to set and get the Ship as a bean, rather than by its ID. The advantage of this approach is that it encapsulates the Ship bean's unique identifier and deals with business concepts as beans, not database-dependent IDs.

package com.titan.cabin;
import com.titan.ship.Ship;
import java.rmi.RemoteException;

public interface Cabin extends javax.ejb.EJBObject {
    public Ship getShip() throws RemoteException;
    public void setShip(Ship ship) throws RemoteException;
    
    public String getName() throws RemoteException;
    public void setName(String str) throws RemoteException;
    public int getDeckLevel() throws RemoteException;
    public void setDeckLevel(int level) throws RemoteException;
    public int getBedCount() throws RemoteException;
    public void setBedCount(int bc) throws RemoteException; 
}

9.7.1.1. Maintaining the database mapping

The simplest strategy for managing the cabin-to-ship relationship would be to support the relationship as defined in the database. In the relational database, the CABIN table includes a foreign key to the SHIP table's primary key, called SHIP_ID. In its current definition, we maintain the database mapping by preserving the SHIP_ID as an integer value. We can modify the behavior slightly by using the Ship bean in the get and set methods rather than the ID:

public class CabinBean implements javax.ejb.EntityBean {

    public int id;
    public String name;
    public int deckLevel;
    public int bedCount;
    public int ship_id;
    public Ship ship;

    public javax.ejb.EntityContext ejbContext;
    public transient javax.naming.Context jndiContext;

    public Ship getShip() throws RemoteException {
      try {
        if (ship != null)
            return ship;
        else {
            ShipHome home = (ShipHome)getHome("jndiName_ShipHome");
            ship = home.findByPrimaryKey(new ShipPK(ship_id));
            return ship;
        }
      } catch(javax.ejb.FinderException fe) {
         throw new RemoteException("Invalid Ship",fe);
      }
    }
    public void setShip(Ship ship) throws RemoteException {
        ship_id = ((ShipPK)ship.getPrimaryKey()).id;
        this.ship = ship;
    }
    protected Object getHome(String name) throws RemoteException {
        try {
            String jndiName = 
                ejbContext.getEnvironment().getProperty(name);
            return getJndiContext().lookup(jndiName);
        } catch(javax.naming.NamingException ne) {
            throw new RemoteException("Could not lookup ("+name+")",ne);
        }
    }
    private javax.naming.Context getJndiContext() 
        throws javax.naming.NamingException {
        if (jndiContext != null)
            return jndiContext;
            
        Properties p = new Properties();
        
        // ... Specify the JNDI properties specific to the vendor.

        jndiContext = new InitialContext(p);
        return jndiContext;
    }
    public void ejbActivate() {
        ship = null;
    }
}

From the client's standpoint, the Cabin bean now models its relationship to the Ship bean as a business concept and not as a database ID. The advantage of this approach is that we maintain the database mapping defined by the CABIN table while hiding the mapping from the client. The disadvantage is that the bean must frequently dereference the ship's primary key whenever a client invokes the getShip() method. If the entity bean has been deactivated since the ship's remote reference was last obtained, the reference will need to be reobtained.

After you've deployed this new version of the Cabin bean, you can use a client application to see if your code works. You can use code like this in your client:

Context ctx = getInitialContext();
CabinHome cabinHome = (CabinHome)ctx.lookup("CabinHome");
CabinPK pk = new CabinPK();
pk.id = 1;
Cabin cab = cabinHome.findByPrimaryKey(pk);
System.out.println(cab.getName());
Ship ship = cab.getShip();
System.out.println(ship.getName());

9.7.1.2. Mapping serializable to VARBINARY

With the Cabin bean, the use of the database mappings is the most straightforward approach to preserving the Cabin bean's relationship to the Ship bean. With other relationships, a relational database foreign key may not already exist. In this case, we need to preserve the relationship in the form of primary keys or handles.

Both primary keys and handles are serializable, so they can be preserved in a relational database as the JDBC type VARBINARY. The data type in the actual database will vary; some databases use BLOB while others use a different data type to indicate variable-length binary data. These data types are typically used for storing arbitrary binary data-like images. If you are using an existing database and need to preserve a nonrelational association between entities, you must update the table structure to include a VARBINARY type column.

Whether we use the primary key or the handle, we need to obtain it from the Ship bean and preserve it in the database. To do this, we use Java serialization to convert the primary key or handle into a byte array, which we can then save in the database as binary data. We can convert this data back into a usable key or handle as needed. One way to do this is to define a couple of simple methods that can take any Serializable object and make it into a byte array and convert it back. These methods are defined as follows:

public byte [] serialize(Object obj) throws IOException {
   ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
   ObjectOutputStream oos = new ObjectOutputStream(byteStream);
   oos.writeObject(obj);
   return byteStream.toByteArray();
}
public Object deserialize(byte [] byteArray) 
   throws IOException,ClassNotFoundException {
   ByteArrayInputStream byteStream = new ByteArrayInputStream(byteArray);
   ObjectInputStream oos = new ObjectInputStream(byteStream);
   return oos.readObject();
}

9.7.1.3. Preserving the primary key

If you can't preserve a foreign database ID, you can preserve the primary key directly. We can use the serialize() method defined earlier in setShip(Shipship) to preserve the primary key of the Ship bean in the database:

public class CabinBean implements javax.ejb.EntityBean {

    public int id;
    public String name;
    public int deckLevel;
    public byte [] shipBinary;
    public int bedCount;
    public Ship ship;

    public javax.ejb.EntityContext ejbContext;
    public transient javax.naming.Context jndiContext;

    public Ship getShip() throws RemoteException {
      try {
        if (ship != null)
            return ship;
        else {
            ShipHome home = (ShipHome)getHome("jndiName_ShipHome");
            ShipPK pk = (ShipPK)deserialize(shipBinary);
            ship = home.findByPrimaryKey(pk);
                return ship;
        }
      } catch(Exception e) {
          throw new RemoteException("Invalid Ship",e);
      }
    }
    public void setShip(Ship ship) throws RemoteException {
        try {
        Object pk = ship.getPrimaryKey();
            shipBinary = serialize(pk);
            this.ship = ship;
        } catch(Exception e) {
             throw new RemoteException("Ship not set",e);
        }
    }

We have replaced the ship_id field with shipBinary. Remember, we are now looking at situations where the relationship between entities cannot be modeled in a relational database. When the CabinBean class is persisted to the database, the shipBinary field is written to the database with the primary key's serialized value. If the bean uses container-managed persistence, the shipBinary field will need to be mapped to the BLOB or binary column of the table. If the bean uses bean-managed persistence, the JDBC API will simply return a binary data type from the appropriate column.

9.7.1.4. Preserving the handle

Using the handle to preserve simple relationships is almost exactly the same as using the primary key. To preserve the handle, we can use the same shipBinary field that we used to save the primary key strategy; we only need to make a couple of simple changes to the setShip() and getShip() methods:

public Ship getShip() throws RemoteException {
    try {
      if (ship != null)
          return ship;
      else {
          Handle handle = (Handle)deserialize(shipBinary);
          ship = (Ship)handle.getEJBObject();
          return ship;
      }
    } catch (Exception e) {
       throw new RemoteException("Invalid Ship",e);
    }
}
public void setShip(Ship ship) throws RemoteException {
    try {
        Object handle = ship.getHandle();
        shipBinary = serialize(handle);
        this.ship = ship;
    } catch (Exception e) {
        throw new RemoteException("Ship not set",e);
    }
}

In many cases, serializing the handle is simpler then using the primary key. This version of the getShip() method is much simpler: you don't need to reconstruct the primary key, and the getInitialContext() method is no longer needed. However, the use of handles over primary keys should be done with care. Handles are simpler, but also more volatile. A change in the container, naming, networking, or security can cause the handle to become invalid.

This strategy is especially useful when the primary key is more complex than our simple ShipPK. If the primary key is made up of several fields, for example, using a binary format provides more benefits.

9.7.1.5. Native Java persistence

Some database products support native persistence of Java objects. These might be object databases or even relational databases that can store Java objects. Entity beans that use native persistence are the simplest to develop because there is no need to convert fields to byte streams so they can be stored in the database. In the following listing, the CabinBean class has been changed so that it uses native Java persistence with the Ship bean's primary key:

public class CabinBean implements javax.ejb.EntityBean {

    public int id;
    public String name;
    public int deckLevel;
    public ShipPK shipPK;
    public int bedCount;
    public Ship ship;

    public javax.ejb.EntityContext ejbContext;
    public transient javax.naming.Context jndiContext;

    public Ship getShip() throws RemoteException {
      try {
        if (ship != null)
            return ship;
        else {
            ShipHome home = (ShipHome)getHome("jndiName_ShipHome");
            ship = home.findByPrimaryKey(shipPK);
            return ship;
        }
      } catch(Exception e) {
         throw new RemoteException("Invalid Ship",e);
      }
    }
    public void setShip(Ship ship) throws RemoteException {
        try {
            shipPK = (ShipPK)ship.getPrimaryKey();
            this.ship = ship;
        } catch(Exception e) {
             throw new RemoteException("Ship not set",e);
        }
... 
}

9.7.2. Complex Entity Relationships

Situationsin which an entity has a relationship to several other entities of a particular type are as common as simple relationships. An example of a complex relationship is a ship. In the real world, a ship may contain thousands of cabins. To develop a Ship bean that models the relationship between a real-world ship and its cabins, we can use the same strategies that we discussed earlier. The most important difference is that we're now discussing one-to-many associations rather than one-to-one. Before we get started, let's add a couple of new methods to the Ship bean's remote interface:

package com.titan.ship;

import com.titan.cabin.Cabin;
import javax.ejb.EJBObject;
import java.rmi.RemoteException;

public interface Ship extends javax.ejb.EJBObject {
    public Cabin [] getCabins() throws RemoteException;
    public void addCabin(Cabin cabin) throws RemoteException;
    public String getName() throws RemoteException;
    public void setName(String name) throws RemoteException;
    public void setCapacity(int cap) throws RemoteException;
    public int getCapacity() throws RemoteException;
    public double getTonnage() throws RemoteException;
    public void setTonnage(double tonnage) throws RemoteException;
}

The Ship bean's remote interface indicates that it is associated with many Cabin beans, which makes perfectly good sense as a business concept. The application developer using the Ship remote interface is not concerned with how the Cabin beans are stored in the Ship bean. Application developers are only concerned with using the two new methods, which allow them to obtain a list of cabins assigned to a specific ship and to add new cabins to the ship when appropriate.

9.7.2.1. One-to-many database mapping

The database structure provides a good relational model for mapping multiple cabins to a single ship. In the relational database, the CABIN table contains a foreign key called ship_id, which we used previously to manage the cabin's relationship to the ship.

As a foreign key, the ship_id provides us with a simple relational mapping from a ship to many cabins using an SQL join:

"select ID from CABIN  where SHIP_ID = "+ship_id

Nothing complicated about that. Unfortunately, this relationship cannot be done conveniently with container-managed persistence, because we would need to map a collection to the join, which is not as straightforward as mapping a single entity field to a database field. More advanced object-to-relational mapping software will simplify this task, but these advanced tools are not always available and have their own limitations. If your EJB server doesn't support object-to-relational mapping, you will need an alternative solution. One alternative is to use bean-managed persistence to leverage the database mapping. If, however, your EJB server supports JavaBlend™ or some other object-to-relational technology, you may still be able to use container-managed persistence with a one-to-many database mapping.

If you examine the SHIP table definition, there is no column for storing a list of cabins, so we will not store this relationship directly in the SHIP table. Instead, we use a Vector called cabins to store this relationship temporarily. Every time the bean's ejbLoad() method is called, it populates the vector with cabin IDs, as it does all the other fields. Here are the getCabins() and ejbLoad() methods of the ShipBean class, with the changes for managing the vector of cabin IDs in bold:

public class ShipBean implements ShipBusiness, javax.ejb.EntityBean {
    public int id;
    public String name;
    public int capacity;
    public double tonnage;
    public Vector cabins;

    private EntityContext ejbContext;
    private transient Context jndiContext;

    public Cabin [] getCabins() throws RemoteException {
        try {
            Cabin [] cabinArray = new Cabin[cabins.size()];
            CabinHome home = (CabinHome)getHome("jndiName_CabinHome");
            for (int i = 0; i < cabinArray.length; i++) {
                CabinPK pk = (CabinPK)cabins.elementAt(i);
                cabinArray[i] = home.findByPrimaryKey(pk);
            }
            return cabinArray;
        } catch(Exception e) {
            throw new RemoteException("Cannot get cabins",e);
        }
    }
    public void ejbLoad() throws RemoteException {
        try {
            ShipPK pk = (ShipPK) ejbContext.getPrimaryKey();
            loadUsingId(pk.id);
        } catch(FinderException fe) {
            throw new RemoteException();
        }
    }
    private void loadUsingId(int id)
            throws RemoteException, FinderException {
        Connection con = null;
        PreparedStatement ps = null;
        ResultSet result = null;
        try {
          con = getConnection();
          ps = con.prepareStatement(
            "select name, capacity, tonnage from Ship where id = ?");
          ps.setInt(1,id);
          result = ps.executeQuery();
          if (result.next()) {
            this.id = id;
            this.name = result.getString("name");
            this.capacity = result.getInt("capacity");
            this.tonnage = result.getDouble("tonnage");
          } else {
            throw new FinderException("Cannot find Ship with id = "+id);
          }
          result.close();
          ps = con.prepareStatement(
              "select ID from CABIN where SHIP_ID = ?");
          ps.setInt(1,id);
          result = ps.getResultSet();
          cabins = new Vector();
          while(result.next()) {
              CabinPK pk = new CabinPK(result.getInt(1));
              cabins.addElement(pk);
          }
        }
        catch (SQLException se) {
          throw new RemoteException (se.getMessage());
        }
        finally {
          try {
            if (result != null) result.close();
            if (ps != null) ps.close();
            if (con!= null) con.close();
          } catch(SQLException se) {
            se.printStackTrace();
          }
        }
    }
...
}

9.7.2.2. Mapping serializable to VARBINARY

Using byte arrays and Java serialization works with complex relationships just as well as it did with simple relationships. In this case, however, we are serializing some kind of collection instead of a single reference. If, for example, the Ship bean were container-managed, the ejbLoad() and ejbStore() methods could be used to convert our cabins vector between its representation as a Vector and a byte array. The following code illustrates how this could work with container-managed persistence; it applies equally well to Cabin primary keys or Cabin bean Handle objects:

public class ShipBean implements ShipBusiness, javax.ejb.EntityBean {
    public int id;
    public String name;
    public int capacity;
    public double tonnage;
    public Vector cabins;
    public byte [] cabinBinary;

    public void ejbLoad() throws RemoteException {
        try {
          if (cabinBinary != null)
            cabins = (java.util.Vector)deserialize(cabinBinary);
        } catch(Exception e) {
            throw new RemoteException("Invalid Cabin aggregation ",e);
        }
    }
    public void ejbStore() throws RemoteException {
        try {
          if (cabins != null)
               cabinBinary = serialize(cabins);
        } catch(Exception e) {
            throw new RemoteException("Invalid Cabin aggregation",e);
        }
    }
    public byte [] serialize(Object obj) throws IOException {
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(byteStream);
        oos.writeObject(obj);
        return byteStream.toByteArray();
    }
    public Object deserialize(byte [] byteArray)
           throws IOException,ClassNotFoundException {
        ByteArrayInputStream byteStream = 
            new ByteArrayInputStream(byteArray);
        ObjectInputStream oos = new ObjectInputStream(byteStream);
        return oos.readObject();
    }
...
}

9.7.2.3. Native Java persistence

As with the simple relationships, preserving complex relationships is the easiest with native Java persistence. The ShipBean definition is much simpler because the cabins vector can be stored directly, without converting it to a binary format. Using this strategy, we can opt to preserve the CabinPK types or Cabin bean handles for the aggregated Cabin beans.



Library Navigation Links

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