Thursday, January 18, 2007

Entity beans

Entity beans


EJB Tutorials


Entity beans overview


Session beans are all about functionality, application logic, and application state. Entity beans, on the other hand, represent persistent business entities or data. In a typical project requirements document, session beans would model the tasks or verbs, while entity beans would model the nouns: people, places, and things. While an entity bean usually represents a row from a database table, the datastore can be any persistence mechanism that the container or developer can manage, including incoming data from other applications (legacy connector applications, for example). Note that for read-only applications, entity beans may be overkill and raise performance issues. For these applications, you may want to consider direct access from the client or, more likely, a session bean using JDBC or other access methods as possible alternatives.

The specification mandates that there normally must be a create() / ejbCreate() method pair, an ejbPostCreate() method with matching arguments for each ejbCreate() method, and always a findByPrimaryKey() / ejbFindByPrimaryKey() method pair.

As mentioned earlier, create() / ejbCreate() and remove() / ejbRemove() have completely different functions with entity beans: create() creates persistent data, usually meaning the insertion of a row into a database table, and remove() deletes the persistent data. In this instance, the create() method will have at least a primary key argument and normally will have several data initialization arguments as well.

The findByPrimaryKey() method, like the create() method, returns a component interface and is the means by which a client obtains an entity bean to represent existing data. There may be multiple finder methods based on different values, each of which must return either a component interface or a Collection of component interfaces. Finder methods are named, again in matching sets, findByXXX() and ejbFindByXXX() with appropriate arguments. The ejbFindByPrimaryKey() method, like the ejbCreate() method, returns the primary key; other ejbFindXXX() methods may return a single primary key or a Collection of primary keys. The container handles the magic of converting from primary key to component interface instance in the create() method (but see the differences between BMP and CMP in Table 1 below).

A primary key is a unique identifier for a bean instance and is a key to the data, usually correlating to the primary key for a row in a table. A primary key must be an Object; primitives are not allowed. If no existing class can serve as a primary key or if the primary key consists of more than one variable (or column, if you prefer), then a custom class must be written to represent the key, with instance variables acting as composite key components. To work with both container-managed and bean-managed persistence, a primary key class must meet these guidelines:

  • The class must be public, and must have a public constructor with no parameters.
  • All fields in the primary key class must be declared as public.
  • For CMP, variable names in the class must be a subset of the names of container-managed fields.
  • The class must provide specific hashCode() and equals(Object obj) methods.

In addition to create, finder, and remove methods, an entity bean may expose home methods in its home interface. Home methods provide logic for operations that are not specific to a unique bean instance. This usually means an operation that requires working with more than one instance to obtain the desired result. Some examples include counting U.S. customers with a zip code of 80110; imposing a late charge on all accounts 30 days overdue; or calculating sales totals for a specific region. Home methods may have an arbitrary name in the home interface. In the bean implementation, the corresponding method must match arguments and have a name composed of the prefix ejbHome plus the name given in the home interface with the initial letter capitalized. For instance, a home interface home method defined as int countCustomers( String country, String postalCode ) would, in the bean implementation, be written as int ejbHomeCountCustomers( String country, String postalCode ).

Entity beans may be pooled in a manner similar to stateful session beans, but in this case passivation/activation also causes the data to be persisted to and read from the datastore, respectively; all other resources must be released and reacquired, as appropriate.

An entity bean provides shared access to its data, and it is possible that the same data may be modified by multiple clients. In this sort of scenario, it becomes critical that beans can work within the proven methodology of transactions (see What are transactions? ). The container provides for both bean-managed and container-managed demarcated transactions; the type of transaction management and transactional attributes are declared in the deployment descriptor. Entity beans must use container-managed transactions.

As previously noted, there are two methods of persistence available: bean-managed persistence (BMP), which is implemented by the developer, and container-managed persistence (CMP), which is implemented by the container. Both of these persistence methods are discussed in the following sections and illustrated in the example application. In each case, the container controls the timing of inserts, updates, refreshes/synchronization, and deletes.

While the specifics of BMP and CMP will be covered shortly, Table 1 (taken from Chapter 6 of the "J2EE Tutorial" -- see Resources ) is a useful quick reference for responsibility and coding differences between the two persistence types.

Table 1. BMP/CMP differences

Difference Container-managed Bean-managed
Class definition Abstract Not abstract
Database access calls Generated by tools Coded by developers
Persistent state Represented by virtual persistent fields Coded as instance variables
Access methods for persistent and relationship fields Required None
findByPrimaryKey method Handled by container Coded by developers
Customized finder methods Handled by container, but the developer must define the EJB QL queries Coded by developers
Select methods Handled by container None
Return value of ejbCreate Should be null Must be the primary key



The entity bean life cycle

The milestones in an entity bean's life cycle are Does Not Exist, Pooled, and Ready. In the Pooled state, the instance is not associated with any particular entity. In the Ready state, the instance has been drawn from the pool, associated with a specific entity, and assigned a unique object identity.

Figure 4. The javax.ejb.EntityBean interface
The javax.ejb.EntityBean interface

Every entity bean implements the javax.ejb.EntityBean interface. It should be evident from Figure 4 that there are several more life cycle methods here than there are in the SessionBean interface, and it is usually the case that much more attention must be paid to them, particularly for BMP.

To start an entity bean's life cycle, the container invokes newInstance(), then sends an EntityContext to the bean using the setEntityContext() callback method. At that point, the bean instance is moved into a bean pool. While in the pool, an instance may be used to execute the bean's ejbFindByPrimaryKey(), other finder methods, or ejbHomeXXX() methods. The instance does not move to the Ready state on finder method execution; for ejbHome methods, the bean is returned to the pool after execution.

