Entity beans can form relationships with other entity beans. In Figure 6-1, at the beginning of this chapter, the Customer EJB has a one-to-one relationship with the Address EJB. The Address EJB is a fine-grained business object that should always be accessed in the context of another entity bean, which means it should have only local interfaces and not remote interfaces. An entity bean can have relationships with many different entity beans at the same time. For example, we could easily add relationship fields for Phone, CreditCard, and other entity beans to the Customer EJB. At this point, we’re choosing to keep the Customer EJB simple.
Using Figure 6-1 as a guide, we define the Address EJB as follows:
package com.titan.address; import javax.ejb.EntityContext; public abstract class AddressBean implements javax.ejb.EntityBean { public Integer ejbCreateAddress(String street, String city, String state, String zip) { setStreet(street); setCity(city); setState(state); setZip(zip); return null; } public void ejbPostCreateAddress(String street, String city, String state, String zip) { } // persistence fields public abstract Integer getId( ); public abstract void setId(Integer id); public abstract String getStreet( ); public abstract void setStreet(String street); public abstract String getCity( ); public abstract void setCity(String city); public abstract String getState( ); public abstract void setState(String state); public abstract String getZip( ); public abstract void setZip(String zip); // standard callback methods public void setEntityContext(EntityContext ec){} public void unsetEntityContext( ){} public void ejbLoad( ){} public void ejbStore( ){} public void ejbActivate( ){} public void ejbPassivate( ){} public void ejbRemove( ){} }
The AddressBean
class defines several persistence
fields (street, city, state, and zip) and an
ejbCreateAddress( )
method, which is called when a new
Address EJB is created. The persistence fields are represented by
abstract accessor methods. These abstract methods are matched with
XML deployment descriptor elements. At deployment time, the container
maps the Customer and Address EJB’s persistence
fields to the database. This means there must be a table in our
relational database that contains columns matching the persistence
fields in the Address EJB. In this example, we will use a separate
ADDRESS
table for storing address information:
CREATE TABLE ADDRESS ( ID INT PRIMARY KEY NOT NULL, STREET CHAR(40), CITY CHAR(20), STATE CHAR(2), ZIP CHAR(10) )
The ID
column in this table is an auto-increment
field, created automatically by the database or container system. It
is the primary key of the Address EJB. Once the bean is created, its
primary key must never again be modified. When primary keys are
autogenerated values, such as the ID
column in the
ADDRESS
table, the EJB container obtains the
primary key value from the database.
The other columns in this table correspond to the Address bean’s persistence fields. Entity beans do not have to define all the columns in the corresponding table as persistence fields. In fact, there’s no requirement that an entity bean correspond to a single table; it may be persisted to columns in several different tables. The bottom line is that the container allows the abstract persistence schema of an entity bean to be mapped to a database in a variety of ways, allowing a clean separation between the persistence classes and the database.
In addition to the bean class, we must define the local interface for the Address EJB. This interface allows the EJB to be accessed by other entity beans (namely, the Customer EJB) within the same address space or process:
// Address EJB's local interface public interface AddressLocal extends javax.ejb.EJBLocalObject { public String getStreet( ); public void setStreet(String street); public String getCity( ); public void setCity(String city); public String getState( ); public void setState(String state); public String getZip( ); public void setZip(String zip); } // Address EJB's local home interface public interface AddressHomeLocal extends javax.ejb.EJBLocalHome { public AddressLocal createAddress(String street,String city, String state,String zip) throws javax.ejb.CreateException; public AddressLocal findByPrimaryKey(Integer primaryKey) throws javax.ejb.FinderException; }
You may have noticed that the ejbCreate( )
method
of the AddressBean
class and the
findByPrimaryKey( )
method of the home interface
both define the primary key type as
java.lang.Integer
. The primary key is generated
automatically. Most EJB vendors allow entity beans’
primary keys to be mapped
to autogenerated fields. If your vendor does not support
autogenerated primary keys, you must set the primary
key’s value in the ejbCreate( )
method.
The relationship field for the Address EJB is defined in the
CustomerBean
class using an abstract accessor
method, the same way that persistence fields are declared. In the
following code, the CustomerBean
has been modified
to include the Address EJB as a relationship field:
import javax.ejb.EntityContext; import javax.ejb.CreateException; public abstract class CustomerBean implements javax.ejb.EntityBean { ... // persistence relationship public abstract AddressLocal getHomeAddress( ); public abstract void setHomeAddress(AddressLocal address); // persistence fields public abstract boolean getHasGoodCredit( ); public abstract void setHasGoodCredit(boolean creditRating); ...
The getHomeAddress( )
and setHomeAddress( )
accessor methods are self-explanatory;
they allow the bean to access and modify its
homeAddress
relationship. The name of the accessor
method is determined by the name of the relationship field, as
declared in the deployment descriptor. In this case, we have named
the customer’s address
homeAddress
, so the corresponding accessor method
names will be getHomeAddress( )
and
setHomeAddress( )
.
To accommodate the relationship between the Customer EJB and the home
address, a foreign key, ADDRESS_ID
, is needed in
the CUSTOMER
table. The foreign key points to the
ADDRESS
record. In practice, it would be more
common to give the ADDRESS
table a foreign key to
the CUSTOMER
table. However, the schema used here
demonstrates alternative database mappings:
CREATE TABLE CUSTOMER ( ID INT PRIMARY KEY NOT NULL, LAST_NAME CHAR(20), FIRST_NAME CHAR(20), ADDRESS_ID INT )
When a new Address EJB is created and set as the Customer
EJB’s homeAddress
relationship,
the Address EJB’s primary key is placed in the
ADDRESS_ID
column of the
CUSTOMER
table:
// get local reference AddressLocal address = ... // establish the relationship customer.setHomeAddress(address);
To give the Customer a home address, we need to deliver the address
information to the Customer. This appears to be a simple matter of
declaring matching setHomeAddress( )
/getHomeAddress( )
accessors in the
remote interface, but it’s not! While
it’s valid to make persistence fields available to
remote clients, persistence relationships are more complicated. The
remote interface of a bean is not allowed to expose its relationship
fields. In the case of the homeAddress
field, we
have declared the type to be AddressLocal
, which
is a local interface, so the setHomeAddress( )
/getHomeAddress( )
accessors cannot be
declared in the remote interface of the Customer EJB. The reason for
this restriction on remote interfaces is fairly simple: the
EJBLocalObject
, which implements the local
interface, is optimized for use within the same address space or
process as the bean instance and is not capable of being used across
the network. In other words, references that implement the local
interface of a bean cannot be passed across the network, so a local
interface cannot be declared as a return type of a parameter of a
remote interface.
Local interfaces (interfaces that extend
javax.ejb.EJBLocalObject
), on the other hand, can expose any
kind of relationship field. With local interfaces, the caller and the
enterprise bean being called are located in the same address space,
so they can pass around local references without a problem. For
example, if we had defined a local interface for the Customer EJB, it
could include a method that allows local clients to access its
Address relationship directly:
public interface CustomerLocal extends javax.ejb.EJBLocalObject { public AddressLocal getHomeAddress( ); public void setHomeAddress(AddressLocal address); }
When it comes to the Address EJB, it’s better to define a local interface only because it’s such a fine-grained bean. To get around remote-interface restrictions, the business methods in the bean class exchange address data instead of Address references. For example, we can declare a method in the Customer bean that allows the client to send address information:
public abstract class CustomerBean implements javax.ejb.EntityBean { public Integer ejbCreate(Integer id) { setId(id); return null; } public void ejbPostCreate(Integer id) { } // business method public void setAddress(String street,String city,String state,String zip) { try { AddressLocal addr = this.getHomeAddress( ); if(addr == null) { // Customer doesn't have an address yet. Create a new one. InitialContext cntx = new InitialContext( ); AddressHomeLocal addrHome = (AddressHomeLocal) cntx.lookup("java:comp/env/ejb/AddressHomeLocal"); addr = addrHome.createAddress(street,city,state,zip); this.setHomeAddress(addr); } else { // Customer already has an address. Change its fields. addr.setStreet(street); addr.setCity(city); addr.setState(state); addr.setZip(zip); } } catch(Exception e) { throw new EJBException(e); } } ...
The setAddress( )
business method in the
CustomerBean
class is also declared in the remote
interface of the Customer EJB, so it can be called by remote clients:
public interface CustomerRemote extends javax.ejb.EJBObject { public void setAddress(String street,String city,String state,String zip) throws RemoteException; public Name getName( ) throws RemoteException; public void setName(Name name) throws RemoteException; public boolean getHasGoodCredit( ) throws RemoteException; public void setHasGoodCredit(boolean creditRating) throws RemoteException; }
When the CustomerRemote.setAddress( )
method is
invoked on the CustomerBean
, the
method’s arguments are used to create a new Address
EJB and set it as the homeAddress
relationship
field, if one doesn’t already exist. If the Customer
EJB already has a homeAddress
relationship, that
Address EJB is modified to reflect the new address information.
When creating a new Address EJB, the home object is obtained from the
JNDI ENC (environment naming context) and its createAddress( )
method is called. This results in the
creation of a new Address EJB and the insertion of a corresponding
ADDRESS
record into the database. After the
Address EJB is created, it’s used in the
setHomeAddress( )
method. The
CustomerBean
class must explicitly call the
setHomeAddress( )
method, or the new address will
not be assigned to the customer. Creating an Address EJB without
assigning it to the customer results in a
disconnected Address EJB. More precisely, it
results in an ADDRESS
record in the database that
is not referenced by any CUSTOMER
records.
Disconnected entity beans are fairly normal and even desirable in
many cases. In this case, however, we want the new Address EJB to be
assigned to the homeAddress
relationship field of
the Customer EJB.
The viability of disconnected entities depends, in part, on the referential integrity of the database. For example, if the referential integrity allows only non-null values for the foreign key column, creating a disconnected entity may result in a database error.
When the setHomeAddress( )
method is invoked, the
container links the ADDRESS
record to the
CUSTOMER
record automatically. In this case, it
places the ADDRESS
primary key in the
CUSTOMER
record’s
ADDRESS_ID
field and creates a reference from the
CUSTOMER
record to the ADDRESS
record.
If the Customer EJB already has a homeAddress
, we
want to change its values instead of creating a new Address EJB. We
don’t need to use setHomeAddress( )
if we are simply updating the values of an existing
Address EJB, because the Address EJB we modified already has a
relationship with the entity bean.
We also want to provide clients with a business method for obtaining a Customer EJB’s home address information. Since we are prohibited from sending an instance of the Address EJB directly to the client (because it’s a local interface), we must package the address data in some other form and send that to the client. There are two solutions to this problem: acquire the remote interface of the Address EJB and return that; or return the data as a dependent value object.
We can obtain the remote interface for the Address EJB only if one is
defined. The Address EJB is too fine-grained to justify creating a
remote interface, but in many other circumstances, a bean may indeed
want to have a remote interface. If, for example, the Customer EJB
referenced a SalesPerson EJB, the CustomerBean
could convert the local reference into a remote reference. This would
be done by accessing the local EJB object, getting its primary key
(EJBLocalObject.getPrimaryKey( )
), obtaining the
SalesPerson EJB’s remote home from the JNDI ENC, and
then using the primary key and remote home reference to find a remote
interface reference:
public SalesRemote getSalesRep( ){ SalesLocal local = getSalesPerson( ); Integer primKey = local.getPrimaryKey( ); InitialContext cntx = new InitialContext( ); Object ref = cntx.lookup("java:comp/env/ejb/SalesHomeRemote"); SalesHomeRemote home = (SalesHomeRemote) PortableRemoteObject.narrow(ref, SalesHomeRemote.class); SalesRemote remote = home.findByPrimaryKey( primKey ); return remote; }
The other option is to use a dependent value to pass the Address
EJB’s data between remote clients and the Customer
EJB. This is the approach recommended for fine-grained beans like the
Address EJB—we don’t want to expose these
beans directly to remote clients.
The
following code shows how the AddressDO
dependent
value class is used in conjunction with the local component
interfaces of the Address EJB (the
DO
in AddressDO
is a
convention used in this book—it’s a qualifier
that stands for “dependent
object”):
public abstract class CustomerBean implements javax.ejb.EntityBean { public Integer ejbCreate(Integer id) { setId(id); return null; } public void ejbPostCreate(Integer id) { } // business method public AddressDO getAddress( ) { AddressLocal addrLocal = getHomeAddress( ); if(addrLocal == null) return null; String street = addrLocal.getStreet( ); String city = addrLocal.getCity( ); String state = addrLocal.getState( ); String zip = addrLocal.getZip( ); AddressDO addrValue = new AddressDO(street,city,state,zip); return addrValue; } public void setAddress(AddressDO addrValue) throws EJBException { String street = addrValue.getStreet( ); String city = addrValue.getCity( ); String state = addrValue.getState( ); String zip = addrValue.getZip( ); AddressLocal addr = getHomeAddress( ); try { if(addr == null) { // Customer doesn't have an address yet. Create a new one. InitialContext cntx = new InitialContext( ); AddressHomeLocal addrHome = (AddressHomeLocal) cntx.lookup("java:comp/env/ejb/AddressHomeLocal"); addr = addrHome.createAddress(street, city, state, zip); this.setHomeAddress(addr); } else { // Customer already has an address. Change its fields. addr.setStreet(street); addr.setCity(city); addr.setState(state); addr.setZip(zip); } } catch(NamingException ne) { throw new EJBException(ne); } catch(CreateException ce) { throw new EJBException(ce); } } ...
Here is the definition for an AddressDO
dependent
value class, which is used by the enterprise bean to send address
information to the client:
public class AddressDO implements java.io.Serializable { private String street; private String city; private String state; private String zip; public AddressDO(String street, String city, String state, String zip ) { this.street = street; this.city = city; this.state = state; this.zip = zip; } public String getStreet( ) { return street; } public String getCity( ) { return city; } public String getState( ) { return state; } public String getZip( ) { return zip; } }
The AddressDO
dependent value is immutable: it
cannot be altered once it is created. As stated earlier, immutability
helps to reinforce the fact that the dependent value class is a copy,
not a remote reference. To use the AddressDO
, we
add accessor methods to the CustomerRemote
interface:
public interface CustomerRemote extends javax.ejb.EJBObject { public void setAddress(AddressDO address) throws RemoteException; public AddressDO getAddress( ) throws RemoteException; public void setAddress(String street,String city,String state,String zip) throws RemoteException; public Name getName( ) throws RemoteException; public void setName(Name name) throws RemoteException; public boolean getHasGoodCredit( ) throws RemoteException; public void setHasGoodCredit(boolean creditRating) throws RemoteException; }
You can now use a client application to test the Customer EJB’s relationship with the Address EJB. Here is the client code that creates a new Customer, gives it an address, and then changes the address:
import javax.naming.InitialContext; import javax.rmi.PortableRemoteObject; import javax.naming.Context; import javax.naming.NamingException; import java.util.Properties; public class Client { public static void main(String [] args) throws Exception { // obtain CustomerHomeRemote Context jndiContext = getInitialContext( ); Object obj=jndiContext.lookup("CustomerHomeRemote"); CustomerHomeRemote home = (CustomerHomeRemote) javax.rmi.PortableRemoteObject.narrow(obj, CustomerHomeRemote.class); // create a Customer Integer primaryKey = new Integer(1); CustomerRemote customer = home.create(primaryKey); // create an address AddressDO address = new AddressDO("1010 Colorado", "Austin", "TX", "78701"); // set address customer.setAddress(address); address = customer.getAddress( ); System.out.print(primaryKey+" = "); System.out.println(address.getStreet( )); System.out.println(address.getCity( )+","+ address.getState( )+" "+ address.getZip( )); // create a new address address = new AddressDO("1600 Pennsylvania Avenue NW", "DC", "WA", "20500"); // change Customer's address customer.setAddress(address); address = customer.getAddress( ); System.out.print(primaryKey+" = "); System.out.println(address.getStreet( )); System.out.println(address.getCity( )+","+ address.getState( )+" "+ address.getZip( )); // remove Customer customer.remove( ); } public static Context getInitialContext( ) throws javax.naming.NamingException { Properties p = new Properties( ); // ... Specify the JNDI properties specific to the vendor. //return new javax.naming.InitialContext(p); return null; } }
The following listing shows the EJB 2.1 deployment descriptor for the Customer and Address EJBs. You don’t need to worry about the details of the deployment descriptor yet; it will be covered in depth in Chapter 7.
<?xml version="1.0" encoding="UTF-8" ?> <ejb-jar xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/ejb-jar_2_1.xsd" version="2.1"> <enterprise-beans> <entity> <ejb-name>CustomerEJB</ejb-name> <home>com.titan.customer.CustomerHomeRemote</home> <remote>com.titan.customer.CustomerRemote</remote> <ejb-class>com.titan.customer.CustomerBean</ejb-class> <persistence-type>Container</persistence-type> <prim-key-class>java.lang.Integer</prim-key-class> <reentrant>False</reentrant> <cmp-version>2.x</cmp-version> <abstract-schema-name>Customer</abstract-schema-name> <cmp-field><field-name>id</field-name></cmp-field> <cmp-field><field-name>lastName</field-name></cmp-field> <cmp-field><field-name>firstName</field-name></cmp-field> <primkey-field>id</primkey-field> <security-identity><use-caller-identity/></security-identity> </entity> <entity> <ejb-name>AddressEJB</ejb-name> <local-home>com.titan.address.AddressHomeLocal</local-home> <local>com.titan.address.AddressLocal</local> <ejb-class>com.titan.address.AddressBean</ejb-class> <persistence-type>Container</persistence-type> <prim-key-class>java.lang.Integer</prim-key-class> <reentrant>False</reentrant> <cmp-version>2.x</cmp-version> <abstract-schema-name>Address</abstract-schema-name> <cmp-field><field-name>id</field-name></cmp-field> <cmp-field><field-name>street</field-name></cmp-field> <cmp-field><field-name>city</field-name></cmp-field> <cmp-field><field-name>state</field-name></cmp-field> <cmp-field><field-name>zip</field-name></cmp-field> <primkey-field>id</primkey-field> <security-identity><use-caller-identity/></security-identity> </entity> </enterprise-beans> <relationships> <ejb-relation> <ejb-relation-name>Customer-Address</ejb-relation-name> <ejb-relationship-role> <ejb-relationship-role-name> Customer-has-an-Address </ejb-relationship-role-name> <multiplicity>One</multiplicity> <relationship-role-source> <ejb-name>CustomerEJB</ejb-name> </relationship-role-source> <cmr-field> <cmr-field-name>homeAddress</cmr-field-name> </cmr-field> </ejb-relationship-role> <ejb-relationship-role> <ejb-relationship-role-name> Address-belongs-to-Customer </ejb-relationship-role-name> <multiplicity>One</multiplicity> <relationship-role-source> <ejb-name>AddressEJB</ejb-name> </relationship-role-source> </ejb-relationship-role> </ejb-relation> </relationships> <assembly-descriptor> <security-role> <role-name>Employees</role-name> </security-role> <method-permission> <role-name>Employees</role-name> <method> <ejb-name>CustomerEJB</ejb-name> <method-name>*</method-name> </method> <method> <ejb-name>AddressEJB</ejb-name> <method-name>*</method-name> </method> </method-permission> <container-transaction> <method> <ejb-name>AddressEJB</ejb-name> <method-name>*</method-name> </method> <method> <ejb-name>CustomerEJB</ejb-name> <method-name>*</method-name> </method> <trans-attribute>Required</trans-attribute> </container-transaction> </assembly-descriptor> </ejb-jar>
The EJB 2.0 deployment descriptor looks the same, except it uses a document declaration that points to a DTD instead referencing an XML Schema. Here’s the difference:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 2.0//EN" "http://java.sun.com/dtd/ejb-jar_2_0.dtd"> <ejb-jar> ... </ejb-jar>
Exercise 6.3 in the Workbook shows how to deploy this example on the JBoss server.