6.2. Bean-Managed PersistenceBean-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.[4]
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. 6.2.1. Making the ShipBean a Bean-Managed EntityThe 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:
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 } } 6.2.2. Exception HandlingThere 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.
Exceptions have an impact on transactions and are fundamental to transaction processing. Exceptions are examined in greater detail in Chapter 8, "Transactions". 6.2.3. EntityContextAn 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, "Resource Management and the Primary Services", 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, "State and Sequence Diagrams" provides tables for each bean type describing which EJBContext methods can be invoked at what times. 6.2.4. EJB 1.1: EJBContextThe 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, "Entity Beans" 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, "Session Beans". The transactional methods (getUserTransaction(), setRollbackOnly(), getRollbackOnly()) are described in detail in Chapter 8, "Transactions". 6.2.5. EJB 1.0: EJBContextIn 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, "Obtaining a Connection to the Database", 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.[5] 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.[6] 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, "Transactions". 6.2.6. Obtaining a Connection to the DatabaseTitan'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. 6.2.6.1. EJB 1.1: Using JDBC in EJBTo 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. 6.2.6.2. EJB 1.0: Using JDBC in EJBTo 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. 6.2.7. The ejbCreate( ) MethodThe 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, "Transactions". 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); }
6.2.8. The ejbLoad( ) and ejbStore( ) MethodsThroughout 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.
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. 6.2.9. The ejbRemove( ) MethodIn 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(); } } } 6.2.10. ejbFind( ) MethodsIn bean-managed EntityBeans, the find methods in the home interface must match the ejbFind methods in the actual bean class. In other words, for each method named findlookup-type() in the home interface, there must be a corresponding ejbFindlookup-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 findlookup-type() to a corresponding ejbFindlookup-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 Ship findByPrimaryKey(ShipPK primaryKey) throws FinderException, RemoteException; public Enumeration findByCapacity(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 ShipPK ejbFindByPrimaryKey(ShipPK primaryKey) throws FinderException, RemoteException {} public Enumeration ejbFindByCapacity(int capacity) throws FinderException, RemoteException {} }
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.
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. 6.2.11. EJB 1.1: Deploying the Bean-Managed Ship BeanWith 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\titan\ship\*.class META-INF\ejb-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. 6.2.12. EJB 1.0: Deploying the Bean-Managed Ship BeanTo 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. Copyright © 2001 O'Reilly & Associates. All rights reserved. |
|