An entity instance moves to the Ready state either when the ejbCreate() and ejbPostCreate() method pair is called, or when the ejbActivate() method is called. While in the Ready state, an instance has a specific object identity.

During a bean's life cycle, the container may call ejbLoad() and ejbStore() at any time and in any order it deems necessary to read from and write to the persistent datastore.

The container may also call ejbPassivate() at any time to move the instance back into the pool. Generally, the ejbStore() method will be invoked just prior to this call. As with any passivation method, resources should be released and reacquired upon activation.

An instance will be moved back to the pool if a transaction rollback occurs during ejbCreate() or ejbPostCreate() operations. ejbRemove() invocations will result in transition to the pool as well.

An instance is removed from the pool and goes to the Does Not Exist state immediately after the container calls unsetEntityContext(), so this is the time to perform any final clean-up operations.




What are transactions?

Transaction technology was developed primarily in the database area, and a transaction generally means one or more statements that comprise a logical unit of work (known as an LUW). In this context, the term transaction is used to mean an all-or-nothing series of operations; that is, everything should complete successfully or nothing should, in which case all state should revert to the state that existed prior to the transaction start. The classic example of a transaction involves withdrawing money from one bank account and depositing it in another. If only the withdrawal completes, money is lost. Debits and credits in a double-entry accounting system provide another example: both the debit and credit must complete; otherwise, it should appear that neither operation was attempted. Once a transaction begins, it continues until a commit (make the changes persistent) or a rollback (revert to the previous state) is issued.

You should understand that transaction handlers only control the results on persistent data. If your application's variables change during the course of a transaction that is eventually rolled back, you will have to write your own code to revert those variables to the previous state.

Transactions also allow concurrent, multiuser operations to appear as if they occurred serially. Isolation levels control the degree of concurrency and how transactions may affect each other. JDBC, the Java API for working with relational databases, defines the following isolation levels:

  • TRANSACTION_NONE
  • TRANSACTION_READ_COMMITTED
  • TRANSACTION_READ_UNCOMMITTED
  • TRANSACTION_REPEATABLE_READ
  • TRANSACTION_SERIALIZABLE

A client normally may set the isolation level or use the default provided by the JDBC driver. It is important, however, to be aware that entity beans have some transactional restrictions:

  • As mentioned in Entity beans overview, while with session and message-driven beans the developer may start and end transactions, entity beans must use container-managed transactions.

  • The EJB 2.0 specification does not define any method for container-managed transactions to set the isolation level. As a result, standard container-managed transactions use the driver default.

Other transactional systems may use different isolation levels or contexts. If the default level has to be changed to meet the application's requirements, the developer must write to resource manager-specific APIs to accomplish the task (JDBC is a resource manager).

EJB containers are required to support the Java Transaction API (JTA) and the Connector APIs, providing flat transaction support for all types of beans (see Controlling transactions ). The container may, but is not required to, use a Java Transaction Service (JTS) implementation for propagating transactions across servers. JTS is a Java binding of the CORBA Object Transaction Service (OTS) 1.1 specification, which uses the IIOP protocol. Regardless of the specific transaction technology used, the actual transaction operations are mostly transparent to the EJB developer.

For more information about transactions, JDBC, isolation levels, JTS, and JTA, see Chapter 17, "Support for Transactions," of the Enterprise JavaBean 2.0 specification, along with the other material in Resources.




Controlling transactions

Bean-managed transactions

Session and message-driven beans can declare that transactions are bean-managed and use the Java Transaction API directly for transaction demarcation and control. The interface for JTA is javax.transaction.UserTransaction, which is accessed through an EJBContext object. EJBContext is the parent of EntityContext, MessageDrivenContext, and SessionContext, which are obtained from the appropriate setXXXContext method. Once the UserTransaction object is obtained, the bean can invoke the begin(), commit(), and rollback() methods for transaction control. When using JTA, the developer must not use any resource manager methods for transactions (like JDBC commits, or rollbacks), but instead must use only UserTransaction methods.

Typical code to access and use the UserTransaction follows:

javax.transaction.UserTransaction ut = 
ejbContext.getUserTransaction();
// start the transaction
ut.begin();
// Do some work.
if( aGoodResult )
{
// commit the transaction
ut.commit();
}
else
{
// roll back the transaction
ut.rollback();
}

To enable bean-managed transactions (BMT), the developer must ensure that the deployment descriptor contains a <transaction-type>Bean</transaction-type> entry.

Container-managed transactions

Any bean can use container-managed transaction handling (CMT), which is completely declarative (there is no indication in the code that transactions are being used), by means of a <transaction-type>Container</transaction-type> entry in the deployment descriptor. The container's responsibility for CMT is declared in the deployment descriptor with a <trans-attribute></trans-attribute> pair enclosing one of the following transaction attributes:

  • NotSupported
  • Required
  • Supports
  • RequiresNew
  • Mandatory
  • Never

The Required attribute is a safe choice available to all EJB components: if a transaction is in progress when an operation is performed, the operation joins it; if not, a new transaction is started. The other attributes provide transactional variations, but are restricted to specific bean types.

A system exception will automatically cause a transaction rollback. This is not the case for application-level exceptions. If an application exception occurs that affects data integrity or otherwise causes the transaction to be invalid, under CMT the exception handler should invoke the EJBContext.setRollbackOnly() method. As noted in the javax.ejb.EJBContext API documentation, EJBContext.setRollbackOnly() will "mark the current transaction for rollback.... A transaction marked for rollback can never commit. Only enterprise beans with container-managed transactions are allowed to use this method."

For more information about javax.transaction.UserTransaction and transaction attributes, see Chapter 17, "Support for Transactions," in the EJB 2.0 specification, and the other JTA/JTS material in Resources.




