Bean-managed persistence is more complicated than container-managed persistence because you must explicitly write the persistence logic into the bean class. In order to write the persistence handling code into the bean class, you must know what type of database is being used and the how the bean class’s fields map to that database.
Bean-managed persistence gives you more flexibility in how state is managed between the bean instance and the database. Entity beans that are defined by complex joins, a combination of different databases, or other resources such as legacy systems will benefit from bean-managed persistence. Essentially, bean-managed persistence is the alternative to container-managed persistence when the deployment tools are inadequate for mapping the bean instance’s state to the database. It is likely that enterprise developers will use bean-managed persistence for creating custom beans for their business system.
The disadvantage of bean-managed persistence is that more work is
required to define the bean. You have to understand the structure of
the database and develop the logic to create, update, and remove data
associated with an entity. This requires diligence in using the EJB
callback methods such as ejbLoad()
and
ejbStore()
appropriately. In addition, you must
explicitly develop the find methods defined in the bean’s home
interface.
Another disadvantage of bean-managed persistence is that it ties the bean to a specific database type and structure. Any changes in the database or in the structure of data require changes to the bean instance’s definition; these changes may not be trivial. A bean-managed entity is not as database-independent as a container-managed entity, but it can better accommodate a complex or unusual set of data.[19]
To understand how bean-managed persistence works, we will modify the
Ship bean to use bean-managed persistence. The nice thing about this
change is that we do not need to modify any of the client’s
API. All the changes take place in the ShipBean
class and the deployment descriptor.
The bulk of the source code for a bean-managed Ship bean is applicable to both EJB 1.1 and EJB 1.0. Changes to accommodate EJB 1.0 containers are indicated by comments in the source code. There are two types of changes required:
EJB 1.0 requires that a
RemoteException
be thrown if a
system exception occurs. You will notice that a special comment,
EJB
1.0:
throw
new
RemoteException(),
is placed in the appropriate
locations to note this difference. EJB 1.1 requires that the
javax.ejb.EJBException
be thrown if a system error, like
an SQLException
, is encountered while executing a
method. The EJBException
is a subclass of
RuntimeException
in EJB 1.1, so you don’t
have to declare it in the method signature.
Multi-entity find methods in EJB
1.0 are required to return null
, if no matching
entities are found. This difference is noted in the code of the
findByCapacity()
method. Multi-entity find methods
in EJB 1.1 return an empty
Collection
or Enumeration
if no matching entities are found.
Here is the complete definition of the
bean-managed
ShipBean
:
package com.titan.ship; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.ejb.EntityContext; import java.rmi.RemoteException; import java.sql.SQLException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.DriverManager; import java.sql.ResultSet; import javax.sql.DataSource; import javax.ejb.CreateException; import javax.ejb.EJBException; import javax.ejb.FinderException; import javax.ejb.ObjectNotFoundException; import java.util.Enumeration; import java.util.Properties; import java.util.Vector; public class ShipBean implements javax.ejb.EntityBean { public int id; public String name; public int capacity; public double tonnage; public EntityContext context; public ShipPK ejbCreate(int id, String name, int capacity, double tonnage) throws CreateException { // EJB 1.0: Also throws RemoteException if ((id < 1) || (name == null)) throw new CreateException("Invalid Parameters"); this.id = id; this.name = name; this.capacity = capacity; this.tonnage = tonnage; Connection con = null; PreparedStatement ps = null; try { con = this.getConnection(); ps = con.prepareStatement("insert into Ship (id, name, capacity, tonnage) " +
"values (?,?,?,?)"
); ps.setInt(1, id); ps.setString(2, name); ps.setInt(3, capacity); ps.setDouble(4, tonnage); if (ps.executeUpdate() != 1) { throw new CreateException ("Failed to add Ship to database"); } ShipPK primaryKey = new ShipPK(); primaryKey.id = id; return primaryKey; } catch (SQLException se) { // EJB 1.0: throw new RemoteException("", se); throw new EJBException (se); } finally { try { if (ps != null) ps.close(); if (con!= null) con.close(); } catch(SQLException se) { se.printStackTrace(); } } } public void ejbPostCreate(int id, String name, int capacity, double tonnage) { // Do something useful with the primary key. } public ShipPK ejbCreate(int id, String name ) throws CreateException { // EJB 1.0: Also throws RemoteException return ejbCreate(id,name,0,0); } public void ejbPostCreate(int id, String name) { // Do something useful with the EJBObject reference. } public ShipPK ejbFindByPrimaryKey(ShipPK primaryKey) throws FinderException, { // EJB 1.0: Also throws RemoteException Connection con = null; PreparedStatement ps = null; ResultSet result = null; try { con = this.getConnection(); ps = con.prepareStatement( "select id from Ship where id = ?"); ps.setInt(1, primaryKey.id); result = ps.executeQuery(); // Does ship id exist in database? if (!result.next()) { throw new ObjectNotFoundException( "Cannot find Ship with id = "+id); } } catch (SQLException se) { // EJB 1.0: throw new RemoteException("", se); throw new EJBException(se); } finally { try { if (result != null) result.close(); if (ps != null) ps.close(); if (con!= null) con.close(); } catch(SQLException se){ se.printStackTrace(); } } return primaryKey; } public Enumeration ejbFindByCapacity(int capacity) throws FinderException { // EJB 1.0: Also throws RemoteException Connection con = null; PreparedStatement ps = null; ResultSet result = null; try { con = this.getConnection(); ps = con.prepareStatement("select id from Ship where capacity = ?"
); ps.setInt(1,capacity); result = ps.executeQuery(); Vector keys = new Vector(); while(result.next()) { ShipPK shipPk = new ShipPK(); shipPk.id = result.getInt("id"); keys.addElement(shipPk); } // EJB 1.1: always return collection, even if empty. return keys.elements(); // EJB 1.0: return null if collection is empty. // return (keys.size() > 0) ? keys.elements() : null; } catch (SQLException se) { // EJB 1.0: throw new RemoteException("",se); throw new EJBException (se); } finally { try { if (result != null) result.close(); if (ps != null) ps.close(); if (con!= null) con.close(); } catch(SQLException se) { se.printStackTrace(); } } } public void setEntityContext(EntityContext ctx) { context = ctx; } public void unsetEntityContext() { context = null; } public void ejbActivate() {} public void ejbPassivate() {} public void ejbLoad() { // EJB 1.0: throws RemoteException ShipPK pk = (ShipPK) context.getPrimaryKey(); Connection con = null; PreparedStatement ps = null; ResultSet result = null; try { con = this.getConnection(); ps = con.prepareStatement( " select name, capacity, tonnage from Ship where id = ?"); ps.setInt(1,pk.id); result = ps.executeQuery(); if (result.next()){ id = id; name = result.getString("name"); capacity = result.getInt("capacity"); tonnage = result.getDouble("tonnage"); } else { /* EJB 1.0: throw new RemoteException(); */ throw new EJBException(); } } catch (SQLException se) { // EJB 1.0: throw new RemoteException("",se); throw new EJBException(se); } finally { try { if (result != null) result.close(); if (ps != null) ps.close(); if (con!= null) con.close(); } catch(SQLException se) { se.printStackTrace(); } } } public void ejbStore() { // EJB 1.0: throws RemoteException Connection con = null; PreparedStatement ps = null; try { con = this.getConnection(); ps = con.prepareStatement("update Ship set name = ?, capacity = ?, " +
"tonnage = ? where id = ?"
); ps.setString(1,name); ps.setInt(2,capacity); ps.setDouble(3,tonnage); ps.setInt(4,id); if (ps.executeUpdate() != 1) { // EJB 1.0: throw new RemoteException ("ejbStore failed"); throw new EJBException("ejbStore"); } } catch (SQLException se) { // EJB 1.0: throw new RemoteException("",se); throw new EJBException (se); } finally { try { if (ps != null) ps.close(); if (con!= null) con.close(); } catch(SQLException se) { se.printStackTrace(); } } } public void ejbRemove() { // EJB 1.0: throws RemoteException Connection con = null; PreparedStatement ps = null; try { con = this.getConnection(); ps = con.prepareStatement("delete from Ship where id = ?"
); ps.setInt(1, id); if (ps.executeUpdate() != 1) { // EJB 1.0 throw new RemoteException("ejbRemove"); throw new EJBException("ejbRemove"); } } catch (SQLException se) { // EJB 1.0: throw new RemoteException("",se); throw new EJBException (se); } finally { try { if (ps != null) ps.close(); if (con!= null) con.close(); } catch(SQLException se) { se.printStackTrace(); } } } public String getName() { return name; } public void setName(String n) { name = n; } public void setCapacity(int cap) { capacity = cap; } public int getCapacity() { return capacity; } public double getTonnage() { return tonnage; } public void setTonnage(double tons) { tonnage = tons; } private Connection getConnection() throws SQLException { // Implementations for EJB 1.0 and EJB 1.1 shown below } }
There are three types of exceptions thrown from a bean: application exceptions, which indicate business logic errors, runtime exceptions, and checked subsystem exceptions, which are throw from subsystems like JDBC or JNDI.
Application exceptions include standard EJB application exceptions
and custom application exceptions. The standard EJB application
exceptions are CreateException
,
FinderException
,
ObjectNotFoundException
,
DuplicateKeyException
, and
RemoveException
. These exceptions are thrown from
the appropriate methods to indicate that a business logic error has
occurred. Custom exceptions are exceptions you develop for specific
business problems. You will develop custom exceptions in Chapter 7.
RuntimeException
types are thrown from the virtual
machine itself and indicate that a fairly serious programming error
has occurred. Examples include
NullPointerException
and
IndexOutOfBoundsException
. These exceptions are
handled by the
container
automatically and should not be handled inside a bean method.
Checked exceptions thrown by
other subsystems must be wrapped in an
EJBException
and rethrown
from the method. Several examples of this can be found in the
previous example, in which an SQLException
that
was thrown from JDBC was caught and rethrown as an
EJBException
. Checked exceptions from other
subsystems, such as those thrown from JNDI, JavaMail, JMS, etc.,
should be handled in the same fashion. The
EJBException
is a subtype of the
RuntimeException
, so it doesn’t need to be
declared in the method’s throws
clause.
Checked exceptions thrown by other subsystems must be wrapped in a
RemoteException
and rethrown
from the method. In the previous example, we caught an
SQLException
that was thrown by JDBC, and threw
our own RemoteException
. Checked exceptions, such
as those thrown from JNDI, JavaMail, JMS, etc., should be handled in
the same fashion.
Exceptions have an impact on transactions and are fundamental to transaction processing. Exceptions are examined in greater detail in Chapter 8.
An
EntityContext
is given
to the bean instance at the beginning of its life cycle, before
it’s made available to service any clients. The
EntityContext
should be placed in an instance
field of the bean and maintained for the life of the instance.
The setEntityContext()
method saves the
EntityContext
assigned to the bean in the instance
field context
. As the bean instance is swapped
from one EJB object to the next, the information obtainable from the
EntityContext
reference changes to reflect the EJB
object that the instance is assigned to. This is possible because the
EntityContext
is an interface, not a static class
definition. This means that the container can implement the
EntityContext
with a concrete class that it
controls. As the bean instance is swapped from one EJB object to
another, some of the information made available through the
EntityContext
will also change.
Both SessionContext
, used by session beans, and
EntityContext
extend
EJBContext
. Here is the definition of
EntityContext
:
public interface EntityContext extends EJBContext { public EJBObject getEJBObject() throws java.lang.IllegalStateException; public Object getPrimaryKey() throws java.lang.IllegalStateException; }
The superinterface, the EJBContext
, defines
several methods that provide a lot of information about the bean
instance’s properties, security, and transactional environment.
The next section discusses the EJBContext
in more
detail. Here we focus on the methods defined in the
EntityContext
.
The getEJBObject()
method returns a remote
reference to the bean instance’s EJB object. This is the same
kind of reference that might be used by an application client or
another bean. The purpose of this method is to provide the bean
instance with a reference to itself when it needs to perform a
loopback operation. A loopback occurs when a bean invokes a method on
another bean, passing itself as one of the parameters. Here is a
hypothetical example:
public class A_Bean extends EntityBean { public EntityContext context; public void someMethod() { B_Bean b = ... // Get a remote reference to a bean of type B_Bean. // EJB 1.0: Use native casting instead of narrow() EJBObject obj = context.getEJBObject(); A_Bean mySelf = (A_Bean) PortableRemoteObject.narrow(obj,A_Bean.class); b.aMethod( mySelf ); } ... }
It is illegal for a bean instance to pass a this
reference to another bean; instead, it passes its remote reference,
which the bean instance gets from its context. As discussed in Chapter 3, loopbacks or reentrant behavior are
problematic in EJB and should be avoided by new EJB developers.
Session beans also define the getEJBObject()
method in the SessionContext
interface; its
behavior is exactly the same.
The
getEJBHome()
method is available to both entity
and session beans and is defined in the EJBContext
class. The getEJBHome()
method returns a remote
reference to the EJB home for that bean type. The bean instance can
use this method to create new beans of its own type or, in the case
of entity beans, to find other bean entities of its own type. Most
beans won’t need access to their EJB home, but if you need one,
getEJBHome()
provides a way to get it.
The
getPrimaryKey()
method allows a bean instance to
get a copy of the primary key to which it is currently assigned.
Outside of the ejbLoad()
and
ejbStore()
methods, the use of this method, like
the getEJBHome()
method in the
EJBContext
, is probably rare, but the
EntityContext
makes the primary key available for
those unusual circumstances when it is needed.
As the context in which the bean instance operates changes, some of
the information made available through the
EntityContext
reference will be changed by the
container. This is why the methods in the
EntityContext
throw the
java.lang.IllegalStateException
. The
EntityContext
is always available to the bean
instance, but the instance is not always assigned to an EJB object.
When the bean is between EJB objects, it has no EJB object or primary
key to return. If the getEJBObject()
or
getPrimaryKey()
methods are invoked when the bean
is not assigned to an EJB object (when it is swapped out), they throw
an IllegalStateException
. Appendix B provides tables for each bean type describing
which EJBContext
methods can be invoked at
what
times.
The EntityContext
extends the
javax.ejb.EJBContext
class, which is also the base
class for the SessionContext
used by session
beans. The EJBContext
defines several methods that
provide useful information to a bean at runtime. Here is the
definition of the EJBContext
interface:
package javax.ejb; public interface EJBContext { public EJBHome getEJBHome(); // security methods public java.security.Principal getCallerPrincipal(); public boolean isCallerInRole(java.lang.String roleName); // deprecated methods public java.security.Identity getCallerIdentity(); public boolean isCallerInRole(java.security.Identity role); public java.util.Properties getEnvironment(); // transaction methods public javax.transaction.UserTransaction getUserTransaction() throws java.lang.IllegalStateException; public boolean getRollbackOnly() throws java.lang.IllegalStateException; public void setRollbackOnly() throws java.lang.IllegalStateException; }
The
getEJBHome()
method returns a reference to the
bean’s home interface. This is useful if
the bean needs to create or find beans of its own type. As an
example, if all employees in Titan’s system (including
managers) are represented by the Employee bean, then a manager
employee that needs access to subordinate employees can use the
getEJBHome()
method to get beans representing the
appropriate employees:
public class EmployeeBean implements EntityBean {
int id;
String firstName;
...
public Enumeration getSubordinates() {
// EJB 1.0: Use native Java casting instead of narrow()
Object ref = ejbContext.getEJBHome()
;
EmployeeHome home = (EmployeeHome)
PortableRemoteObject.narrow(ref, EmployeeHome.class);
Enumeration subordinates = home.findByManagerID(this.id);
return subordinates;
}
...
}
The
getCallerPrincipal()
method is used to obtain the
Principal
object representing the client that is
currently accessing the bean. The
Principal
object can, for
example, be used by the Ship bean to track the identity of clients
making updates:
public class ShipBean implements EntityBean {
String modifiedBy;
EntityContext context;
...
public void setTonnage(double tons){
tonnage = tons;
Principal principal = context.getCallerPrincipal();
String modifiedBy = principal.getName();
}
...
}
The
isCallerInRole()
method tells you whether the client
accessing the bean is a member of a specific role, identified by a
role name. This method is useful when more access control is needed
than the simple method-based access control can provide. In a banking
system, for example, the Teller role might be allowed to make
withdrawals, but only a Manager can make withdrawals over $10,000.00.
This kind of fine-grained access control cannot be addressed
through EJB’s security attributes because it involves a
business logic problem. Therefore, we can use the
isCallerInRole()
method to augment the automatic
access control provided by EJB. First, let’s assume that all
Managers also are Tellers. Let’s also assume that the
deployment descriptor for the Account bean specifies that clients
that are members of the Teller role can invoke the
withdraw()
method. The business logic in the
withdraw()
method uses
isCallerInRole()
to further refine access control
so that only the Manager role can withdraw over $10,000.00.
public class AccountBean implements EntityBean {
int id;
double balance;
EntityContext context;
public void withdraw(Double withdraw)
throws AccessDeniedException {
if (withdraw.doubleValue() > 10000) {
boolean isManager = context.isCallerInRole("Manager");
if (!isManager) {
// Only Managers can withdraw more than 10k.
throw new AccessDeniedException();
}
}
balance = balance - withdraw.doubleValue();
}
...
}
The EJBContext
contains some deprecated methods
that were used in EJB 1.0 but will be abandoned in a future version
of the specification. Support for these deprecated methods is
optional so that EJB 1.1 servers can host EJB 1.0 beans. EJB servers
that do not support the deprecated security methods will throw a
RuntimeException
. The deprecated security methods
are based on EJB 1.0’s use of the Identity
object instead of the Principal
object. The
semantics of the deprecated methods are basically the same, but
because Identity
is an abstract class, it has
proven to be too difficult to use. Chapter 6 goes into detail on how to use
the Identity
driven security methods. EJB 1.1
beans should use the Principal
-based security
methods.
The
getEnvironment()
method has been replaced by the
JNDI Environment Naming Context,
which is discussed later in the book. Support in EJB 1.1 for the
deprecated getEnvironment()
method is discussed in
detail in Chapter 7.
The transactional methods (getUserTransaction()
,
setRollbackOnly()
,
getRollbackOnly()
) are described in
detail in Chapter 8
.
In EJB 1.0, the EntityContext
serves essentially
the same purpose as in EJB 1.1. It extends the
javax.ejb.EJBContext
class, which is also the base
class for the SessionContext
used by session
beans, and it defines several methods that provide useful information
to a bean at runtime. Here is the definition of the
EJBContext
for Version 1.0:
package javax.ejb; public interface EJBContext { public EJBHome getEJBHome(); public java.util.Properties getEnvironment(); // security methods public java.security.Identity getCallerIdentity(); public boolean isCallerInRole(java.security.Identity role); // transaction methods public javax.transaction.UserTransaction getUserTransaction() throws java.lang.IllegalStateException; public boolean getRollbackOnly() throws java.lang.IllegalStateException; public void setRollbackOnly() throws java.lang.IllegalStateException; }
The
getEJBHome()
method is used to obtain a
reference to the bean’s home interface. Repeating the same
example: if all employees in Titan’s system (including
managers) are represented by the Employee bean, then a manager can
access subordinate employees using the
getEJBHome()
method:
public class EmployeeBean implements EntityBean { int id; String firstName; ... public Enumeration getSubordinates() { EmployeeHome home = (EmployeeHome) ejbContext.getEJBHome(); Enumeration subordinates = home.findByManagerID(this.id); return subordinates; } ... }
The
EJBContext.getEnvironment()
method is used by both
session and entity
beans. This method provides the bean instance with a set of
properties defined at deployment; it returns an instance of
java.util.Properties
, which is a type of hash
table. The bean’s deployment descriptor provides the
properties, which can include anything you consider necessary for the
bean to function. The environment properties are always
available to the bean instance from any method.
Properties are usually used to modify the business behavior at runtime. As an example, an Account bean used in a banking system might use properties to set a limit for withdrawals. Here’s how the code might look:
public class AccountBean implements EntityBean { int id; double balance; EntityContext ejbContext; public void withdraw(Double withdraw) throws WithdrawLimitException { Properties props = ejbContext.getEnvironment(); String value = props.getProperty("withdraw_limit"); Double limit = new Double(value) if (withdraw.doubleValue() > limit.doubleValue()) throw new WithdrawLimitException(limit); else balance = balance - withdraw.doubleValue(); } } ... }
When we create the deployment descriptor for the
AccountBean
, we set the
withdraw_limit
property in a
Properties
object, which in turn defines the
environment properties for the entire bean. The following code shows
how environment properties are set when creating a deployment
descriptor:
Properties props = new Properties(); props.put("withdraw_limit","100,000.00"); deploymentDesc.setEnvironmentProperties(props);
In this case, we set the withdraw_limit
to be
$100,000.00. Environment properties can be used for a variety of
purposes; setting limits is just one application. In Section 6.2.6, you will learn how to use
Environment properties to obtain database connections.
The
getCallerIdentity()
method is used to obtain the
java.security.Identity
object that represents the
client accessing the bean. The Identity
object
might, for example, be used by the Ship bean to track the identity of
the client making updates:
public class ShipBean implements EntityBean {
String modifiedBy;
EntityContext context;
...
public void setTonnage(double tons) {
tonnage = tons;
Identity identity = context.getCallerIdentity();
String modifiedBy = identity.getName();
}
...
}
isCallerInRole()
determines whether the client invoking the
method is a member of a specific role, identified by a
Identity
object. We can use the same example that
we discussed for EJB 1.1: a bank in which a Teller can make
withdrawals, but only a Manager can make withdrawals over $10,000.00.
This kind of fine-grained access control cannot be addressed by
EJB’s security attributes because it’s a business logic
problem. Therefore, we can use isCallerInRole()
to
augment the automatic access control provided by EJB. In the
Account bean, the access control attributes specify that only clients
that are members of the Teller role can invoke the
withdraw()
method. The business logic in the
withdraw()
method uses the
isCallerInRole()
method to further refine access
control so that only Manager role types, which are also a Teller role
type, can withdraw over $10,000.00.
public class AccountBean implements EntityBean {
int id;
double balance;
EntityContext ejbContext;
public void withdraw(Double withdraw)
throws WithdrawLimitException, AccessDeniedException {
if (withdraw.doubleValue() > 10000) {
Identity managerRole = new RoleIdentity("Manager");
boolean isManager = ejbContext.isCallerInRole(managerRole);
if (!isManager) {
// Only tellers can withdraw more than 10k.
throw new AccessDeniedException();
}
}
balance = balance - withdraw.doubleValue();
}
...
}
Unfortunately, while the EJB 1.0 specification requires the use of
the Identity
type as a role identifier, it
doesn’t specify how a bean should acquire the
Identity
object.[20] The Identity
class is an abstract class, so simply instantiating it is not
possible. In our example, a mysterious
RoleIdentity
object was instantiated with the name
of the role being tested. This provided us with an
Identity
object that could be used in the
isCallerInRole(Identity role)
method. But where
did the RoleIdentity
object come from?
The RoleIdentity
class is a simple extension of the
java.security.Identity
class, and provides us with
a simple, concrete implementation of Identity
that
we can instantiate with a string name.[21] Here is the definition of this class:
import java.security.Identity; public class RoleIdentity extends Identity { public RoleIdentity(String name) { super(name); } }
Use of the RoleIdentity
class works in those EJB
servers that limit comparison operations of
Identity
to the name attribute. In other words,
these servers simply compare the string values returned by the
getName()
methods of the
Identity
objects.
Some EJB vendors may use more complicated mechanisms for comparing
the Identity
objects. In these cases, you may have
to enhance the RoleIdentity
defined here, or use a
vendor-specific mechanism for verifying membership in a role.
BEA’s WebLogic Server, for example, works well with the
RoleIdentity
, but it also provides a proprietary
mechanism for obtaining group Identity
objects
(i.e., roles to which identities belong). The following code fragment
shows how the Account bean would be coded to use the WebLogic
security API instead of RoleIdentity
:
public class AccountBean implements EntityBean { int id; double balance; EntityContext ejbContext; public void withdraw(Double withdraw) throws WithdrawLimitException, AccessDeniedException { if (withdraw.doubleValue() > 10000) { Identity managerRole = (Identity) weblogic.security.acl.Security.getRealm().getGroup("Manager"); boolean isManager = ejbContext.isCallerInRole(managerRole) if (!isManager) { // Only tellers can withdraw more than 10k. throw new AccessDeniedException(); } } balance = balance - withdraw.doubleValue(); } ... }
In general, proprietary APIs like the previous one should be avoided so that the bean remains portable across EJB servers.
The transactional methods (getUserTransaction()
,
setRollbackOnly()
,
getRollbackOnly()
) are described in
detail in Chapter 8.
Titan’s business system is based on a relational database, so we need to start with access to the database. The JDBC API provides a standard and convenient way for programs written in Java to access relational databases. We use JDBC throughout this book and assume that you’re already familiar with it.
To get access to the database we simply request a connection from a
DataSource
, which we obtain from the JNDI
environment naming context:
private Connection getConnection() throws SQLException { try { Context jndiCntx = new InitialContext(); DataSource ds = (DataSource)jndiCntx.lookup("java:comp/env/jdbc/titanDB"); return ds.getConnection(); } catch (NamingException ne) { throw new EJBException(ne); } }
In EJB 1.1, every bean has access to its JNDI environment naming context (ENC), which is part of the bean-container contract. In the bean’s deployment descriptor, resources such as the JDBC DataSource, JavaMail, and Java Messaging Service can be mapped to a context (name) in the ENC. This provides a portable model for accessing these types of resources. In EJB 1.0, standard mechanisms for accessing JDBC connections and other resources were not defined. Here’s the relevant portion of the EJB 1.1 deployment descriptor:
<enterprise-beans>
<entity>
<resource-ref>
<description>DataSource for the Titan database</description>
<res-ref-name>jdbc/titanDB
</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
<resource-ref>
...
<entity>
...
<enterprise-beans>
The <resource-ref>
tag is used for any
resource ( JDBC, JMS, JavaMail) that is accessed from the ENC. It
describes the JNDI name of the resource
(<res-ref-name>
), the factory type
(<res-type>
), and whether
authentication is performed
explicitly by the bean or automatically by the container
(<res-auth>
). In this example, we are
declaring that the JNDI name "jdbc/titanDB
" refers
to a javax.sql.DataSource
resource manager, and
that authentication to the database is handle automatically by the
container. The JNDI name specified in the
<res-ref-name>
tag is always relative to the
standard JNDI ENC context name, "java:comp/env"
.
When the bean is deployed, the deployer maps the information in the
<resource-ref>
tag to a live database. This
is done in a vendor-specific manner, but the end result is the same.
When a database connection is requested using the JNDI name
"java:comp/jdbc/titanDB"
, a
DataSource
for the Titan database is returned.
Consult your vendor’s documentation for details on how to map
the DataSource
to the database at deployment time.
The
getConnection()
method provides us with a simple and consistent mechanism for
obtaining a database connection for our ShipBean
class. Now that we have a mechanism for obtaining a database
connection, we can use it to insert, update, delete, and find
Ship
entities in the database.
To get access to the database, we simply request a connection
from the DriverManager
. To do this, we add a
private method to the ShipBean
class called
getConnection()
:
private Connection getConnection() throws SQLException { Properties environmentProps = context.getEnvironment(); String url = environmentProps.getProperty("jdbcURL"); return DriverManager.getConnection(url); }
The getConnection()
method provides an excellent
opportunity to use the EntityContext
that was
passed to the bean instance at the beginning of its life cycle. We
use the
EJBContext.getEnvironment()
method to obtain
properties that help us
acquire JDBC connections.
When we create the deployment descriptor for the
ShipBean
, we use a Properties
object to tell the bean what URL to use to obtain a database
connection. The following code, taken from the
MakeDD
class, shows how it’s done:
Properties props = new Properties(); props.put("jdbcURL","jdbc:<subprotocol>:<subname>"); shipDD.setEnvironmentProperties(props);
We create a new instance of Properties
, add the
"jdbcURL"
property, and then call the
setEnvironmentProperties()
method of the
DeploymentDescriptor
class to pass the properties
to the actual bean when it is deployed.
The information in the property table
is used in the getConnection()
method.
This technique solves a nasty
problem in an elegant, vendor-independent way: how does the bean make
use of vendor-specific resources? The JDBC URL used is
vendor-specific and therefore shouldn’t be made part of the
bean itself. However, when you are deploying a bean, you certainly
know what vendor-specific environment you are deploying it in; thus
the URL logically belongs to the deployment descriptor. In short, the
environment properties lets vendor-specific and environment-specific
information be defined in the deployment process, where it belongs,
and not during the bean development process.
The getConnection()
method provides us with a
simple and consistent mechanism for obtaining a database connection
for our ShipBean
class. Now that we have a
mechanism for obtaining a database connection we can use it to
insert, update, delete, and find Ship
entities in
the
database.
The
ejbCreate()
methods are called by the container when a client
invokes the corresponding create()
method on the
bean’s home. With bean-managed persistence, the
ejbCreate()
methods are responsible for adding the
new entity to the database. This means that the new version of
ejbCreate()
will be much more complicated than our
container-managed version from earlier examples; with
container-managed beans, ejbCreate()
doesn’t
have to do much more than initialize a few fields. The EJB
specification also states that ejbCreate()
methods
in bean-managed persistence must return the primary key of the newly
created entity. This is another difference between bean-managed and
container-managed persistence; in our container-managed beans,
ejbCreate()
was required to return
void
.
The following code contains the ejbCreate()
method
of the ShipBean
, modified for bean-managed
persistence. Its return type has been changed from
void
to the Ship bean’s primary key,
ShipPK
. Furthermore, the method uses the JDBC API
to insert a new record into the database based on the information
passed as parameters. The changes to the original
ejbCreate()
method are emphasized in bold.
public ShipPK ejbCreate(int id, String name, int capacity, double tonnage) throws CreateException { // EJB 1.0: Also throws RemoteException if ((id < 1) || (name == null)) throw new CreateException("Invalid Parameters"); this.id = id; this.name = name; this.capacity = capacity; this.tonnage = tonnage; Connection con = null; PreparedStatement ps = null; try { con = this.getConnection(); ps = con.prepareStatement( "insert into Ship (id, name, capacity, tonnage) " + "values (?,?,?,?)"); ps.setInt(1, id); ps.setString(2, name); ps.setInt(3, capacity); ps.setDouble(4, tonnage); if (ps.executeUpdate() != 1) { throw new CreateException ("Failed to add Ship to database"); } ShipPK primaryKey = new ShipPK(); primaryKey.id = id; return primaryKey; } catch (SQLException se) { // EJB 1.0: throw new RemoteException(""se); throw new EJBException (se); } finally { try { if (ps != null) ps.close(); if (con!= null) con.close(); } catch(SQLException se) { se.printStackTrace(); } } }
At the beginning of the method, we verify that the parameters are
correct, and throw a
CreateException
if the id
is less than 1, or the
name
is null
. This shows how
you would typically use a CreateException
to
report an application logic error.
The ShipBean instance fields are still initialized using the
parameters passed to ejbCreate()
as before, but
now we manually insert the data into the SHIP
table in our database. To do so, we use a
JDBC PreparedStatement
for SQL requests because it
makes it easier to see the parameters being used. Alternatively, we
could have used a stored procedure through a JDBC
CallableStatement
or a simple JDBC
Statement
object. We insert the new bean into the
database using an SQL INSERT
statement and the
values passed into ejbCreate()
parameters. If the
insert is successful (no exceptions thrown), we create a primary key
and return it to the container. If the insert operation is
unsuccessful, we throw a new CreateException
,
which illustrates its use in more ambiguous situation. Failure to
insert the record could be construed as an application error or a
system failure. In this situation, the JDBC subsystem hasn’t
thrown an exception, so we shouldn’t interpret the inability to
insert a record as a failure of the subsystem. Therefore, we throw a
CreateException
instead of an
EJBException
(EJB 1.1) or
RemoteException
(EJB 1.0). Throwing a
CreateException
provides the application the
opportunity to recover from the error, a transactional concept that
is covered in more detail in Chapter 8.
Behind the scenes, the container uses the primary key and the
ShipBean
instance that returned it to provide the
client with a remote reference to the new Ship entity. Conceptually,
this means that the ShipBean
instance and primary
key are assigned to a newly constructed EJB object, and the EJB
object stub is returned to the client.
Our home interface requires us to provide a second
ejbCreate()
method with different parameters. We
can save work and write more bulletproof code by making the second method
call the
first:
public ShipPK ejbCreate(int id, String name) throws CreateException { return ejbCreate(id,name,0,0); }
Throughout the life of an
entity, its
data will be changed by client
applications. In the ShipBean
, we provide accessor
methods to change the name
,
capacity
, and tonnage
of the
Ship bean after it has been created. Invoking any of these accessor
methods changes the state of the ShipBean
instance, which must be reflected in the database. It is also
necessary to ensure that the state of the bean instance is always
up-to-date with the database.
In container-managed persistence, synchronization between the bean
and the database takes place automatically; the container handles it
for you. With bean-managed persistence, you are responsible for
synchronization: the bean must read and write to the database
directly. The container works closely with the bean-managed entities
by advising them when to synchronize their state through the use of
two callback methods:
ejbStore()
and ejbLoad()
.
The ejbStore()
method is called when the container
decides that it is a good time to write the entity bean’s data
to the database. The container makes these decisions based on all the
activities it is managing, including transactions, concurrency, and
resource management. Vendor implementations may differ slightly in
when the ejbStore()
method is called, but this is
not the bean developer’s concern. In most cases, the
ejbStore()
method will be called after a business
method has been invoked on the bean. Here is the
ejbStore()
method for the ShipBean
:
public void ejbStore() { // EJB 1.0: throws RemoteException Connection con = null; PreparedStatement ps = null; try { con = this.getConnection(); ps = con.prepareStatement( "update Ship set name = ?, capacity = ?, " + "tonnage = ? where id = ?"); ps.setString(1,name); ps.setInt(2,capacity); ps.setDouble(3,tonnage); ps.setInt(4,id); if (ps.executeUpdate() != 1) { // EJB 1.0: throw new RemoteException ("ejbStore failed"); throw new EJBException("ejbStore"); } } catch (SQLException se) { // EJB 1.0: throw new RemoteException("",se); throw new EJBException (se); } finally { try { if (ps != null) ps.close(); if (con!= null) con.close(); } catch(SQLException se) { se.printStackTrace(); } } }
Except for the fact that we are doing an update instead of an insert,
this method is similar to the ejbCreate()
method
we coded earlier. A JDBC PreparedStatement
is
employed to execute the SQL UPDATE
command, and
the bean’s persistent fields are used as parameters to the
request. This method synchronizes the database with the state of the
bean.
Notice that we throw an
EJBException
when a problem occurs.
All EJB callback methods declare the EJBException
and RemoteException
in their throws clause. If you
need to throw an exception from one of these callback methods, it
must be an EJBException
or a subclass. The
RemoteException
type is included in
the method signature to support backward compatibility with EJB 1.0
beans. Throwing the RemoteException
from callback
methods is deprecated in EJB 1.1, which means that it will not be
supported in subsequent versions.
In EJB 1.0, we throw a RemoteException
when a
problem occurs. All EJB callback methods declare the
RemoteException
in their throws clause. If you
need to throw an exception from one of these callback methods, it
must be a RemoteException
.
EJB also provides an
ejbLoad()
method that synchronizes the state of the entity with the database.
This method is usually called prior to a new transaction or business
method invocation. The idea is to make sure that the bean always
represents the most current data in the database, which could be
changed by other beans or other non-EJB applications. Here is the
ejbLoad()
method for a bean-managed
ShipBean
class:
public void ejbLoad() { // EJB 1.0: throws RemoteException ShipPK pk = (ShipPK) context.getPrimaryKey(); Connection con = null; PreparedStatement ps = null; ResultSet result = null; try { con = this.getConnection(); ps = con.prepareStatement( "select name, capacity, tonnage from Ship where id = ?"); ps.setInt(1,pk.id); result = ps.executeQuery(); if (result.next()) { id = id; name = result.getString("name"); capacity = result.getInt("capacity"); tonnage = result.getDouble("tonnage"); } else { // EJB 1.0: throw new RemoteException(); throw new EJBException(); } } catch (SQLException se) { // EJB 1.0: throw new RemoteException("",se); throw new EJBException(se); } finally { try { if (result != null) result.close(); if (ps != null) ps.close(); if (con!= null) con.close(); } catch(SQLException se) { se.printStackTrace(); } } }
To execute the ejbLoad()
method we need a primary
key. To get a primary key, we query the bean’s
EntityContext
. Note that we don’t get the
primary key directly from the ShipBean
’s
id
field because we cannot guarantee that this
field is always valid—the ejbLoad()
method
might be populating the bean instance’s state for the first
time, in which case the fields would all be set to the default
values. This situation would occur following bean activation. We can
guarantee that the EntityContext
for the
ShipBean
is valid because the EJB specification
requires that the bean instance EntityContext
reference is valid before the ejbLoad()
method can
be invoked. More about this in the life cycle section later in
this
chapter.
In addition to
handling their
own inserts and updates,
bean-managed entities must also handle their own deletions. When a
client application invokes the remove method on the EJB home or EJB
object, that method invocation is delegated to the bean-managed
entity by calling ejbRemove()
. It is the bean
developer’s responsibility to implement an
ejbRemove()
method that deletes the entity’s
data from the database. Here’s the
ejbRemove()
method for our bean-managed
ShipBean
:
public void ejbRemove() { // EJB 1.0: throws RemoteException Connection con = null; PreparedStatement ps = null; try { con = this.getConnection(); ps = con.prepareStatement("delete from Ship where id = ?"); ps.setInt(1, id); if (ps.executeUpdate() != 1) { // EJB 1.0 throw new RemoteException("ejbRemove"); throw new EJBException("ejbRemove"); } } catch (SQLException se) { // EJB 1.0: throw new RemoteException("",se); throw new EJBException (se); } finally { try { if (ps != null) ps.close(); if (con!= null) con.close(); } catch(SQLException se){ se.printStackTrace(); } } }
In
bean-managed
EntityBean
s, the
find methods
in the home interface must match the ejbFind methods in the actual
bean class. In other words, for each method named
find
lookup-type
()
in the home interface, there must be a corresponding
ejbFind
lookup-type
()
method in the bean implementation with the same arguments and
exceptions. When a find method is invoked on an EJB home, the
container delegates the
find
lookup-type
()
to a corresponding
ejbFind
lookup-type
()
method on the bean instance. The bean-managed entity is responsible
for finding records that match the find requests. In
ShipHome
, there are two find methods:
public interface ShipHome extends javax.ejb.EJBHome { public ShipfindByPrimaryKey
(ShipPK primaryKey) throws FinderException, RemoteException; public EnumerationfindByCapacity
(int capacity) throws FinderException, RemoteException; }
And here are the signatures of the corresponding ejbFind methods in
the ShipBean
:
public class ShipBean extends javax.ejb.EntityBean { public ShipPKejbFindByPrimaryKey
(ShipPK primaryKey) throws FinderException, RemoteException {} public EnumerationejbFindByCapacity
(int capacity) throws FinderException, RemoteException {} }
Aside from the names, there’s one difference
between these two groups of methods. The
find methods in the home interface return either an object
implementing the bean’s remote interface—in this case,
Ship
—or a collection of such objects in the
form of a java.util.Enumeration
or
java.util.Collection
. The ejbFind methods in the
bean class return either a
primary key for the appropriate bean—in this case,
ShipPK
—or a collection of primary keys. The
methods that return a single object (whether a remote interface or a
primary key) are used whenever you need to look up a single reference
to a bean. If you are looking up a group of references (for example,
all ships with a certain capacity), you have to use the method that
returns either the Collection
or
Enumeration
type. In either case, the container
intercepts the primary keys and converts them into remote references
for the client.
In EJB 1.0, you are not allowed to write find and ejbFind methods
that return arrays, vectors, or other Collection
types. The find methods in the home interface return either an object
implementing the bean’s remote interface—in this case,
Ship
—or an Enumeration
of
such objects. The ejbFind methods in the bean class return either a
primary key for the appropriate bean or an
Enumeration
of primary keys. The methods that
return a single object (whether a remote interface or a primary key)
are used whenever you need to look up a single reference to a bean.
If you are looking up a group of references (for example, all ships
with a certain capacity), you have to use the method that returns a
serializable implementation of Enumeration
. In
either case, the container intercepts the primary keys and converts
them into remote references for the client.
It shouldn’t come as a surprise that the object you
return—whether it’s a primary key or a remote
interface—must be appropriate for the type of bean you’re
defining. For example, you shouldn’t put find methods in a Ship
bean to look up and return Cabin
objects. If you
need to return collections of a different bean type, use a business
method in the remote interface, not a find method from the home
interface.
Both find methods in the ShipBean
class methods
throw a FinderException
if a failure in the
request occurs when an SQL exception condition is encountered. The
findByPrimaryKey()
throws the
ObjectNotFoundException
if there are no records in the database that match the
id
argument.
The findByCapacity()
method returns an empty
collection, and not a null reference, if no SHIP
records were found with a matching capacity. Find methods also throw
FinderException
and EJBException
, in addition to any application-specific exceptions that you
consider appropriate.
The findByCapacity()
method returns a
null
if no SHIP
records were
found with a matching capacity. Find methods also throw
FinderException
and
RemoteException
. The find methods can also throw any
application-specific exceptions that you consider appropriate.
It is mandatory that all entity home interfaces include the method
findByPrimaryKey()
. This method returns a single
remote reference and takes one parameter, the primary key for that
bean type. You cannot deploy an entity bean that doesn’t
include a
findByPrimaryKey()
method in its home interface.
Following the rules outlined earlier, we can define two ejbFind
methods in ShipBean
that match the two find
methods defined in the ShipHome
:
public ShipPK ejbFindByPrimaryKey(ShipPK primaryKey)
throws FinderException, {
// EJB 1.0: Also throws RemoteException
Connection con = null;
PreparedStatement ps = null;
ResultSet result = null;
try {
con = this.getConnection();
ps = con.prepareStatement(
"select id from Ship where id = ?");
ps.setInt(1, primaryKey.id);
result = ps.executeQuery();
// does ship id exist in database?
if (!result.next()){
throw new ObjectNotFoundException(
"Cannot find Ship with id = "+id);
}
} catch (SQLException se) {
// EJB 1.0: throw new RemoteException("", se);
throw new EJBException(se);
}
finally {
try {
if (result != null) result.close();
if (ps != null) ps.close();
if (con!= null) con.close();
} catch(SQLException se) {se.printStackTrace();}
}
return primaryKey;
}
public Enumeration ejbFindByCapacity(int capacity)
throws FinderException {
// EJB 1.0: Also throws RemoteException
Connection con = null;
PreparedStatement ps = null; ResultSet result = null;
try {
con = this.getConnection();
ps = con.prepareStatement(
"select id from Ship where capacity = ?");
ps.setInt(1,capacity);
result = ps.executeQuery();
Vector keys = new Vector();
while(result.next()) {
ShipPK shipPk = new ShipPK();
shipPk.id = result.getInt("id");
keys.addElement(shipPk);
}
// EJB 1.1: always return collection, even if empty.
return keys.elements();
// EJB 1.0: return null if collection is empty.
// return (keys.size() > 0) ? keys.elements() : null;
}
catch (SQLException se) {
// EJB 1.0: throw new RemoteException("",se);
throw new EJBException (se);
}
finally {
try {
if (result != null) result.close();
if (ps != null) ps.close();
if (con!= null) con.close();
} catch(SQLException se){
se.printStackTrace();
}
}
}
The mandatory findByPrimaryKey()
method uses the
primary key to locate the
corresponding database record. Once it has verified that the record
exists, it simply returns the primary key to the container, which
then uses the key to activate a new instance and associate it with
that primary key at the appropriate time. If the there is no record
associated with the primary key, the method throws a
ObjectNotFoundException
.
The ejbFindByCapacity()
method returns an
enumeration of primary keys that match the criteria passed into the
method. Again, we construct a prepared statement that we use to
execute our SQL query. This time, however, we expect multiple results
so we use the java.sql.ResultSet
to iterate
through the results, creating a vector of primary keys for each
SHIP_ID
returned.
Find
methods are not executed on bean instances that are currently
supporting a client application. Only bean instances that are not
assigned to an EJB object (instances in the instance pool) are
supposed to service find requests, which means that the
ejbFind()
method in the bean instance has somewhat
limited use of its EntityContext
. The
getPrimaryKey()
and
getEJBObject()
methods will throw exceptions
because the bean instance is a pooled instance and is not associated
with a primary key or EJBObject
. Where do the
objects returned by a find method come from? This seems like a simple
enough question, but the answer is surprisingly complex. Remember
that a find method isn’t executed by a bean instance that is
actually supporting the client; the container finds an idle bean
instance from the instance pool. The container is responsible for
creating the EJB objects and remote references for the primary keys
returned by the ejbFind method in the bean class. As the client
accesses these remote references, bean instances are swapped into the
appropriate EJB objects, loaded with data, and made ready to
service the clients requests.
With a
complete definition of the Ship bean,
including the remote interface, home interface, and primary key, we
are ready to create a deployment descriptor. Here is the
XML deployment descriptor for EJB
1.1. This deployment descriptor is not significantly different from
the descriptor we created for the container-managed Ship bean
earlier. In this deployment descriptor the
persistence-type
is Bean
and
there are no container-managed field declarations. We also must
declare the
DataSource
resource factory that we use
to query and update the database.
<?xml version="1.0"?> <!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 1.1//EN" "http://java.sun.com/j2ee/dtds/ejb-jar_1_1.dtd"> <ejb-jar> <enterprise-beans> <entity> <description> This bean represents a cruise ship. </description> <ejb-name>ShipBean</ejb-name> <home>com.titan.ship.ShipHome</home> <remote>com.titan.ship.Ship</remote> <ejb-class>com.titan.ship.ShipBean</ejb-class> <persistence-type>Bean</persistence-type> <prim-key-class>com.titan.ship.ShipPK</prim-key-class> <reentrant>False</reentrant> <resource-ref> <description>DataSource for the Titan database</description> <res-ref-name>jdbc/titanDB</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> </entity> </enterprise-beans> <assembly-descriptor> <security-role> <description> This role represents everyone who is allowed full access to the Ship bean. </description> <role-name>everyone</role-name> </security-role> <method-permission> <role-name>everyone</role-name> <method> <ejb-name>ShipBean</ejb-name> <method-name>*</method-name> </method> </method-permission> <container-transaction> <method> <ejb-name>ShipBean</ejb-name> <method-name>*</method-name> </method> <trans-attribute>Required</trans-attribute> </container-transaction> </assembly-descriptor> </ejb-jar>
Save the Ship bean’s XML deployment descriptor into the
com/titan/ship
directory as
ejb-jar.xml
and package it a JAR file:
dev % jar cf ship.jar com/titan/ship/*.class META-INF/ejb-jar.xml F:..dev>jar cf ship.jar com itanship*.class META-INFejb-jar.xml
To test this new bean, use the client application that we used to
test the container-managed Ship bean. You will probably need to
change the names and IDs of the ships you create; otherwise, your
inserts will cause a database error. In the SQL statement that we
defined to create the SHIP
table, we placed a
primary key restriction on the ID column of the
SHIP
table so that only unique ID values can be
inserted. Attempts to insert a record with a duplicate ID will cause
an SQL-Exception
to be thrown.
To deploy the bean-managed
ShipBean
, you can reuse the
MakeDD
application we developed earlier to create
a serialized DeploymentDescriptor
. You will need
to comment out the section that sets the container-managed fields in
the EntityDescriptor
, as follows:
/* COMMENTED OUT FOR BEAN-MANAGED SHIP BEAN ***************************************** Class beanClass = ShipBean.class; Field [] persistentFields = new Field[4]; persistentFields[0] = beanClass.getDeclaredField("id"); persistentFields[1] = beanClass.getDeclaredField("name"); persistentFields[2] = beanClass.getDeclaredField("capacity"); persistentFields[3] = beanClass.getDeclaredField("tonnage"); shipDD.setContainerManagedFields(persistentFields); ************************ */
Not specifying any container-managed fields tells the EJB deployment tools that this bean uses bean-managed persistence.
We also need to add some code to set the
environment properties for the
ShipBean
:
Properties props = new Properties(); props.put("jdbcURL","jdbc:
subprotocol:subname");
shipDD.setEnvironmentProperties(props);
This code defines the property "jdbcURL"
, which
holds the part of the URL that we need to get a database connection.
Replace the URL in this example with whatever is appropriate for the
EJB server and JDBC driver that you are using. Our bean will be able
to access the properties defined here through the
Entity-Context
and use this URL to get a database
connection.
You will need to consult your EJB vendor’s documentation to determine what JDBC URL is needed for your specific EJB server and database combination. BEA’s WebLogic Server, for example, uses a pooled driver that is accessed using the JDBC URL, jdbc:weblogic:jts:ejbPool. Other EJB servers and database combinations will use different JDBC URLs.
After running MakeDD
to generate the deployment
descriptor, use the JAR utility to archive the Ship bean for
deployment. Archiving this version of the Ship bean is no different
than archiving the earlier version.
To test this new bean, use the client application that we used to
test the container-managed Ship bean. You will probably need to
change the names and IDs of the ships you create; otherwise, your
inserts will cause a database error. In the SQL statement that we
defined to create the SHIP
table, we placed a
primary key restriction on the ID column of the
SHIP
table so that only unique ID values can be
inserted. Attempts to insert a record with a duplicate ID will cause
an
SQL-Exception
to
be
thrown.
[19] Containers that use object-to-relational mapping tools in bean-managed persistence can mitigate this disadvantage.
[20] The remainder of this section appeared first in jGuru’s Server-Side Java column “Create forward-compatible beans in EJB, Part 2” in JavaWorld (http://www.javaworld.com/javaworld/jw-01-2000/jw-01-ssj-ejb2.html ).
[21] A similar
RoleIdentity
class was originally defined by Jian
Lin in a post to the ejb-interest mailing list on September 24,
1999.