EJB Bean Managed Persistence

Routine persistent storage of ejb data is normally provided by the container, through so-called Container Managed persistence (CMP).  It has the advantage of being easy to set-up through VisualAge wizards, and it is optimized for the WebSphere container.  It has the limitation of not supporting the full SQL language for queries and updates.  It is limited to queries that apply to a single table.  Routine queries are further limited to those that use the tables primary key(s), although special finder methods may be developed in which the programmer supplies the parameters for a WHERE clause.  Thus, although easy to develop and adequate for basic persistent storage of ejb data, CMP is not well suited for more complex operations that involve multiple tables, such as JOINs.

Bean Manage Persistence (BMP) allows a programmer to take full advantage of the SQL language to define database operations that map to and from ejbs.  But it comes at a cost of additional work and complexity.  The discussion that follows begins with an overview of the steps that are necessary to develop, use, and deploy a BMP ejb.  It presumes that the reader is familiar with CMP, as discussed in an earlier lesson, and JDBC, also discussed in another lesson.  A critical gotcha is discussed with respect to the database, below.

This image of a generic multi-tier architecture may be helpful.


Development Strategy

Client

Developing a BMP versus and CMP ejb is primarily visible at the lowest level of the ejb architecture.  That is, the client -- normally a session ejb or a mapper class -- has no real awareness that the entity bean is a BMP bean.  The only distinction is in the particular remote interfaces used to cast the remote instance of the bean.

EJB

As would be expected, the BMP ejb has a much larger role in supporting persistent storage of an ejb's data.  It must identify the particular table and database that stores the ejb's fields.  It must generate the SQL statement to store the ejb's fields and load that statement with the data from those fields.  After execution of the statement, it must process the ResultSet returned by transferring the data from the ResultSet to the fields. 

Most of this function is provided by the programmer through a set of required methods.  These include  ejbCreate, ejbFindByPrimaryKey, ejbLoad, ejbStore, and  ejbRemove.  Although VisualAge will generate the stubs for these methods, the programmer must supply the actual code to implement their respective operations.  Routine public and setter methods are required set and get individual data fields.  If complex SQL commands are involved, the bean must include corresponding public methods to invoke them.

Container

Although the BMP bean is responsible for creating, executing, and processing the SQL statement and results, the ejb Container provides substantial support.  Just as with CMP, the container maintains a pool of actual ejb instances that it dynamically allocates when a new ejb is "created."  It also provides transaction support for BMP ejbs, although this function is more appropriately defined at the session level for many applications.  But, most important of all, it maintains a pool of connections to the DBMS that may be dynamically allocated to the individual ejbs when they need to carry out a database operation.  It is critical that the BMP ejb request one of these pooled connections, rather than accessing the database driver directly and creating its own connection, as is done with conventional JDBC implementations.  It only through these pooled connections that Container can provide transaction support.

Database

Just as the programmer is responsible for creating and processing the SQL statements, so he or she is responsible for defining the actual database tables.  This may be done through SQL Create statements that are executed only once, but is most cases it will probably be easier to do so through the tools and/or interfaces provided by the particular DBMS.  With respect to DB2, one can use the Command Center to create the database, if necessary, and from that context either a wizard or a form-like tool to create the actual table(s).  

One major gotcha (that took me two days to discover) is that once the table is created, it cannot be accessed by a BMP ejb until is has been loaded with at least one entry.  If the database is empty, it will result in exceptions being thrown deep in the guts of the system that appear to be RMI/CORBA errors. And since, transaction processing will rollback the database to its original state, no data will show up in it.  To correct this problem, manually insert an entry into the table.  For DB2, this can be done through the Command Center where you can manually write an INSERT statement.

Another gotcha concerns testing in the VisualAge Test Environment.  If you are using a DataSource with connection pooling, that DataSource must be registered with the environment.  To do so, you must first start the environments own naming service.  Then you can create a new data source and/or activate an existing one.  To start the naming service, you must first stop WebSphere's naming service if it is running and using the default port (900) or set everything up so that the tow naming services do not have a port conflict.


Example Code

In these examples, the BMP ejb is called AddressBMP in contrast with an earlier CMP ejb that was called simply Address.  

Client

As mentioned, the main difference in the client -- here a session bean -- is the lookup for the specific BMP bean and subsequent use of its particular Primary Key.

First, an addressBMPHome variable is set when the session bean is created:

public void ejbCreate() throws javax.ejb.CreateException, java.rmi.RemoteException {
	addressBMPHome = buildAddressHome();
	}

The following code builds the addressBMPHome.

public AddressBMPHome buildAddressHome() {  // BMP change
	try
	
	{	    	    
	    Properties properties = new Properties();
	    properties.put( javax.naming.Context.PROVIDER_URL, "IIOP://localhost:900/" );
	    properties.put( javax.naming.Context.INITIAL_CONTEXT_FACTORY, "com.ibm.ejs.ns.jndi.CNInitialContextFactory" );
	    
	    InitialContext initialContext = new InitialContext( properties );
	     
	    Object initialReference = initialContext.lookup( "edu/jbs/address/beans/AddressBMP" );  // BMP change
	    
	    // BMP change
	    AddressBMPHome home = (AddressBMPHome)
	    	javax.rmi.PortableRemoteObject.narrow( initialReference, AddressBMPHome.class );
	    	    
	    	return home;
	    	
	}
	catch(Throwable theException)
	{
		
	    System.out.println( "Error in Servlet: HelloSession.performTask" );
	    theException.printStackTrace();
	    return null;
	    
	}
	
}

In a typical action, such as adding a new ejb, that home object is used conventionally to create the BMP ejb:

public boolean add(Entry entry) {

	try  
	{
		
		address = addressBMPHome.create( entry.getNameFirst(), entry.getNameLast() );
		
		return setAddressValues( address, entry );
		
	}  catch ( CreateException e )  {
		System.out.println( "CreateException " + e + " caught in AddressSession.add" );
		return false;
	}  catch ( RemoteException e )  {
		System.out.println( "RemoteException " + e + " caught in AddressSession.add" );
		return false;
	}
	
}

BMP EJB

Following is the complete code for the AddressBMP bean.  There are several things to note.  

The bean includes static strings that define the SQL statements; the main distinction is that actual values are included as question marks in the statements and have to be filled in in when the statement is "prepared."  

Second, rather than executing statements directly, they are compiled into so-called Prepared Statements.  

Third, and very importantly, connections to the database are obtained from the container through a JNDI lookup from a Container DataSource defined during deployment.

Fourth, the core function is provided through the various ejb___ methods.  Note, in particular, how the question marks in the SQL strings are filled in in the Prepared Statement.

package edu.jbs.address.beans;

import java.rmi.RemoteException;
import java.security.Identity;
import java.util.Properties;
import javax.ejb.*;

import javax.naming.*;
import javax.sql.*;
import java.sql.*;

import com.ibm.ejs.ns.jndi.*;

/**
 * This is an Entity Bean class with BMP fields
 */
public class AddressBMPBean implements EntityBean {
	private javax.ejb.EntityContext entityContext = null;
	private final static long serialVersionUID = 3206093459760846163L;