Entity beans with bean-managed persistence (BMP)

The primary difference between BMP and CMP is that, under BMP, developers must write their own datastore access and persistence routines and respond appropriately to life cycle related calls and callbacks. The bean must provide a public, no-arg constructor for instance creation (invoked by the container).

The bean implements the following methods (for information on the sequence of events, see The entity bean life cycle ):

  • public void setEntityContext(EntityContext ec)
    The container normally invokes this method exactly once, just after instantiation, to pass the EntityContext. If the bean will use any EntityContext methods, this is the time to obtain the EntityContext and save the reference in an instance variable. In addition, it is a good time to get any resources that will be used for the bean's lifetime. Be aware that the bean has no object identity at this point and is about to be placed into the pool.

  • public void unsetEntityContext()
    This method is also normally invoked exactly once, just before the bean is terminated and sent into the Does Not Exist state. There is no object identity available during this method. This is the proper time to release any resources obtained and to perform any final clean-up.

  • public PrimaryKeyClass ejbCreate(...)
    This method is invoked sometime after the client calls a home interface create() method with the same arguments. Notice that the ejbCreate() method returns the primary key, while the (container-implemented) create() method returns a component instance.

    The ejbCreate() method normally validates any arguments, then the developer-written code inserts a row into a database (or performs other datastore create operations), and initializes the corresponding instance variables. In particular, the instance variable or variables representing the primary key must be set at this time.

    It is possible to write a bean without an ejbCreate() method, when the class deals only with existing data. In this case, there must be no create() methods exposed on the home interface.

    javax.ejb.CreateException and javax.ejb.DuplicateKeyException are standard, API-provided application exceptions that the method may throw.

  • public void ejbPostCreate(...)
    There must be a matching ejbPostCreate() method for each ejbCreate() method. The container calls this method after the ejbCreate() invocation so that the bean can perform any necessary post-creation operations. The entity object identity is now available.

    The method may throw the javax.ejb.CreateException standard, API-provided application exception.

  • public <primary key type or collection> ejbFind<METHOD>(...)
    These methods are called when the client calls a findXXX() method with matching arguments. An instance is selected from the pool, then returned to the pool upon method completion; therefore, any resources obtained during the method should be released. The developer-written code performs datastore search operations in these methods. The finder methods return a single primary key or a Collection of primary keys, depending on whether the argument is expected to return information about a unique entity or multiple entities. For a finder method that returns a Collection, an empty Collection should be returned if no data matching the input arguments is found.

    javax.ejb.ObjectNotFoundException may be thrown for a finder that should return a unique value. javax.ejb.FinderException may be thrown by any finder method that encounters an unexpected error.

  • public void ejbLoad()
    The container invokes this method when it determines that instance variables representing persistent data must be synchronized with the datastore. This method may be called at any time. The EntityContext methods are available at this time. The developer-written code accesses data in a row from a database (or performs other datastore retrieval operations) in this method.

  • public void ejbStore()
    The container invokes this method when it determines that the datastore must be synchronized with the bean instance variables that represent persistent data. This method may be called at any time. The developer-written code updates data in a database row (or performs other datastore update operations) in this method. The developer can rely on instance data to be current during this method call.

  • public void ejbRemove()
    This method is invoked some time after the client calls a remove() method. The EntityContext methods are available at this time. The developer-written code deletes a row from a database (or performs other datastore removal operations) in this method. On completion, the instance leaves the Ready state and is moved to the pool. Because of the transition, the bean state should be the same as it would be after an ejbPassivate() call. A simple way to ensure this result is to invoke ejbPassivate() after the deletion is performed (the container does not invoke ejbPassivate() after ejbRemove() ).

    The method may throw javax.ejb.RemoveException if an unexpected error occurs.

  • public void ejbActivate()
    The container invokes this method when assigning an object identity and moving the instance from the Pooled state to the Ready state. This is a good time to acquire (or reacquire) any resources needed for the specific identity instance. Persistent data should not be accessed from this method; the container will call ejbLoad() to obtain persisted data.

    ejbActivate() will be called for a previously passivated bean. The container will also transition a bean instance from the pool using this method to service an existing entity, which may not have gone through the create() method during the current program run. Therefore, to determine the identity of the data it represents, the bean must call getPrimaryKey() on its EntityContext and set the instance variables representing the primary key. Other EntityContext methods may be called as well. The information associated with the EntityContext will be valid until ejbPassivate() or ejbRemove() is called.

  • public void ejbPassivate()
    The container calls this method when it decides to transition the instance back to the pool. This could happen at any time. Any identity-specific resources should be released in this method. The EntityContext methods are available at this time. Persistent data should not be written in this method; the container will call ejbStore() to persist data.

    Because a bean instance can not be certain of the data it represents in the ejbActivate() method until it invokes getPrimaryKey(), and because the instance variables representing the rest of the entity data will be reset on the ensuing ejbLoad() call, it makes sense to set those variables to null here, so they will be eligible for garbage collection. Note that, although the ejbRemove() method transitions an instance to the pool on completion, ejbPassivate() is not called.

  • public type ejbHome<METHOD>(...)
    The important thing to understand about home business methods is that there is no specific object identity during method operations. An instance is selected from the pool and, on method completion, returned to the pool. As with ejbRemove(), then, the method should clean up after itself before being returned to the pool. There is no real difference between home methods written for BMP and those written for CMP.



The Rock Survey database

