Primary Keys

Unlike a Session EJB, an Entity EJB has a clearly defined identity that allows it to be found and operated on. This identity is cirtical to the use of Entities and has a significant impact on the design of the EJBs. This article provides guidelines for defining an effective primary key and describes the mechanisms for mapping it to CMP.

Suitable Classes

The EJB Specification places some constraints on which classes can be used as primary keys for Entity EJBs. They must:

  1. Be a valid RMI-IIOP Value Type; this essentially means that they must implement Serializable
  2. Override hashCode() and equals(Object) in a manner that simplifies management by the Container. This means that equals must be written in a way that it returns true for two primary keys that have the same identity and false for all other pairs. It also means that hashCode should return a well distributed value.

The requirement to override equals and hashCode means that Java primitives are not suitable for use as primary keys. Instead the equivilent Object (such as java.lang.Integer or java.lang.Long) should be used.

Typesafe Key Classes

Although simple classes such as Integer, Long or String can be used as primary keys, they provide little compile time checking on the compatibility between key and the Entity it is identifying. For example, if both a Person and an Order have an Integer primary key class then there can be no compile-time checking that the value being used to locate an Order is actually the identity of an Order rather than a Person.

One way to provide type safety is to use a unique primary key class for each Entity. So rather than using Integer for both Person and Order, we would define a separate class for each. One common convention is to use the name of the Entity followed by "PK"; for example, PersonPK and OrderPK.

It is very common for applications to use similar types for all keys, which allows much of the implementation to be provided by a common base class. A simple implementation of this could be:

public abstract class AbstractPrimaryKey implements Serializable {
    /**
     * The identity of the object. 
     * Must be public for compatibility with CMP.
     */
    public int id;
    
    public int hashCode() {
        return id;
    }
    
    public boolean equals(Object other) {
        if (getClass().isInstance(other)) {
            return this.id == ((AbstractPrimaryKey)other).id;
        } else {
            return false;
        }
    }
    
    public String toString() {
        return Integer.toString(id);
    }
}
 
public class PersonPK extends AbstractPrimaryKey {
}
 
public class OrderPK extends AbstractPrimaryKey {
}

Using a Single CMP Field as Primary Key

The simplest way to map the primary key to a persistent value is to use a single cmp-field as the primary key. Which field to use is specified in ejb-jar.xml using a primkey-field element. The type of the field must match the type of the primary key as declared in the prim-key-class element.

The following XML snippet illustrates using the "orderId" cmp-field as the primary key:

    <entity>
      <ejb-name>Order</ejb-name>
      ...
      <prim-key-class>java.lang.Integer</prim-key-class>
      <cmp-field>
        <field-name>orderId</field-name>
      </cmp-field>
      ...
      <primkey-field>orderId</primkey-field>
      ...
    </entity>

The accessors for orderId would be defined as:

    public abstract Integer getOrderId();
    public abstract void setOrderId(Integer orderId);

The application must set the value of the cmp-field during ejbCreate so that CMP is able to use the value of the field to define the primary key. An example ejbCreate method would be:

    public Integer ejbCreate(Integer orderId) throws CreateException {
        setOrderId(orderId);
        ...
    }

 
    public void ejbPostCreate(Integer orderId) {
        ...
    }

This approach can be used in conjunction with a typesafe primary key. However, unless a mapping is defined for the primary key class, then the instance will be serialized and stored as a byte array in the database; this is not a good choice for a database key and large objects may not even be supported. One solution would be to define a DVC mapping for the primary key class, but a simpler alternative is to define a multi-field mapping with a single value as illustrated in the following section.

Using Multiple CMP Fields as a Primary Key

It is possible to use a complex object as a primary key and store that object using a single cmp-field. However, when that object is stored a database it will be serialized into a byte array which is not a good choice from a database management perspective; it may not even be supported by the database.

CMP provides a mechanism for mapping objects to multiple cmp-fields, allowing them to be stored directly in the database. However, this mapping imposes some additional restrictions on the Class used for the primary key:

  • It must have a public, no-argument constructor
  • All fields in the class must be public
  • The names of the fields must be a sub-set of the cmp-field names; in other words, there must be a cmp-field with the same name for every field in the primary key class

An example of such a key could be that for an OrderStatus Entity whose identity is defined by an orderId and a statusDate. The class for the key could be defined as:

public class OrderStatusPK implements Serializable {
    public int orderId;
    public Date statusDate;
    
    public OrderStatusPK() {
    }
	
    // overrides for equals and hashCode omitted for clarity
}

The CMP Entity must declare cmp-fields with the same name as each of the fields in the primary key class:

    <entity>
      <ejb-name>OrderStatus</ejb-name>
      ...
      <prim-key-class>OrderStatusPK</prim-key-class>
      <cmp-field>
        <field-name>orderId</field-name>
      </cmp-field>
      <cmp-field>
        <field-name>statusDate</field-name>
      </cmp-field>
      ...
      <!-- no primkey-field elements are needed -->
    </entity>

It must also declare appropriate accessors:

    public abstract int getOrderId();
    public abstract void setOrderId(int orderId);
    public abstract Date getStatusDate();
    public abstract void setStatusDate(Date statusDate);

A primitive must be used for the orderId accessor to match the field in OrderStatusPK. This does not conflict with the need for the primary key to override equals() and hashCode() as that is done by the primary key class itself and not by the cmp-fields.

When creating the object, all cmp-fields must be assigned in ejbCreate(). The create method can itself use business logic to define the values, for example:

    public OrderStatusPK ejbCreate(OrderLocal order) throws CreateException {
        Integer orderPK = (Integer) order.getPrimaryKey();
        setOrderId(orderPK.intValue());
        setStatusDate(new Date());
    }

 
    public void ejbPostCreate(OrderLocal order) {
    }

The accessors for the primary key fields should not be exposed outside the EJB. The specification suggests this directly for the set methods as any attempt to call them from outside ejbCreate will result in an IllegalStateException .There is also no need to use the get methods to read key fields as they can be obtained from the primary key object returned from getPrimaryKey() on the component interface; this is often more efficient as the Container does not need to instantiate the EJB in order to call the business method. For example:

    // bad example, requires EJB to be loaded
    OrderStatusLocal status = ...; // obtain a reference to order status
    int orderId = status.getOrderId();

 
    // good example, does not require EJB to be loaded
    OrderStatusLocal status = ...; // obtain a reference to order status
    OrderStatusPK pk  = (OrderStatusPK) status.getPrimaryKey();
    int orderId = pk.orderId;

Use with Typesafe Primary Keys

This mechanism can be used to map typesafe primary key classes to individual columns in the database and avoid the need to serialize the object into a byte array or to define DVCs for every primary key class.

The typesafe OrderPK defined above can be mapped into the Order EJB using the following XML snippet:

    <entity>
      <ejb-name>Order</ejb-name>
      ...
      <prim-key-class>OrderPK</prim-key-class>
      <cmp-field>
        <!-- field name must match id field in OrderPK class -->
        <field-name>id</field-name>
      </cmp-field>
      ...
      <!-- no primkey-field element required -->
    </entity>

and the accessors:

    public abstract int getId();
    public abstract void setId(int id);

Again, these should not exposed on the component interface as they cannot be set and the value is better obtained from the primary key itself.

Container Supplied Keys (Unkown Primary Key)

In some circumstances, a CMP EJB may not actually know the implementation of its primary key. One very common scenario is where the key has no business meaning and is supplied by a generator, an IDENTITY column in a database, or a database sequence. In these cases, the EJB does not specify the actual class of the primary key but instead defines it as being a java.lang.Object; the actual class used is determined by how the bean is deployed.

The following XML snippet illustrates how this mode is enabled for the Order EJB:

    <entity>
      <ejb-name>Order</ejb-name>
      ...
      <prim-key-class>java.lang.Object</prim-key-class>
      ...
      <!-- no primkey-field element required -->
    </entity>

This mode places restrictions on the clients of this EJB in that they do not know the actual class that will be used by the Container; they only know that getPrimaryKey() will return an Object and that they must save that object if they wish to use findByPrimaryKey(Object) to obtain a reference to the EJB.

In reality, such EJBs are often deployed with a known configuration and it is possible for the client to cast the Object returned by getPrimaryKey() to a specific class. For example, the client may trust that the key will have been generated by a certain mechanism and that it can be cast to an Integer. However, such assumptions are outside the specification and may not be portable between deployments or containers.

For details on how to configure JBoss to use a suitable key generator, please see Key Generators.