Entity beans can form relationships with other entity beans. In Figure 6-1, at the beginning of this chapter, the Customer EJB is shown to have 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 easy add relationship fields for Phone, CreditCard, and other entity beans to the Customer EJB. At this point, however, we’re choosing to keep the Customer EJB simple.
Following Figure 6-1
(earlier in the chapter) 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 an ejbCreateAddress() method, which is called when a new Address EJB is created, as well as several persistence fields (street, city, state, and zip). The persistence fields are represented by the abstract accessor methods, which is the idiom required for persistence fields in all entity bean classes. These abstract accessor methods are matched with their own set of XML deployment descriptor elements, which define the abstract persistence schema of the Address EJB. At deployment time, the container’s deployment tool will map the Customer EJB’s and Address EJB’s persistence fields to the database. This means that there must be a table in our relational database that contains columns that match the persistence fields in the Address EJB. In this example, we will use a separate ADDRESS table for storing address information, but the data could just as easily have been declared in the other table:
CREATE TABLE ADDRESS ( ID INT PRIMARY KEY NOT NULL, STREET CHAR(40), CITY CHAR(20), STATE CHAR(2), ZIP CHAR(10) )
Entity beans do not have to define all the columns from corresponding
tables as persistence fields. In fact, an entity bean may not even
have a single corresponding table; it may be persisted to several
tables. The bottom line is that the container’s deployment tool
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. The
ID
column 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 the 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 will obtain the primary key value from the
database.
In addition to the bean class, we will define the local interface for the Address EJB, which allows it 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
. As I mentioned earlier, the
primary key is autogenerated. Most EJB 2.0 vendors will allow entity
beans’ primary keys to be mapped to autogenerated fields. If
your vendor does not support autogenerated primary keys, you will
need to set the primary key value in the
ejbCreate()
method. This is usually true of
single-value primary keys, but not necessarily of compound primary
keys.
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 relationshippublic 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. These accessor methods
represent a relationship field, which is a
virtual field that references another entity bean. The name of the
accessor method is determined by the name of the relationship field,
as declared in the XML 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
, will be added
to the CUSTOMER
table. The foreign key will point
to the ADDRESS
record. In practice, this schema is
actually the reverse of what is usually done, where the
ADDRESS
table contains a foreign key to the
CUSTOMER
table. However, the schema used here is
useful in demonstrating alternative database mappings and is utilized
again in Chapter 7:
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 will be placed in the ADDRESS_ID
column of the CUSTOMER
table, creating a
relationship in the database:
// 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 directly available to
remote clients, persistence relationships are more complicated than
that.
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.
We take advantage of the EJBLocalObject
optimization for better performance, but that same advantage limits
location transparency; we must use it only within the same address
space.
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 that allows the client to send address information to create a home address for the Customer:
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); 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()
business
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. In fact, simply creating an Address
EJB without assigning it to the customer with the
setHomeAddress()
method will result in a
disconnected Address EJB. More precisely, it
will result 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
was defined. Entity beans can have a set of either local interfaces
or remote interfaces, or both. In the situation with which
we’re dealing 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—in general, 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 methodpublic 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 follows the
conventions laid out in this book. It’s immutable, which means
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.
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, then changes the address using the method defined earlier:
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 CustomerHome 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 addressAddressDO address = new AddressDO("1010 Colorado",
"Austin", "TX", "78701");
// set addresscustomer.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 addressaddress = new AddressDO("1600 Pennsylvania Avenue NW",
"DC", "WA", "20500");
// change Customer's addresscustomer.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 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.
<!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> <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>
Please refer to Workbook Exercise 6.3, A Simple Relationship in CMP 2.0. This workbook is available free, in PDF format, at http://www.oreilly.com/catalog/entjbeans3/workbooks.