Now it's time to take a look at the database and tables that will store the Rock Survey data. For the tutorial, we'll use the Cloudscape database engine, which is included with the J2EE RI. To use a database within the J2EE environment, the following steps are necessary:

  1. A JDBC driver for the DBMS must be added to J2EE resources.

  2. The database to be used in an application must be created or available. The tutorial database will be named gsejbDB.

  3. A JDBC DataSource entry must be added to J2EE resources. The DataSource entry for gsejbDB will be jdbc/gsejbDB.

  4. The coded JNDI access and the real JNDI name must be reconciled. This step was actually done when the Alice application was deployed in Appendix C: About the example applications.

  5. Finally, the database engine (Cloudscape for the J2EE RI) must be started (see Appendix A: Installing and running J2EE ).

Steps 1 and 3 are accomplished with the J2EE Administration Tool, j2eeadmin, which is discussed in the J2EE documentation under "J2EE SDK Tools" (see Resources ). There is no standard for database creation, which is DBMS specific. For Cloudscape, step 1 is included in the J2EE RI as installed; Database creation is an option ( create=true ) in the JDBC URL for the connection, so it will be handled in step 3. For step 3, start J2EE, then use j2eeadmin from the command line as follows:

j2eeadmin -addJdbcDatasource jdbc/gsejbDB jdbc:cloudscape:rmi:gsejbDB;create=true

To ensure that the DataSource was added, use this command to see the current DataSource s:

j2eeadmin -listJdbcDatasource

The Rock Survey tables are minimal, with no analytical decomposition or normalization, to avoid distraction from the main focus of the tutorial, which is EJB technology. You should note that the "design" is not very flexible and should not be used as a model for production tables.

Table 2. The table for storing Survey data -- "SurveyBeanTable"

PK Name Type Description
Yes Type VARCHAR Survey Type = "ROCK"

SF INTEGER Single female count

MF INTEGER Married female count

SM INTEGER Single male count

MM INTEGER Married male count

USF INTEGER US female count

NUSF INTEGER Non-US female count

USM INTEGER US male count

NUSM INTEGER Non-US male count

Igneous INTEGER Igneous count

Metamorphic INTEGER Metamorphic count

Meteorite INTEGER Meteorite count

Sedimentary INTEGER Sedimentary count

InCountry INTEGER My country count

ExCountry INTEGER Another country count

Island INTEGER Island count

OS INTEGER Outer space count

Total INTEGER Total rocks

Pounds FLOAT Total rock weight

The "SurveyBeanTable" table holds all survey results except for last name entries, and will contain exactly one row. The initial set of columns may appear confusing at first, but they just contain the breakdowns by gender, marital status, and residence. The other columns should be self-explanatory. For the Rock Survey example, the "SurveyBeanTable" table will be managed by an entity bean using CMP.

Table 3. The table for storing survey names -- SurveyNames

PK Name Type Description
Yes Type VARCHAR Survey type = "ROCK"
Yes LastName VARCHAR Last name given for survey

Total INTEGER Count

Last name data is kept in a separate table that contains a row with a count for each distinct last name. As we'll see in The Survey Results application, the three most frequent last names are displayed with the other results, so we need a way to track them. For the Rock Survey example, the SurveyNames table will be managed by an entity bean using BMP.




Creating and resetting the Rock Survey tables

The program to create and load the tables, CreateRSTables, was actually included when the Alice application (see Appendix C: About the example applications ) was deployed, but could not run against the database until we added the DataSource in the previous section. After starting Cloudscape, you should be able to run it now by clicking the Go button next to "Reset the Database."

CreateRSTables is a helper class rather than a bean and uses JDBC for its task. The program first drops the "SurveyBeanTable" and SurveyNames tables from the database to remove any existing data, then creates them. Next, it loads predefined rows to give us something to work with. This is a straight JDBC application; there's nothing interesting about it from the J2EE/EJB technology standpoint, other than the way it retrieves a connection to the database. As shown in the code below, CreateRSTables retrieves a DataSource using a JNDI lookup for java:comp/env/jdbc/gsejbDB -- remember adding that entry in The Rock Survey database? It then obtains a connection from the DataSource:

    Connection con     = null;
DataSource ds = null;
InitialContext ic = null;
Statement stmt = null;
String sJNDIdbName = "java:comp/env/jdbc/gsejbDB";
try
{
ic = new InitialContext();
ds = (DataSource)ic.lookup( sJNDIdbName );
con = ds.getConnection();
stmt = con.createStatement();
}

...




Example: The Rock Survey, take 2

Now that we've actually created a database, and have some knowledge of entity beans and transactions, it's time to add data handling capability to the Rock Survey application. The first go-around (see Example: The Rock Survey, take 1 ) handled the user view and tracked the survey through the pages of the Web component, but never actually stored anything at the end of the survey. This portion of take 2 will add a BMP entity bean ( SurveyNamesBean ) to deal with data for the SurveyNames table. It will also expand the persist() method in RockSurveyBean to include a transaction manager, and control adding and updating names and name counts.

The primary key for the SurveyNames table (see Table 3 in The Rock Survey database ) consists of two Strings (SQL type VARCHAR ). You may recall from Entity beans overview that a primary key consisting of more than one element requires a custom primary key class. The SurveyNamesKey class represents the primary key for SurveyNames. The class consists of the two composite key Strings, a no-arg and a two-argument constructor, getXXX methods for the data, and equals() and hashCode() methods. The class is used by the SurveyNamesBean create() and findByPrimaryKey() methods.

The SurveyNamesBean class performs access and maintenance operations against the SurveyNames table. SurveyNamesBean is an entity bean with BMP, and implements all of the life cycle methods discussed in The entity bean life cycle and Entity beans with bean-managed persistence (BMP). In addition, it has getter methods for the Type, LastName, and Total columns. There is also an incrementTotal() method, which adds 1 to the current count. The class uses the same technique of connecting to the database that the CreateRSTables program uses (see Creating and resetting the Rock Survey tables ), and, as a BMP bean, uses standard SQL for access and updates. As recommended for EJB 2.0 components, the class exposes its methods with local home and component interfaces, as shown below:

// the local home interface for SurveyNamesBean
import javax.ejb.*;

public interface SurveyNamesLocalHome extends EJBLocalHome
{
public SurveyNamesLocal create( SurveyNamesKey snkIn )
throws CreateException;

public SurveyNamesLocal findByPrimaryKey(
SurveyNamesKey snkIn )
throws FinderException;

// no home business methods

} // end interface SurveyNamesLocalHome

// the local component interface for SurveyNamesBean
import javax.ejb.*;

public interface SurveyNamesLocal extends EJBLocalObject
{
public String getLastName();

public String getType();

public int getTotal();

public void incrementTotal();

} //end interface SurveyNamesLocal

SurveyNamesBean is managed by a new version of RockSurveyBean named RockSurvey2Bean. The primary additions are instance variables for the SessionContext and a javax.transaction.UserTransaction, along with variables to access SurveyNamesBean. The persist() method now actually does something, being expanded to handle the creation and updating of SurveyNames data. As you might imagine, there are many details in the code, but the following snippets show the essence of the new operations.

persist() uses the now familiar JNDI lookup with the local interface version to get the SurveyNamesBean home interface:

  ...
ic = new InitialContext();
snlHome =
(SurveyNamesLocalHome)ic.lookup(
"java:comp/env/ejb/SurveyNamesBean" );
...

The code then obtains a UserTransaction ( ut is a UserTransaction and sc is a SessionContext ) and begins a new transaction:

  ut = sc.getUserTransaction(); 
...
ut.begin(); // begin transaction

The specification mandates that entity beans use container-managed transactions, but by using the Required transaction attribute, the entity bean adds and updates will join in the transaction initiated by the session bean.

Because the same last names will often be entered into the survey, especially over time, the application takes the approach of first attempting to find existing data for the given last name, using findByPrimaryKey(). If successful, the existing count is incremented; if the finder fails, then new data is created with an initial count of one. The snk variable below is a reference to a SurveyNamesKey instance; snl is a SurveyNamesLocal reference:

  ...