	public java.lang.String name_first;
	public final static java.lang.String LOAD_ADDRESS = "SELECT name_first, name_middle, name_last, address, city, state, zip FROM AddressBMP WHERE name_first = ? AND  name_last = ?";;
	public java.lang.String name_middle;
	public java.lang.String name_last;
	public java.lang.String address;
	public java.lang.String city;
	public java.lang.String state;
	public java.lang.String zip;
	public final static java.lang.String FIND_BY_PRIMARYKEY = "SELECT name_first, name_last from AddressBMP WHERE (name_first = ?) AND (name_last = ?) ";
	public final static java.lang.String UPDATE = "UPDATE AddressBMP SET name_middle = ?, address = ?, city = ?, state = ?, zip = ? WHERE name_first = ? AND name_last = ?";
	public final static java.lang.String REMOVE = "DELETE from AddressBMP  WHERE name_first = ? AND name_last = ?";
	private javax.sql.DataSource ds;
	public final static java.lang.String DATASOURCE = "jdbc/AddressBMP";
	public final static java.lang.String INSERT = "INSERT INTO AddressBMP(name_first, name_last ) VALUES ( ?, ? )";
	//public final static java.lang.String INSERT_STRING = "INSERT INTO ADDRESSBMP(name_first, name_last) VALUES(?,?)";
/**
 * ejbActivate method comment
 * @exception java.rmi.RemoteException The exception description.
 */
 
public void ejbActivate() throws java.rmi.RemoteException {}
/**
 * ejbCreate method for a BMP entity bean
 * @return edu.jbs.address.beans.AddressBMPKey
 * @param key edu.jbs.address.beans.AddressBMPKey
 * @exception javax.ejb.CreateException The exception description.
 * @exception java.rmi.RemoteException The exception description.
 */
 
public edu.jbs.address.beans.AddressBMPKey ejbCreate( String nf, String nl ) throws javax.ejb.CreateException, java.rmi.RemoteException {
	
	this.setName_first( nf );
	this.setName_last( nl );	
	
	Connection connection = null;
	PreparedStatement sqlStatement = null;

	try  {
		
		connection = getConnection( );
		
		sqlStatement = connection.prepareStatement( INSERT );
		
		sqlStatement.setString( 1, name_first );
		sqlStatement.setString( 2, name_last );
	
		if ( sqlStatement.executeUpdate() != 1 )  {
			throw new CreateException( "Failure in ejbCreate():  row already exists" );
		}
		return new AddressBMPKey( name_first, name_last );
	}  catch ( SQLException e )  {  // end try
		throw new CreateException( "Failure in ejbCreate(): " + e.getMessage() );
	}  finally  {  // end catch		
		try  {
			if ( sqlStatement != null ) sqlStatement.close();
			if ( connection != null ) connection.close();
		}  catch ( SQLException e )  {  // end try
			System.out.println( "Exception caught in ejbCreate:  failure to close connection" );
		} 
		
	}  // end finally
}
/**
 * ejbFindByPrimaryKey method comment
 * @return edu.jbs.address.beans.AddressBMPKey
 * @param primaryKey edu.jbs.address.beans.AddressBMPKey
 * @exception java.rmi.RemoteException The exception description.
 * @exception javax.ejb.FinderException The exception description.
 */
public edu.jbs.address.beans.AddressBMPKey ejbFindByPrimaryKey(edu.jbs.address.beans.AddressBMPKey key) throws java.rmi.RemoteException, javax.ejb.FinderException {
	
	boolean wasFound = false;
	boolean foundMultiples = false;

	Connection connection = null;
	PreparedStatement sqlStatement = null;
	
	try  {

		connection = getConnection();
		sqlStatement = connection.prepareStatement( FIND_BY_PRIMARYKEY );
		sqlStatement.setString( 1, key.nameFirst );
		sqlStatement.setString( 2, key.nameLast );
		
		ResultSet result = sqlStatement.executeQuery();

		wasFound = result.next();
		foundMultiples = result.next();
		
	}  catch ( SQLException e )  {  // end try
		throw new RemoteException( "Failure in ejbFindByPrimaryKey(): " + e.getMessage() );
	}  finally  {  // end catch		
		try  {
			if ( sqlStatement != null ) sqlStatement.close();
			if ( connection != null ) connection.close();
		}  catch ( SQLException e )  {  // end try
			System.out.println( "Exception caught in ejbFindByPrimaryKey:  failure to close connection" );
		} 		
	}  // end finally
	if ( wasFound && !foundMultiples )  {
		return new AddressBMPKey( key.nameFirst, key.nameLast );
	}  else  {
		throw new FinderException( "Multiple riows or now rows found in ejbFindByPrimaryKey" );
	}
	
}
/**
 * ejbLoad method comment
 * @exception java.rmi.RemoteException The exception description.
 */
public void ejbLoad() throws java.rmi.RemoteException {
	
	boolean wasFound = false;
	boolean foundMultiples = false;

	AddressBMPKey key = (AddressBMPKey)getEntityContext().getPrimaryKey();
	Connection connection = null;
	PreparedStatement sqlStatement = null;
	

	try  {
		
		connection = getConnection();
		
		sqlStatement = connection.prepareStatement( LOAD_ADDRESS );
		sqlStatement.setString( 1, key.nameFirst );
		sqlStatement.setString( 2, key.nameLast );
		
		ResultSet result = sqlStatement.executeQuery();

		wasFound = result.next();
		if ( wasFound )  {
			this.setName_first ( result.getString(1) );
			this.setName_middle( result.getString(2) );
			this.setName_last  ( result.getString(3) );
			this.setAddress    ( result.getString(4) );
			this.setCity       ( result.getString(5) );
			this.setState      ( result.getString(6) );
			this.setZip        ( result.getString(7) );
		}		
		foundMultiples = result.next();		
	}  catch ( SQLException e )  {  // end try
		throw new RemoteException( "Failure in ejbLoad(): " + e.getMessage() );
	}  finally  {  // end catch		
		try  {
			if ( sqlStatement != null ) sqlStatement.close();
			if ( connection != null ) connection.close();
		}  catch ( SQLException e )  {  // end try
			System.out.println( "Exception caught in ejbLoad:  failure to close connection" );
		} 		
	}  // end finally
	if ( wasFound && !foundMultiples )  {
		return;
	}  else  {
		throw new RemoteException( "Multiple riows or now rows found in ejbLoad" );
	}
	
}
/**
 * ejbPassivate method comment
 * @exception java.rmi.RemoteException The exception description.
 */
public void ejbPassivate() throws java.rmi.RemoteException {}
/**
 * ejbPostCreate method for a BMP entity bean
 * @param key edu.jbs.address.beans.AddressBMPKey
 * @exception java.rmi.RemoteException The exception description.
 */
public void ejbPostCreate(edu.jbs.address.beans.AddressBMPKey key) throws java.rmi.RemoteException {}
/**
 * ejbRemove method comment
 * @exception java.rmi.RemoteException The exception description.
 * @exception javax.ejb.RemoveException The exception description.
 */
public void ejbRemove() throws java.rmi.RemoteException, javax.ejb.RemoveException {
	
	Connection connection = null;
	PreparedStatement sqlStatement = null;

	try  {
		
		connection = getConnection( );
		
		sqlStatement = connection.prepareStatement( REMOVE );
		
		sqlStatement.setString( 1, name_first );
		sqlStatement.setString( 2, name_last );
	
		if ( sqlStatement.executeUpdate() != 1 )  {
			throw new RemoveException( "Failure in ejbRemove(): SQL error" );
		}
		return;
	}  catch ( SQLException e )  {  // end try
		throw new RemoveException( "Failure in ejbRemove(): " + e.getMessage() );
	}  finally  {  // end catch		
		try  {
			if ( sqlStatement != null ) sqlStatement.close();
			if ( connection != null ) connection.close();
		}  catch ( SQLException e )  {  // end try
			System.out.println( "Exception caught in ejbRemove:  failure to close connection" );
		} 
	}  // end finally
	
}
/**
 * ejbStore method comment
 * @exception java.rmi.RemoteException The exception description.
 */
public void ejbStore() throws java.rmi.RemoteException {
	
	Connection connection = null;
	PreparedStatement sqlStatement = null;
	
	try  {
		
		connection = getConnection();
		
		sqlStatement = connection.prepareStatement( UPDATE );
		sqlStatement.setString( 1, name_middle );
		sqlStatement.setString( 2, address );
		sqlStatement.setString( 3, city );
		sqlStatement.setString( 4, state );
		sqlStatement.setString( 5, zip );
		sqlStatement.setString( 6, name_first );
		sqlStatement.setString( 7, name_last );
	
		
		if ( sqlStatement.executeUpdate() != 1 )  {
		throw new RemoteException( "Failure in ejbStore()" );		}			
	}  catch ( SQLException e )  {  // end try
		throw new RemoteException( "Failure in ejbStore(): " + e.getMessage() );
	}  finally  {  // end try		
		try  {
			if ( sqlStatement != null ) sqlStatement.close();
			if ( connection != null ) connection.close();
		}  catch ( SQLException e )  {  // end try
			System.out.println( "Exception caught in ejbStore:  failure to close connection" );
		} 		
	}  // end finally

}
/**
 * Insert the method's description here.
 * Creation date: (10/10/2001 11:40:33 AM)
 * @return java.lang.String
 */
public java.lang.String getAddress() {
	return address;
}
/**
 * Insert the method's description here.
 * Creation date: (10/10/2001 11:40:52 AM)
 * @return java.lang.String
 */
public java.lang.String getCity() {
	return city;
}
/**
 * Insert the method's description here.
 * Creation date: (10/10/2001 12:30:39 PM)
 * @return java.sql.Connection
 * @exception java.rmi.RemoteException The exception description.
 * @exception java.sql.SQLException The exception description.
 */
public Connection getConnection() throws java.rmi.RemoteException, java.sql.SQLException {
	
	if ( ds == null )  {
		try  {
			Properties properties = new Properties();
	    	properties.put( javax.naming.Context.PROVIDER_URL, "IIOP:///" );
	    	properties.put( javax.naming.Context.INITIAL_CONTEXT_FACTORY, "com.ibm.ejs.ns.jndi.CNInitialContextFactory" );
	    
	    	InitialContext initialContext = new InitialContext( properties );
	     
	    	ds = (DataSource)initialContext.lookup( DATASOURCE );

	    	if ( ds == null )  throw new RemoteException ( "Could not initialize DataSource in getConnection()" );
	    

		}  catch  ( NamingException e )  {  // end try
			throw new RemoteException ( "Could not initialize DataSource in getConnection(): " + e );
		}  // end catch


	}  // end if

	Connection connection = ds.getConnection( );
	return connection;
}
/**
 * getEntityContext method comment
 * @return javax.ejb.EntityContext
 */
public javax.ejb.EntityContext getEntityContext() {
	return entityContext;
}
/**
 * Insert the method's description here.
 * Creation date: (10/10/2001 10:00:59 AM)
 * @return java.lang.String
 */
public java.lang.String getName_first() {
	return name_first;
}
/**
 * Insert the method's description here.
 * Creation date: (10/10/2001 11:33:34 AM)
 * @return java.lang.String
 */
public java.lang.String getName_last() {
	return name_last;
}
/**
 * Insert the method's description here.
 * Creation date: (10/10/2001 11:33:15 AM)
 * @return java.lang.String
 */
public java.lang.String getName_middle() {
	return name_middle;
}
/**
 * Insert the method's description here.
 * Creation date: (10/10/2001 11:41:11 AM)
 * @return java.lang.String
 */
public java.lang.String getState() {
	return state;
}
/**
 * Insert the method's description here.
 * Creation date: (10/10/2001 11:41:25 AM)
 * @return java.lang.String
 */
public java.lang.String getZip() {
	return zip;
}
/**
 * Insert the method's description here.
 * Creation date: (10/10/2001 11:40:33 AM)
 * @param newAddress java.lang.String
 */
public void setAddress(java.lang.String newAddress) {
	address = newAddress;
}
/**
 * Insert the method's description here.
 * Creation date: (10/10/2001 11:40:52 AM)
 * @param newCity java.lang.String
 */
public void setCity(java.lang.String newCity) {
	city = newCity;
}
/**
 * Insert the method's description here.
 * Creation date: (10/10/2001 12:21:13 PM)
 * @param newDs javax.sql.DataSource
 */
public void setDs(javax.sql.DataSource newDs) {
	ds = newDs;
}
/**
 * setEntityContext method comment
 * @param ctx javax.ejb.EntityContext
 * @exception java.rmi.RemoteException The exception description.
 */
public void setEntityContext(javax.ejb.EntityContext ctx) throws java.rmi.RemoteException {
	entityContext = ctx;
}
/**
 * Insert the method's description here.
 * Creation date: (10/10/2001 10:00:59 AM)
 * @param newName_first java.lang.String
 */
public void setName_first(java.lang.String newName_first) {
	name_first = newName_first;
}
/**
 * Insert the method's description here.
 * Creation date: (10/10/2001 11:33:34 AM)
 * @param newName_last java.lang.String
 */
public void setName_last(java.lang.String newName_last) {
	name_last = newName_last;
}
/**
 * Insert the method's description here.
 * Creation date: (10/10/2001 11:33:15 AM)
 * @param newName_middle java.lang.String
 */
public void setName_middle(java.lang.String newName_middle) {
	name_middle = newName_middle;
}
/**
 * Insert the method's description here.
 * Creation date: (10/10/2001 11:41:11 AM)
 * @param newState java.lang.String
 */
public void setState(java.lang.String newState) {
	state = newState;
}
/**
 * Insert the method's description here.
 * Creation date: (10/10/2001 11:41:25 AM)
 * @param newZip java.lang.String
 */
public void setZip(java.lang.String newZip) {
	zip = newZip;
}
/**
 * unsetEntityContext method comment
 * @exception java.rmi.RemoteException The exception description.
 */
public void unsetEntityContext() throws java.rmi.RemoteException {
	entityContext = null;
}
}
 

Deployment

Deployment is conventional, with a couple of exceptions.  First, a DataSource must be defined in WebSphere for the particular database.  Second, that DataSource must be attached to the container in which the BMP ejb runs.  Unlike CMP ejbs for which DataSources can be attached at the bean level, BMP ejbs seem to be restricted to having their DataSources attached at the container level.