Entity beans can be more difficult to understand than session beans because the EJB container does more work behind the scenes. Therefore, as we present our example Customer EJB, we'll show you the entity bean code and explain the services that the EJB container provides.
Our entity bean example consists of a Customer EJB with Bean-Managed Persistence (BMP). It provides persistence for the CustomerVO data we used in the previous chapter (see Listing 5.1 on page 143). With this capability, our J2EE shopping application can now identify customers from a database (and verify a customer's password), or create a new customer. We thus provide a true “login” process. The CustomerVO object includes a customer's name, password, and e-mail address. Our persistent customer will have these fields, as well as a CustomerID primary key and a boolean that indicates whether or not this customer has orders pending in our “virtual” shopping system. Figure 6-1 shows the database table for our Customer EJB and the structure of class CustomerModel that holds the persistent data inside the entity bean. At this time, our Customer EJB has no database “relationships” with any other persistent data.
Figure 6-2 shows the architectural overview of these components. Our application's customer identity verification and new customer creation services are provided by both a Customer EJB entity bean and a CustomerSession EJB stateless session bean. The CustomerSession EJB provides the business logic and the Customer EJB provides the data persistence. The diagram also includes components introduced in previous chapters. These components are unchanged, but we want to show how everything fits together, as well as emphasize their reusability! Note that CustomerSession EJB communicates with Customer EJB through local interfaces. The Customer EJB entity bean uses CustomerDAO (DAO pattern) to implement its persistence.
Design Guideline
Because Customer EJB uses bean-managed persistence, it makes sense to implement vendor independent database access. Hence, we use a DAO to implement JDBC calls to the database. Refer to “Data Access Object Pattern” on page 119 in Chapter 4 for more information. |
Entity bean structure is similar to session bean structure. The client accesses the bean initially through the home (or local home) interface, using either one of the finder methods or a create() call. Either of these returns a remote or local object (or a collection of objects). The remote or local interface object provides the data access methods (which are business methods from an entity bean's point of view). As with the session bean, the EJB container intercepts these calls and forwards them to the bean implementation class.
Figure 6-3 shows the classes and interfaces of our Customer entity bean with local interfaces. (To avoid cluttering the diagram, we show the local and local home interfaces only.) The local home interface holds the create() methods, the finder methods, and any home methods. The local interface contains the business methods. The bean implementation class implements the EntityBean interface and contains the code for the EJB methods and the business methods.
For Customer EJB, we implement both local and remote interfaces. As you will see later, the remote interface implementation helps us understand the system with an application test client. We use the local and local home interfaces when accessing the entity bean with CustomerSession EJB.
Design Guideline
The home interface contains create methods, finder methods, and home methods. All finder methods begin with “findBy,” create methods must be called create(), and the home methods have no naming restrictions in the home or local home interface.
Listing 6.1 shows the source for the home interface, CustomerHome.java. Note that interface CustomerHome extends EJBHome and provides remote access to its methods. We therefore include RemoteException in all of the methods' throw clauses. Furthermore, method create() must specify CreateException, and all of the finder methods must specify FinderException.
// CustomerHome.java import java.util.Collection; import javax.ejb.*; import java.rmi.RemoteException; public interface CustomerHome extends EJBHome { public Customer create(String name, String password, String email) throws RemoteException, CreateException; // Finder methods public Customer findByPrimaryKey(String customerID) throws RemoteException, FinderException; public Collection findByCustomerName(String name) throws RemoteException, FinderException; public Collection findAll() throws RemoteException, FinderException; // Home methods // Return the number of Customers in the database public int getTotalCustomers() throws RemoteException; } |
A finder method may return either a single object or a collection (an object implementing abstract interface Collection), depending on whether the finder method anticipates that more than one database record will satisfy the corresponding database query. For example, findByPrimaryKey() always returns a single object. A finder method that returns a single object throws an ObjectNotFoundException (a subclass of FinderException) when the database query fails to find the requested record. A finder method that returns a collection simply returns an empty collection if it does not find any objects (and no exception is thrown). Note that findByCustomerName() and findAll() both return collections.
Method getTotalCustomers() is a home method, which we access through the home interface, as follows.
// Get a home interface object CustomerHome home = (CustomerHome)PortableRemoteObject.narrow(objref, CustomerHome.class); // Access home method int count = home.getTotalCustomers();
Home methods typically perform database-wide operations. They never invoke business methods that are dependent on a specific entity bean instance.
Why doesn't the create() method include either the boolean variable for the ordersPending field or the primary key field in its signature? The answer is that when we create a customer, it doesn't yet have an order associated with it. Since the ordersPending boolean is always false at creation time, the bean implementation code can initialize this boolean to false. Furthermore, the bean implementation code generates the primary key, so we exclude it from create()'s signature as well.
Design Guideline
Listing 6.2 shows the source for the local home interface, CustomerLocalHome.java. You'll note that its methods are the same as the home interface, but it extends EJBLocalHome and we've removed all RemoteExceptions from the throw clauses. Also, findByPrimaryKey() and create() return objects implementing the CustomerLocal interface instead of the remote interface, Customer. Finders findByCustomerName() and findAll() return collections of CustomerLocal objects.
// CustomerLocalHome.java import java.util.Collection; import javax.ejb.*; public interface CustomerLocalHome extends EJBLocalHome { public CustomerLocal create(String name, String password, String email) throws CreateException; // Finder methods public CustomerLocal findByPrimaryKey(String customerID) throws FinderException; public Collection findByCustomerName(String name) throws FinderException; public Collection findAll() throws FinderException; // Home method public int getTotalCustomers(); } |
The remote and local interfaces specify the business methods for the entity bean. With session beans, these business methods carry out a business process. With entity beans, however, business methods typically get or update one or more fields in a database record associated with the entity bean. The business logic ensures that the new data values make sense. More typically, however, the entity bean simply accepts or retrieves data. It leaves any data consistency checking to a separate business component, such as a “business-enabled” session bean. This allows the business logic to reside in the controlling session bean.
Design Guideline
In the Customer EJB, the remote interface contains methods for setting and getting the entity bean's persistent variables; i.e., those variables that are stored in the database. Note that there is no setter for the primary key (customerID), since primary keys never change. All methods specify RemoteException in their throws clauses. Like session beans, the entity bean's remote interface extends EJBObject. Listing 6.3 contains the source for Customer.java.
// Customer.java import javax.ejb.EJBObject; import java.rmi.RemoteException; public interface Customer extends EJBObject { public String getCustomerID() throws RemoteException; public String getName() throws RemoteException; public void setName(String newName) throws RemoteException; public String getPassword() throws RemoteException; public void setPassword(String newPassword) throws RemoteException; public String getEmail() throws RemoteException; public void setEmail(String newEmail) throws RemoteException; public boolean getOrdersPending() throws RemoteException; public void setOrdersPending(boolean pending) throws RemoteException; } |
We now show you Customer EJB's local interface, CustomerLocal. It contains the same methods as the remote interface without RemoteException in the throw clauses. And, CustomerLocal extends EJBLocalObject. Listing 6.4 contains the source for CustomerLocal.java.
// CustomerLocal.java import javax.ejb.EJBLocalObject; public interface CustomerLocal extends EJBLocalObject { public String getCustomerID(); public String getName(); public String getPassword(); public void setPassword(String newPassword); public String getEmail(); public void setEmail(String newEmail); public boolean getOrdersPending(); public void setOrdersPending(boolean pending); } |
Before we delve into the bean implementation code, let's explore what the EJB container does behind the scenes. First we'll show you how to create an object that implements the home interface. Once we have the home interface, we invoke either a create() method or a finder method to instantiate objects that implement the remote interface. With the remote interface object, we can call the business methods.
All business methods, even if they only perform a data lookup, must execute within a transaction in order for the EJB container to ensure data integrity between the in-memory entity bean and the database. A key concept in entity beans is that the container wraps each transactional business method invocation with calls to ejbLoad() and ejbStore(). This guarantees that the in-memory persistent variables are always synchronized with the database.
ejbLoad(); // read data from database business_method(); // possibly modify data ejbStore(); // update data in database
The ejbLoad() method reads the database and refreshes the entity bean's persistent variables. This step is crucial. Because the EJB container may instantiate more than one entity bean per database record, we must refresh the persistent variables before each business method in case another client changes their values. After business_method() completes, ejbStore() writes the persistent variables back to the database. Calls to ejbStore() work well with transactional business methods that modify the persistent variables. But what if a business method only reads them? In this situation, ejbStore() does not need to update the database.
The use of a dirty flag in ejbStore() helps optimize entity bean behavior. In the entity bean, the bean developer includes a dirty flag as an instance variable of the bean. It is initially set to false in either ejbLoad() (when the initial values come from the database) or in ejbCreate() (when the initial values come from the client). The business methods that modify persistent data set the dirty flag to true. Those business methods that only read persistent data do not. When the EJB container calls ejbStore(), this method checks the dirty flag and updates the database only if the persistent variables have changed.
We'll take a closer look at the dirty flag implementation when we present the code for class CustomerBean later in the chapter (see Listing 6.10 on page 233 through Listing 6.13 on page 237.)
Figure 6-4 shows a sequence diagram for obtaining an object that implements the home interface and using it to call method create(). First, we invoke the narrow() method of PortableRemoteObject and invoke CustomerHome's create() method (passing appropriate values for name, password, and e-mail address). The EJB container intercepts the call, instantiates an instance of CustomerBean (the bean implementation class), and invokes the bean's ejbCreate() method. Method ejbCreate() inserts values for the new Customer into the database. These are the JDBC calls invoked through methods in the CustomerDAO implementation class. The EJB container then invokes ejbPostCreate() followed by ejbStore(). Note that ejbStore() invokes the actual DAO method to update the database only if the dirty flag (instance variable boolean dirty) is true.
Figure 6-5 shows a sequence diagram for obtaining a Customer entity bean with a finder method followed by calls to business methods setPassword() and getName(). In the client's call to findByPrimaryKey(), the container instantiates a CustomerBean object. Method ejbFindByPrimaryKey() returns a primary key to the EJB container. Since the EJB container oversees the life cycle of the entity bean, it may be able to use an already-existing Customer entity bean instance to return to the client. Thus, the EJB container caches entity beans in memory.
Once the client has an object that implements the remote interface (Customer), it can invoke business methods. Let's begin with the call to setPassword(). Note that the EJB container first invokes method ejbActivate() followed by ejbLoad() and the required database select statement. Method setPassword() updates the persistent password field and sets the dirty flag to true. When the EJB container invokes ejbStore(), this executes the database update statement which synchronizes the database with the entity bean.
Likewise, when the client invokes business method getName(), the EJB container calls ejbLoad(). (A call to ejbActivate() is not necessary here, since the bean is already activated.) The EJB container then invokes CustomerBean's getName() followed by ejbStore(). Here, we avoid executing the database update statement because the dirty flag is false. (Method getName() does not set it.)
Design Guideline
Rather than place JDBC calls in private methods inside the bean implementation class, we'll use a DAO to access the database. This helps isolate database dependent code and provides a flexible way to deploy alternate database implementations. The CustomerDAO pattern is equivalent to the MusicDAO implementation you've already seen in “Data Access Object Pattern” on page 119. We'll use the Cloudscape database (part of the J2EE reference implementation) for the underlying database. Figure 6-6 provides a class diagram illustrating the class relationships.
The Customer EJB and CustomerDAO must work together to provide access to the database. To do this, we introduce class CustomerModel, which contains fields that constitute the persistent data of a Customer entity bean. Class CustomerModel is a Value Object, and we use it to transmit the persistent fields between the Customer EJB implementation class and the CustomerDAO implementation class. Note that Customer EJB and the DAO implementation classes are the only two components that use class CustomerModel. Listing 6.5 contains the source for CustomerModel.java.
// CustomerModel.java public class CustomerModel { // Encapsulation of Customer EJB persistent variables private String customerID; // primary key private String name; private String password; private String email; private boolean ordersPending; public CustomerModel(String customerID, String name, String password, String email, boolean ordersPending) { setCustomerID(customerID); setName(name); setPassword(password); setEmail(email); setOrdersPending(ordersPending); } public void setCustomerID(String id) { this.customerID = id; } public void setName(String name) { this.name = name; } public void setPassword(String password) { this.password = password; } public void setEmail(String email) { this.email = email; } public void setOrdersPending(boolean ordersPending) { this.ordersPending = ordersPending; } public String getCustomerID() { return customerID; } public String getName() { return name; } public String getPassword() { return password; } public String getEmail() { return email; } public boolean getOrdersPending() { return ordersPending; } } |
Next we need a system exception class to propagate SQLExceptions from the CustomerDAO to the Customer EJB implementation class. Listing 6.6 contains the source for CustomerDAOSysException.java.
// CustomerDAOSysException.java // System Exception import java.lang.RuntimeException; // CustomerDAOSysException extends standard // RuntimeException. // Thrown by CustomerDAO subclasses when there is an // unrecoverable system error (typically SQLException) public class CustomerDAOSysException extends RuntimeException { public CustomerDAOSysException(String msg) { super(msg); } public CustomerDAOSysException() { super(); } } |
The CustomerDAO interface defines the database access methods required by the Customer EJB bean implementation class. Listing 6.7 contains the source for CustomerDAO.java interface.
// CustomerDAO.java import java.util.Collection; import javax.ejb.CreateException; public interface CustomerDAO { // generate primary key public String dbGetKey(); // inserts new CustomerModel record in database public void dbInsertCustomer(CustomerModel data) throws CreateException; // find specified primary key public boolean dbSelectByPrimaryKey(String primaryKey) throws CustomerDAOSysException; // the following 'dbSelect' methods // return a collection of primary keys public Collection dbSelectByCustomerName(String name) throws CustomerDAOSysException; public Collection dbSelectAll() throws CustomerDAOSysException; // load CustomerModel data from database public CustomerModel dbLoadCustomer(String primaryKey) throws CustomerDAOSysException; // store CustomerModel data to database public void dbStoreCustomer(CustomerModel data) throws CustomerDAOSysException; // delete Customer data from database with primary key public void dbRemoveCustomer(String primaryKey) throws CustomerDAOSysException; // database access method required by home method public int dbCountTotalCustomers() throws CustomerDAOSysException; } |
The CustomerDAOCloudscape class is the implementation of CustomerDAO for the Cloudscape database. You'll note that besides containing the JDBC database access code, it also calls the Cloudscape KeyGen class to obtain a primary key.
Creating a unique primary key is a nontrivial problem, since multiple clients may attempt to create new Customer entity beans simultaneously. The Cloudscape utility KeyGen.getUniversalKeyStringValue() generates primary keys unique to the database. This routine is proprietary, however. If portability is an issue (and it usually is), techniques that dole out blocks of primary keys per request provide an efficient way to generate primary keys. The current value can be stored in a database. Furthermore, many database systems support an auto-generated primary key through a stored procedure.
Design Guideline
The CustomerDAOCloudscape load and store routines encapsulate Customer persistent data fields into a CustomerModel value object. These methods all exhibit the same structure we used in the MusicDAO implementation class: we call getConnection() prior to each database access and use a finally block to release JDBC resources (closing statements and result sets and releasing the connection). Let's discuss several of these methods.
The dbInsertCustomer() method (see page 223) is called from ejbCreate() to insert a new Customer in the database. It builds a database insert command from its arguments. An error during this operation generates an SQLException that we catch and rethrow as a CreateException. Attempting to insert a record with a duplicate primary key causes the database software to throw an SQLException.
We invoke method dbSelectByPrimaryKey() (see page 224) from ejbFindByPrimaryKey(). This method specifies a query to select a record with a specific primary key as follows:
select CustomerID from Customers where CustomerID = ?
We don't read the data in the result set, but simply invoke ResultSet's next() method. If this method returns true, the current row is valid; false means there are no more rows. Thus, if our query returns false, then a record matching the specified primary key does not exist in the database.
We invoke method dbSelectByCustomerName() (see page 225) from ejbFindByCustomerName(). Here we specify the following query to select all records that match the specified name and fetch only the primary key from the database record.
select distinct CustomerID from Customers where Name = ?
Note that this method builds an ArrayList of primary keys, which we return to the finder method (and to the container). It is the EJB container's job to return the matching entity beans to the client.
Finally, look at method dbLoadCustomer() (see page 227) which we invoke from ejbLoad(). Here our select statement requests the named fields from the record that matches the specified primary key.
Select Name, Password, Email, OrdersPending from Customers Where CustomerID = ?
We then extract each value from the result set and assign it to the matching persistent variable in value object CustomerModel.
Listing 6.8 contains the source for CustomerDAOCloudscape.java.
// CustomerDAOCloudscape.java import java.util.*; import javax.ejb.*; import javax.naming.*; import javax.sql.DataSource; import java.sql.*; import COM.cloudscape.util.KeyGen; // Implements CustomerDAO for Cloudscape database. public class CustomerDAOCloudscape implements CustomerDAO { private Connection con = null; private DataSource datasource = null; public CustomerDAOCloudscape() throws CustomerDAOSysException { String dbName = "java:comp/env/jdbc/CustomerDB"; try { InitialContext ic = new InitialContext(); datasource = (DataSource) ic.lookup(dbName); } catch (NamingException ex) { throw new CustomerDAOSysException( "Cannot connect to database: " + dbName + ": " + ex.getMessage()); } } // Cloudscape propriety routine to generate // primary key // called by ejbCreate() public String dbGetKey() { return KeyGen.getUniversalKeyStringValue(); } // Obtain a JDBC Database connection private void getConnection() throws CustomerDAOSysException { try { con = datasource.getConnection(); } catch (SQLException ex) { throw new CustomerDAOSysException( "Exception during DB Connection: " + ex.getMessage()); } } // Release JDBC Database connection private void disConnect() throws CustomerDAOSysException { try { if (con != null) con.close(); } catch (SQLException ex) { throw new CustomerDAOSysException( "SQLException during DB close connection: " + ex.getMessage()); } } private void closeStatement(PreparedStatement s) throws CustomerDAOSysException { try { if (s != null) s.close(); } catch (SQLException ex) { throw new CustomerDAOSysException( "SQL Exception during statement close " + ex.getMessage()); } } private void closeResultSet(ResultSet rs) throws CustomerDAOSysException { try { if (rs != null) rs.close(); } catch (SQLException ex) { throw new CustomerDAOSysException( "SQL Exception during result set close " + ex.getMessage()); } } // called by ejbCreate() public void dbInsertCustomer(CustomerModel newData) throws CreateException { String insertStatement = "insert into Customers values " + "( ? , ? , ? , ?, ? )"; PreparedStatement prepStmt = null; try { getConnection(); prepStmt = con.prepareStatement(insertStatement); prepStmt.setString(1, newData.getCustomerID()); prepStmt.setString(2, newData.getName()); prepStmt.setString(3, newData.getPassword()); prepStmt.setString(4, newData.getEmail()); prepStmt.setBoolean(5, newData.getOrdersPending()); prepStmt.executeUpdate(); } catch (SQLException ex) { throw new CreateException(ex.getMessage()); } finally { closeStatement(prepStmt); disConnect(); } } // called by ejbFindByPrimaryKey() public boolean dbSelectByPrimaryKey(String primaryKey) throws CustomerDAOSysException { String selectStatement = "select CustomerID " + "from Customers where CustomerID = ? "; PreparedStatement prepStmt = null; ResultSet rs = null; boolean result = false; try { getConnection(); prepStmt = con.prepareStatement(selectStatement); prepStmt.setString(1, primaryKey); rs = prepStmt.executeQuery(); result = rs.next(); } catch (SQLException ex) { throw new CustomerDAOSysException( "dbSelectByPrimaryKey: SQL Exception caught " + ex.getMessage()); } finally { closeResultSet(rs); closeStatement(prepStmt); disConnect(); } return result; } // called by ejbFindByCustomerName() public Collection dbSelectByCustomerName(String name) throws CustomerDAOSysException { System.out.println("dbSelectByCustomerName for " + name); String selectStatement = "select distinct CustomerID " + "from Customers where Name = ? "; PreparedStatement prepStmt = null; ResultSet rs = null; ArrayList a = null; try { getConnection(); prepStmt = con.prepareStatement(selectStatement); prepStmt.setString(1, name); rs = prepStmt.executeQuery(); a = new ArrayList(); while (rs.next()) { a.add(new String(rs.getString(1))); } } catch (SQLException ex) { throw new CustomerDAOSysException( "dbSelectByCustomerName: SQL Exception caught " + ex.getMessage()); } finally { closeResultSet(rs); closeStatement(prepStmt); disConnect(); } return a; } // called by ejbFindAll() public Collection dbSelectAll() throws CustomerDAOSysException { String selectStatement = "select distinct CustomerID " + "from Customers "; PreparedStatement prepStmt = null; ResultSet rs = null; ArrayList a = null; try { getConnection(); prepStmt = con.prepareStatement(selectStatement); rs = prepStmt.executeQuery(); a = new ArrayList(); while (rs.next()) { a.add(new String(rs.getString(1))); } } catch (SQLException ex) { throw new CustomerDAOSysException( "dbSelectAll: SQL Exception caught " + ex.getMessage()); } finally { closeResultSet(rs); closeStatement(prepStmt); disConnect(); } return a; } // called by ejbLoad() public CustomerModel dbLoadCustomer(String primaryKey) throws CustomerDAOSysException, NoSuchEntityException { String selectStatement = "Select Name, Password, Email, OrdersPending " + "from Customers " + "Where CustomerID = ? "; PreparedStatement prepStmt = null; ResultSet rs = null; String name = null; String password = null; String email = null; boolean ordersPending = false; try { getConnection(); prepStmt = con.prepareStatement(selectStatement); prepStmt.setString(1, primaryKey); rs = prepStmt.executeQuery(); if (rs.next()) { // Read the data from the resultset name = rs.getString(1); // Name password = rs.getString(2); // Password email = rs.getString(3); // Email ordersPending = rs.getBoolean(4); // OrdersPending } else { throw new NoSuchEntityException( "Row for customerID " + primaryKey + " not found in database."); } System.out.println("dbLoadCustomer: " + name); // Create CustomerModel to return to CustomerEJB return new CustomerModel(primaryKey, name, password, email, ordersPending); } catch (SQLException ex) { throw new CustomerDAOSysException( "dbLoadCustomer: SQL Exception caught " + ex.getMessage()); } finally { closeResultSet(rs); closeStatement(prepStmt); disConnect(); } } // called by ejbStore() public void dbStoreCustomer(CustomerModel data) throws CustomerDAOSysException { System.out.println("dbStoreCustomer: " + data.getName()); String updateStatement = "update Customers set " + "Name = ? , " + "Password = ? , " + "Email = ? , " + "OrdersPending = ? " + "where CustomerID = ?"; PreparedStatement prepStmt = null; try { getConnection(); prepStmt = con.prepareStatement(updateStatement); // Grab values from persistent fields // to store in database prepStmt.setString(1, data.getName()); prepStmt.setString(2, data.getPassword()); prepStmt.setString(3, data.getEmail()); prepStmt.setBoolean(4, data.getOrdersPending()); prepStmt.setString(5, data.getCustomerID()); int rowCount = prepStmt.executeUpdate(); if (rowCount == 0) { throw new SQLException( "Storing row for CustomerID " + data.getCustomerID() + " failed."); } } catch (SQLException ex) { throw new CustomerDAOSysException( "dbStoreCustomer: SQL Exception caught " + ex.getMessage()); } finally { closeStatement(prepStmt); disConnect(); } } // called by ejbRemove() public void dbRemoveCustomer(String primaryKey) throws CustomerDAOSysException { String removeStatement = "delete from Customers where CustomerID = ?"; PreparedStatement prepStmt = null; try { getConnection(); prepStmt = con.prepareStatement(removeStatement); prepStmt.setString(1, primaryKey); int result = prepStmt.executeUpdate(); if (result == 0) { throw new SQLException("Remove for CustomerID " + primaryKey + " failed."); } } catch (SQLException ex) { throw new CustomerDAOSysException( "dbRemoveCustomer: SQL Exception caught " + ex.getMessage()); } finally { closeStatement(prepStmt); disConnect(); } } // Use a Select query to count // the number of records in the database // called by ejbHomeGetTotalCustomers() public int dbCountTotalCustomers() throws CustomerDAOSysException { String selectStatement = "select distinct CustomerID " + "from Customers "; PreparedStatement prepStmt = null; ResultSet rs = null; int count = 0; try { getConnection(); // Request a resultset that is scrollable // so we can easily count rows prepStmt = con.prepareStatement(selectStatement, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE); rs = prepStmt.executeQuery(); // go to the last row // and get its row number rs.last(); count = rs.getRow(); } catch (SQLException ex) { throw new CustomerDAOSysException( "dbCountTotalCustomers: SQL Exception caught " + ex.getMessage()); } finally { closeResultSet(rs); closeStatement(prepStmt); disConnect(); } return count; } } // CustomerDAOCloudscape |
JDBC 2.0 API
The CustomerDAOFactory class performs a JNDI lookup to obtain the name of the class it should instantiate for the CustomerDAO. We specify the name of the class in the deployment descriptor. Listing 6.9 contains the source for CustomerDAOFactory.java.
// CustomerDAOFactory.java import javax.naming.NamingException; import javax.naming.InitialContext; public class CustomerDAOFactory { // Instantiate a subclass that implements the abstract // CustomerDAO interface. // Obtain subclass name from the deployment descriptor public static CustomerDAO getDAO() throws CustomerDAOSysException { CustomerDAO customerDAO = null; String customerDAOClass = "java:comp/env/CustomerDAOClass"; String className = null; try { InitialContext ic = new InitialContext(); // Lookup value of environment entry for // DAO classname // Value is set in deployment descriptor className = (String) ic.lookup(customerDAOClass); // Instantiate implementation class // for the CustomerDAO interface customerDAO = (CustomerDAO) Class.forName(className).newInstance(); } catch (Exception ex) { throw new CustomerDAOSysException( "CustomerDAOFactory.getDAO: " + "NamingException for <" + className + "> DAO class : " + ex.getMessage()); } return customerDAO; } } // CustomerDAOFactory |
The CustomerBean implementation class contains the implementation of the business methods as well as all the finder methods, home methods, and EJB methods. To organize the presentation of the code, we'll display it in sections as shown in Table 6.2.
Listing and Page | Contents |
---|---|
Listing 6.10 on page 233 | Persistent Variables and Business Methods |
Listing 6.11 on page 235 | Finder Methods |
Listing 6.12 on page 237 | Home Method |
Listing 6.13 on page 237 | EJB Methods |
Since we're using a DAO, the database access code has all been gathered into the CustomerDAO implementation class, CustomerDAOCloudscape, which you've just seen.
Listing 6.10 contains the persistent variables and business methods. Note the simplicity of the business methods; they update the persistent data fields as if the in-memory data fields and the records in the database were the same! We can write the business methods this way, because the EJB container calls ejbLoad() before each business method and ejbStore() afterwards. Note also that the business methods that modify persistent variables set the dirty flag.
// CustomerBean.java import java.sql.*; import javax.sql.*; import java.util.*; import javax.ejb.*; import javax.naming.*; public class CustomerBean implements EntityBean { // Persistent data CustomerModel customerData; // Variable for DAO access private CustomerDAO customerDAO = null; // EJB variables private EntityContext context; private boolean dirty; // Business methods public String getCustomerID() { return customerData.getCustomerID(); } public String getName() { return customerData.getName(); } public void setName(String name) { customerData.setName(name); dirty = true; } public String getPassword() { return customerData.getPassword(); } public void setPassword(String newPassword) { customerData.setPassword(newPassword); dirty = true; } public String getEmail() { return customerData.getEmail(); } public void setEmail(String newEmail) { customerData.setEmail(newEmail); dirty = true; } public boolean getOrdersPending() { return customerData.getOrdersPending(); } public void setOrdersPending(boolean pending) { customerData.setOrdersPending(pending); dirty = true; } |
The finder methods call methods in the CustomerDAO interface to carry out the database select queries. Listing 6.11 contains the finder methods from file CustomerBean.java. Method ejbFindByPrimaryKey() returns the primary key; all the other finder methods return a collection of primary keys (or an empty collection if no database records are returned in the select query).
Again, the EJB container's role is significant here and helps explain the implementation code for finder methods. When a finder method returns a primary key, the EJB container matches the entity bean (possibly already instantiated in the container's cache of active entity beans) with the primary key. The EJB container, then, can simply return the cached bean to the client. Thus, by having the implementation code return a primary key, the EJB container is able to optimize an entity bean's life cycle. If the entity bean is not already activated, the EJB container invokes the ejbActivate() method, which causes ejbLoad() to read the persistent data from the database into the newly activated entity bean.
// Finder methods // returns a primary key // or throws ObjectNotFoundException public String ejbFindByPrimaryKey(String primaryKey) throws FinderException { boolean result; try { CustomerDAO dao = getDAO(); result = dao.dbSelectByPrimaryKey(primaryKey); } catch (CustomerDAOSysException ex) { ex.printStackTrace(); throw new EJBException("ejbFindByPrimaryKey: " + ex.getMessage()); } if (result) { return primaryKey; } else { throw new ObjectNotFoundException ("Row for id " + primaryKey + " not found."); } } // returns a Collection of primary keys // or an empty Collection if no objects are found public Collection ejbFindByCustomerName(String name) throws FinderException { System.out.println("ejbFindByCustomerName: name=" + name); Collection result; try { CustomerDAO dao = getDAO(); result = dao.dbSelectByCustomerName(name); } catch (CustomerDAOSysException ex) { ex.printStackTrace(); throw new EJBException("ejbFindByCustomerName: " + ex.getMessage()); } // if we don't find any matching objects, // result is an empty collection return result; } // returns a Collection of primary keys // or an empty Collection if no objects are found public Collection ejbFindAll() throws FinderException { System.out.println("ejbFindAll()"); Collection result; try { CustomerDAO dao = getDAO(); result = dao.dbSelectAll(); } catch (CustomerDAOSysException ex) { ex.printStackTrace(); throw new EJBException("ejbFindAll: " + ex.getMessage()); } // if we don't find any matching objects, // result is an empty collection return result; } |
Listing 6.12 contains the home method in CustomerBean.java. The client invokes this home method through the home (or local home) interface. While these methods access the database, they don't cause the EJB container to instantiate entity beans like the finder methods.
// EJB Home Method public int ejbHomeGetTotalCustomers() { System.out.println("ejbHomeGetTotalCustomers()"); int result; try { CustomerDAO dao = getDAO(); result = dao.dbCountTotalCustomers(); } catch (CustomerDAOSysException ex) { ex.printStackTrace(); throw new EJBException("ejbHomeGetTotalCustomers: " + ex.getMessage()); } return result; } |
Listing 6.13 contains the EJB methods. We've placed several System.out.println statements in some of the methods. This not only helps test our EJB implementation code, but also shows when the EJB container invokes these methods (since the client does not invoke them directly).
Note that ejbStore() does not call the DAO method dbStoreCustomer() unless the boolean flag dirty is true. Recall that only the business methods that modify persistent variables (the setters) set the dirty flag to true.
The EJB container calls ejbLoad() before executing a business method. If the entity bean is being activated (after a finder call) then ejbLoad() initializes the persistent variables. Otherwise, it just refreshes them. The call to the bean context's getPrimaryKey() method ensures that we obtain a valid primary key.
// EJB Methods // Call the Factory to get the // CustomerDAO implementation class only // when necessary; called by EJB Methods private CustomerDAO getDAO() throws CustomerDAOSysException { if (customerDAO == null) { customerDAO = CustomerDAOFactory.getDAO(); } return customerDAO; } public String ejbCreate(String name, String password, String email) throws CreateException { System.out.println("ejbCreate(), name=" + name); CustomerModel newCust = null; try { CustomerDAO dao = getDAO(); String newKey = dao.dbGetKey(); newCust = new CustomerModel( newKey, name, password, email, false); dao.dbInsertCustomer(newCust); } catch (CustomerDAOSysException ex) { throw new CreateException("ejbCreate: " + ex.getMessage()); } // initialize persistent data customerData = newCust; dirty = false; return customerData.getCustomerID(); } public void setEntityContext(EntityContext context) { this.context = context; System.out.println("setEntityContext()"); } public void unsetEntityContext() { System.out.println("unsetEntityContext()"); } // invoked by container just after activation public void ejbActivate() { System.out.println("ejbActivate()"); } // invoked by container just before passivation public void ejbPassivate() { customerDAO = null; } public void ejbLoad() { System.out.println("ejbLoad()"); try { CustomerDAO dao = getDAO(); // Get the primary key from the context // and refresh persistent data from database customerData = dao.dbLoadCustomer( (String)context.getPrimaryKey()); dirty = false; } catch (CustomerDAOSysException ex) { ex.printStackTrace(); throw new EJBException("ejbLoad: " + ex.getMessage()); } } public void ejbStore() { System.out.println("ejbStore(), name=" + customerData.getName()); try { if (dirty) { CustomerDAO dao = getDAO(); // Synchronize the database with // the in-memory values of the persistent data dao.dbStoreCustomer(customerData); dirty = false; } } catch (CustomerDAOSysException ex) { ex.printStackTrace(); throw new EJBException("ejbStore: " + ex.getMessage()); } } public void ejbPostCreate(String name, String password, String email) { System.out.println("ejbPostCreate(), name=" + name); } public void ejbRemove() { System.out.println("ejbRemove()"); try { CustomerDAO dao = getDAO(); dao.dbRemoveCustomer( (String)context.getPrimaryKey()); } catch (CustomerDAOSysException ex) { ex.printStackTrace(); throw new EJBException("ejbRemove: " + ex.getMessage()); } } } // CustomerBean |
Listing 6.14 contains some of the declarative information found in the deployment descriptor for the Customer EJB. Note that the descriptor specifies the interface names for both the local and remote interfaces, as well as the home and local home interfaces. Because Customer EJB is an entity bean, we must also specify its persistence type (we're using bean-managed persistence). Tag <prim-key-class> specifies the primary key type (here, java.lang.String). Under the <resource-ref> tag we specify the database resource name “jdbc/CustomerDB.” Under the <env-entry> tag we specify the CustomerDAO implementation class name “CustomerDAOCloudscape.”
<ejb-jar> <display-name>CustomerJAR</display-name> <enterprise-beans> <entity> <display-name>CustomerBean</display-name> <ejb-name>CustomerBean</ejb-name> <home>CustomerHome</home> <remote>Customer</remote> <local-home>CustomerLocalHome</local-home> <local>CustomerLocal</local> <ejb-class>CustomerBean</ejb-class> <persistence-type>Bean</persistence-type> <prim-key-class>java.lang.String</prim-key-class> <reentrant>False</reentrant> <env-entry> <env-entry-name>CustomerDAOClass</env-entry-name> <env-entry-type>java.lang.String</env-entry-type> <env-entry-value> CustomerDAOCloudscape</env-entry-value> </env-entry> <security-identity> <description></description> <use-caller-identity></use-caller-identity> </security-identity> <resource-ref> <res-ref-name>jdbc/CustomerDB</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> <res-sharing-scope>Shareable</res-sharing-scope> </resource-ref> </entity> </ejb-jar> |
18.191.233.43