try
{
snl = snlHome.findByPrimaryKey( snk );
bWasSuccessful = true;

snl.incrementTotal();
System.out.println("snl find successful." );
}
catch( ObjectNotFoundException onfe )
{ // create if not found
System.out.println("trying create." );
try
{
snl = snlHome.create( snk );
...

If the find or create was successful, the transaction is committed; if not, the transaction is rolled back:

    if( bWasSuccessful ) 
{
bWasSuccessful = doCommit();
}
else { doRollback(); }

return bWasSuccessful;

From the Web component's view, nothing has changed. As we'll see next, deployment descriptor entries map RockSurvey2Bean to the java:comp/env/ejb/RockSurveyBean lookup. Even the same remote home and component interfaces are used, without change, again being mapped using deployment descriptor entries. Therefore, the JSP pages, constants classes, and the RockSurveyRemote home and component classes from Example: The Rock Survey, take 1 are reused in take 2.

From the end user perspective, the only difference is a pause while the data is persisted after the user clicks Done.




Initial deployment settings for the Rock Survey, take 2

It's time to create the initial descriptor information for Survey2App. While we are accumulating quite a few files, remember that the JSP pages, the constants classes, and the RockSurveyRemote home and component classes are copies from Example: The Rock Survey, take 1; RockSurvey2Bean is an expansion of RockSurveyBean. In this portion, we will create the EAR, add the SurveyNamesBean entity bean, add the revised RockSurvey2Bean session bean, and add the Web component's WAR file.

  1. Compile Java source files in the Survey2 directory, or copy the .class files from the prod folder.
  2. Start J2EE and deploytool.
  3. Ensure that the Cloudscape database is started.
  4. Create new application:
    • From the menu, select File, New, Application.
    • Browse to the Survey2 folder.
    • Key Survey2App.ear in for the file name.
    • Click New Application.
    • Application Display name is Survey2App by default; click OK.
  5. Create new Enterprise JavaBean component -- SurveyNamesBean:
    • Ensure that Application Survey2App is selected.
    • From the menu, select File, New, Enterprise Bean -- Intro display appears; click Next.
    • Select Create New JAR File In Application.
    • Ensure that Survey2App is selected in the drop-down menu.
    • In JAR Display Name, key Survey2JAR.
    • Click Edit.
    • Select the .class files that comprise the bean. These are:
      • SurveyNamesBean.class
      • SurveyNamesKey.class
      • SurveyNamesLocal.class
      • SurveyNamesLocalHome.class
    • Click Add; click OK; click Next.
    • Under Bean Type, click Entity.
    • In the Enterprise Bean Class combo box, select the bean implementation: SurveyNamesBean.
    • In Enterprise Bean Name, accept SurveyNamesBean.
    • Select the corresponding interfaces in the combo boxes:
      • For Local Home Interface, select: SurveyNamesLocalHome.
      • For Local Interface, select: SurveyNamesLocal.
    • Click Next.
    • Ensure that Bean-Managed Persistence is selected under Persistence Management.
    • Key SurveyNamesKey in the Primary Key Class textfield. Click Finish.
    • Survey2JAR and SurveyNamesBean now appear under Survey2App.
    • Click the Resource Refs tab; click Add.
    • Under Coded Name, enter "jdbc/gsejbDB". Leave Type as "javax.sql.DataSource", Authentication as "Container", and Sharable checked.
    • Select Survey2JAR, then click the JNDI Names tab.
    • Under EJBs, key ejb/SurveyNamesBean next to SurveyNamesBean.
    • Under References, enter "jdbc/gsejbDB".
  6. Create new Enterprise JavaBean component -- RockSurvey2Bean:
    • Ensure that Application Survey2App is selected.
    • From the menu, select File, New, Enterprise Bean -- Intro display appears; click Next.
    • Select Add to Existing JAR File.
    • Select Survey2JAR in the drop-down menu.
    • Click Edit.
    • Select the .class files that comprise the bean. These are:
      • RockSurvey2Bean.class
      • RockSurveyConstants.class
      • RockSurveyData.class
      • RockSurveyRemote.class
      • RockSurveyRemoteHome.class
      • SurveyConstants.class
    • Click Add; click OK; click Next.
    • Under Bean Type, click Session and Stateful.
    • In the Enterprise Bean Class combo box, select the bean implementation: RockSurvey2Bean.
    • In Enterprise Bean Name, accept RockSurvey2Bean.
    • Select the corresponding interfaces in the combo boxes:
      • For Remote Home Interface, select: RockSurveyRemoteHome.
      • For Remote Interface, select: RockSurveyRemote.
    • Click Next; click Finish.
    • RockSurvey2Bean now appears under Survey2JAR. Select the EJB Refs tab;, click Add.
    • When it invokes the lookup method, the bean refers to the home of an enterprise bean like this:
      Object objRef = ic.lookup("java:comp/env/ejb/SurveyNamesBean");
      So, in the Coded Name column, enter ejb/SurveyNamesBean.
    • Select Entity for Type and Local for Interfaces.
    • For Home Interface, enter SurveyNamesLocalHome.
    • In the Local/Remote Interface column, enter SurveyNamesLocal.
    • At the bottom, select "ejb-jar-ic.jar#SurveyNamesBean" from the Enterprise Bean Name drop-down menu.
    • Select Survey2JAR, then click the JNDI Names tab.
    • Under EJBs, key ejb/RockSurvey2Bean next to RockSurvey2Bean.
  7. Create new Web component:
    • Ensure that Application Survey2App is selected.
    • From the menu, select File, New, Web Component -- Intro display appears; click Next.
    • Select Create New WAR File In Application.
    • Ensure that Survey2App is selected in the drop-down menu.
    • In WAR Display Name, key Survey2WAR.
    • Click Edit.
    • Select the files needed for the Web component. These are:
      • The images directory
      • index.jsp
      • RockSurvey.jsp
      • RockSurveyExit.jsp
      • SurveyWelcome.jsp
    • Click Add; click OK; click Next.
    • Select JSP for the type of Web component, then click Next.
    • In the JSP filename combo box, select index.jsp.
    • In Web Component Name, allow the default -- index.
    • Click Next. There are no Initialization Parameters. Click Next.
    • Under Component Aliases, click Add, enter "/index" (without quotes).
    • Click Next; click Finish.
    • Survey2WAR now appears under Survey2App.
    • Select Survey2WAR, then select the EJB Refs tab and click Add.
    • When it invokes the lookup method, the Web client refers to the home of an enterprise bean like this:
      Object objRef = ic.lookup("java:comp/env/ejb/RockSurveyBean");
      So, in the Coded Name column, enter ejb/RockSurveyBean.
    • Accept Session for Type and Remote for Interface.
    • For Home Interface, enter RockSurveyRemoteHome.
    • In the Local/Remote Interface column, enter RockSurveyRemote.
    • At the bottom, select "ejb-jar-ic.jar#RockSurvey2Bean" from the Enterprise Bean Name drop-down menu. Then click the JNDI Name button and select ejb/RockSurvey2Bean from the drop-down menu.
    • Now select Survey2App and click on the JNDI Names tab.
    • Verify that all JNDI Name columns have the proper entries.

The code we have so far only handles writing data to the SurveyNames table. The bean to handle the survey data table will be added after the CMP discussion, so we will wait until Final Deployment of the Rock Survey example, take 2 for the actual, final deployment of Survey2App.




Entity beans with container-managed persistence (CMP)

Put another way, the primary difference between BMP and CMP mentioned in Entity beans with bean-managed persistence (BMP) is that with CMP, the container handles the datastore access, insert, update, and deletion routines. Another big difference in version 2.0 of the EJB specification is that an entity bean class that uses CMP must be declared abstract.

Let's talk about that for a moment. Abstract classes are normally used when there is a partial implementation and the class wants to mandate the other methods used without specifying their actual operation. In this case, the specification mandates the methods, namely getters and setters. CMP entity beans do not declare instance variables to represent persistent data. Instead, the container-managed data element names are declared in the deployment descriptor. In the bean class, public abstract getXXX and setXXX methods are declared for the data elements; the container subclasses the bean class and generates the actual implementation. The familiar JavaBeans convention is used: if the data element is named value, the getter/setter pair would be type getValue() and void setValue( type aValue ). So, CMP not only generates the datastore routines, but also manages the individual data elements. This means that the developer can access data only through the getters and setters. The specification calls the data elements virtual fields. The deployment descriptor declaration of the virtual fields is called an abstract persistence schema, and the entries look like this:

...
<cmp-version>2.x</cmp-version>
<abstract-schema-name>ValueBean</abstract-schema-name>
<cmp-field>
<field-name>value</field-name>
</cmp-field>
<cmp-field>
<field-name>cost</field-name>
</cmp-field>
...

Before moving on to CMP method handling, a couple more differences between BMP and CMP should be mentioned. One is that the described data elements must be mapped to actual database columns (or whatever the data elements are in the datastore used). This mapping description goes into a vendor-specific container descriptor, usually generated by vendor tools. In the words of the EJB 2.0 specification, "The EJB deployment descriptor ... does not provide a mechanism for specifying how the abstract persistence schema of an entity bean ... is to be mapped to an underlying database. This is the responsibility of the Deployer."

There's another important difference between BMP and CMP. The EJB 2.0 specification introduced the EJB Query Language (QL), a portable subset of SQL92. QL syntax is used to define queries for finder and select methods under CMP. The queries are defined in the deployment descriptor. The QL syntax is normally translated into the access language of the underlying datastore. In version 2.0 CMP, QL is required for all finders except ejbFindByPrimaryKey(). For more information, see Resources.




Handling life cycle methods under CMP

As with BMP, CMP entity beans must respond appropriately in life cycle methods; it's just that the timing and response is different, due to the container's management of persistence. We saw in Entity beans with container-managed persistence (CMP) that a CMP bean class must be declared abstract. As with BMP, the class must provide a public, no-arg constructor. The sequence of events (see The entity bean life cycle ) is also the same.

Let's take a look at how the methods (first described in Entity beans with bean-managed persistence (BMP) ) work when using CMP:

  • public void setEntityContext(EntityContext ec)
    Same as BMP.

  • public void unsetEntityContext()
    Same as BMP.

  • public PrimaryKeyClass ejbCreate(...)
    The method is now called before the actual creation mechanism executes. The code should only validate input arguments as necessary, and initialize the virtual fields with the input arguments through the setter methods. The return type is still the primary key type, but the return value should be null.

    It is possible to write a bean without an ejbCreate() method when the class deals only with existing data. In this case, there must be no create() methods exposed on the home interface.

  • public void ejbPostCreate(...)
    Same as BMP.

  • public <primary key type or collection> ejbFind<METHOD>(...)
    No finder methods are coded by the developer; all finder methods declared in the home interface are implemented by the container. All finder methods, except ejbFindByPrimaryKey(), are specified in the query deployment descriptor; the container uses the EJB Query Language string defined by the developer as the basis for the finder methods.

  • public void ejbLoad()
    The container invokes this method immediately after reading from the datastore and setting the virtual fields. The only tasks the developer needs to do here would involve some sort of data transformation from the raw data kept in the datastore, or to recalculate/reset instance variables that depend on the persistent data state.

  • public void ejbStore()
    The container invokes this method before writing to the datastore. As with ejbLoad(), the only developer tasks to be performed here would have to do with data translation or preparation prior to persistence -- for example, compressing text and invoking the appropriate setter method.

  • public void ejbRemove()
    The container invokes this method before removing an entity from the datastore. The method can be used to perform any operations required before the data is deleted.

  • public void ejbActivate()
    Same as BMP.

  • public void ejbPassivate()
    Same as BMP.

  • public abstract type ejbSelect<METHOD>(...)
    When using CMP entity beans, the developer can define internal query methods called select methods. The actual implementation is done at deployment time using vendor-provided tools and EJB Query Language syntax.



Example: The Rock Survey, take 2 (continued)

The "SurveyBeanTable", created in The Rock Survey database, contains exactly one row consisting of counter and total columns for the survey data. The last step for take 2 of the Rock Survey application will add data management for the table. As you might expect from the way we went about things in Example: The Rock Survey, take 2 and the preceding discussion, we will accomplish the task by adding another entity bean, this time using CMP, and expanding the persist() method in RockSurvey2Bean.

You may ask yourself why any normal human would name a table "SurveyBeanTable" (unexpectedly, the double quotes are part of the name). The answer is that deploytool generated and named it -- see the discussion about generating the default SQL in Final Deployment of the Rock Survey example, take 2. The developer has no real control over the datastore creation when using CMP; table naming and Object/Relational mapping is not addressed by the specification, meaning that it is vendor proprietary. This state of affairs can become a major issue when dealing with existing databases and for data portability among vendor implementations. And, yes, I cheated in The Rock Survey database by using the container-generated SQL to ensure that the data was set up to match the container's expectations.

The SurveyBean class manages "SurveyBeanTable" data, and, other than providing getters/setter for 19 column variables, is surprisingly small. That's because all of the persistence code (read JDBC/SQL here) is generated by the container. As we will see shortly, the reduction in code comes at the price of more complicated and complex deployment descriptors, along with some vendor-dependent entries.

The local home interface should look familiar:

// the local home interface for SurveyBean
import javax.ejb.*;

public interface SurveyLocalHome extends EJBLocalHome
{

public SurveyLocal create( String Type )
throws CreateException;

public SurveyLocal findByPrimaryKey (String Type)
throws FinderException;

} // end interface SurveyLocalHome

The local component interface is completely composed of getter and setters. These are all declared abstract in the SurveyBean code.

// the local component interface for SurveyBean
import javax.ejb.*;

public interface SurveyLocal extends EJBLocalObject
{
public String getType();
public void setType( String TypeIn );

public int getSF();
public void setSF( int SFIn );
...
public double getPounds();
public void setPounds( double PoundsIn );

} // end interface SurveyLocal

In this case, RockSurvey2Bean, the managing session bean, does all of the editing and validation. At times, you may prefer to have these tasks centralized, probably in the entity bean itself. So how do you do that when the setter is abstract? The answer is to have another method to perform any tasks for a data element, and, if the element passes editing, have that method call the setter. A naming convention for the editing methods becomes very helpful for maintainable code in that situation. We do have one edit in the bean: in the ejbCreate() method, which in CMP is invoked before the actual insertion, the primary key String argument is checked to be sure that it is "ROCK". Again, there will only be one row in this table and the key will always be "ROCK". If the test fails, a CreateException is thrown.

I should mention that it is more typical to send the key and also arguments for all of the data to be inserted in the row to the ejbCreate() method. Here, because inserts should happen very infrequently, probably once in this table's lifetime, and because 19 arguments could get unwieldy to handle correctly, we create the row with just the key, then call the setters using a RockSurvey2Bean method, setupSurveyData(), already written to handle data updates. The coding time and maintenance efficiencies gained by using this method -- in this case -- more than offset any performance losses.

In RockSurvey2Bean, instance variables for the SurveyBean local interfaces have been added, and the ejbPassivate() method now sets the interface variables for both entity beans and the UserTransaction to null. The persist() method tests for null values and resets the variables if appropriate.

The new code related to SurveyBean should look familiar. The code first obtains the local home:

      if( slHome == null )
{
sMsg = "SurveyBean";
ic = new InitialContext();
slHome =
(SurveyLocalHome)ic.lookup(
"java:comp/env/ejb/SurveyBean" );
}

Since there will always be only one row in the "SurveyBeanTable" table, the "try the finder first, perform create upon failure" technique is even more appropriate than before. Here's the code:

    try
{
sl = slHome.findByPrimaryKey( "ROCK" );
bWasSuccessful = true;

setupSurveyData();
System.out.println("sl find successful." );
}
catch( ObjectNotFoundException onfe )
{ // create if not found
System.out.println("trying create." );
try
{
sl = slHome.create( "ROCK" );
setupSurveyData();
...
}
...
}

This code is performed in-line with the SurveyNamesBean management code and within the same transaction.

The other addition to RockSurvey2Bean is the setupSurveyData() method, which breaks down the survey input data to the appropriate categories for updating the datastore. Notice that setupSurveyData() is invoked for both creates and updates.

Again, externally, to both the Web component and the end user, there is no difference between take 1 and take 2 other than whatever time the datastore updates take.




Final Deployment of the Rock Survey example, take 2

Now that both entity beans are coded and the RockSurvey2Bean 's persistence method is complete, it's time to add the final information to the deployment descriptor and actually deploy take 2 of the Rock Survey:

  1. If you haven't already, compile Java source files in the Survey2 directory, or copy the .class files from the prod folder.
  2. Start J2EE and deploytool.
  3. Ensure that the Cloudscape database is started.
  4. Create new Enterprise JavaBean component -- SurveyBean:
    • Ensure that Application Survey2App is selected.
    • From the menu, select File, New, Enterprise Bean -- Intro display appears; click Next.
    • Select Add to Existing JAR File.
    • Select Survey2JAR in the drop-down menu.
    • Click Edit.
    • Select the .class files that comprise the bean. These are:
      • SurveyBean.class
      • SurveyLocal.class
      • SurveyLocalHome.class
    • Click Add; click OK; click Next.
    • Under Bean Type, click Entity.
    • In the Enterprise Bean Class combo box, select the bean implementation: SurveyBean.
    • In Enterprise Bean Name, accept SurveyBean.
    • Select the corresponding interfaces in the combo boxes:
      • For Local Home Interface, select: SurveyLocalHome.
      • For Local Interface, select: SurveyLocal.
    • Click Next.
    • Ensure that Container Managed Persistence 2.0 is selected under Persistence Management.
    • In Abstract Scheme Name, key Survey.
    • Select all of the check boxes in the "Fields to be persisted" scrolled list.
    • Key java.lang.String in the Primary Key Class textfield.
    • Select "type" in the Primary Key Field Name drop-down menu. Click Next; click Finish.
    • SurveyBean now appears under Survey2JAR under Survey2App.
    • Click the Entity tab, then click Deployment Settings.
    • Unselect the check boxes under Database Table.
    • Click Database Settings.
    • Under Database JNDI Name, enter "jdbc/gsejbDB".
    • Click OK.
    • Click Generate Default SQL.
      • If you want to see the SQL, click Container Methods, then click on each method. Do not change anything in the SQL Query box. deploytool does not track changes properly, and you'll get to do everything over again. You can also see the table name and columns generated by the container. Note that there is no vendor-independent means of describing the name.
    • Click OK.
    • Click the Security tab.
    • Under Method Permissions, click Local Home. Change Availability for the remove() method to No Users. Click Local. Change Availability for the remove() method to No Users. No deletes allowed.
    • Select Survey2JAR, then click the JNDI Names tab.
    • Under EJBs, key ejb/SurveyBean next to SurveyBean.
  5. Modify Enterprise JavaBean component -- RockSurvey2Bean:
    • Select RockSurvey2Bean.
    • Select the EJB Refs tab; click Add.
    • When it invokes the lookup method, the bean refers to the home of an enterprise bean like this:
      Object objRef = ic.lookup("java:comp/env/ejb/SurveyBean");
      So, in the Coded Name column, enter ejb/SurveyBean.
    • Select Entity for Type and Local for Interfaces.
    • For Home Interface, enter SurveyLocalHome.
    • In the Local/Remote Interface column, enter SurveyLocal.
    • At the bottom, select "ejb-jar-ic.jar#SurveyBean" from the Enterprise Bean Name drop-down menu.
  6. Deploy the Survey2App:
    • Select Survey2App.
    • Select Tools, Deploy from the menu.
    • Under Object To Deploy, ensure that Survey2App is selected.
    • Under Target Server, we are using localhost. Be sure that is selected.
    • Select the Return Client JAR check box.
    • Select Save object before deploying.
    • Click Next.
    • Verify that the JNDI names are correct, then click Next.
    • Key "/Survey2App" in the context root.
    • Click Next, then Finish.

The Deployment Progress dialog will display. Wait until the progress bars are complete and the Cancel button changes to OK, then click OK. The Survey2App application is deployed.

If you look in the Survey2 folder, you will see that it now contains Survey2App.ear and Survey2AppClient.jar. To run the Survey2App, start both J2EE and Cloudscape, then start up your browser and enter http://localhost:8000/Alice as the target URL. When the Alice's World home page appears, click on the "Alice's Surveys - Take 2" link.

No comments: