Iain,
Going through your topics in order :-
Method names create, load, store, findByPrimaryKey and remove in the remote interface are remapped by the container to ejbCreate() etc. Presumably this is to make sure that the container can perform operations such as storing an in-memory copy, looking for a copy in memory etc.
Similarly all methods starting with find such as findByName are remapped to ejbFindByName. They always return a Collection to the container which remaps each element of the Collection to the remote interface class. The exception is ejbFindByPrimaryKey which returns an object of the same class as the primary key. The container then loads an instance without telling you, and also returns a remote interface instance to the caller.
Finally for this section, any method declared in the home interface that does not match the names above is remapped with a prefix of ejbHome. I would guess that the intention is to make it clear, when you look at the code for the bean, that this method is invoked by the container and cannot manipulate data belonging to any particular instance of the entity bean.
The finder method that you quote is wrong. The remote interface is :-
public Collectiion findByName( String name ) throws FinderException, RemoteException;
and the method on the bean is :-
public Collection ejbFindByName( String name ) throws FinderException;
In this case, you would return a Collection of Integer objects to the container which remaps them to CarRemote objects.
The remapping is done in the container and presumably allows existing remote objects to be used if available.
The EJB specification mandates that ejbStore() is called after every method is executed because the container has no means of knowing whether the method changed data in the entity bean or not. So the specification played safe and insisted on a write. You can get round this using a 'dirty' flag as shown in the example below.
Incautious use of EJB's, when combined with fine grained network I/O, can be a serious hit on performance. If you can imagine a more complex car entity with (say) eight attributes, and getters and setters for each attribute, just reading and writing the attributes could result in one network I/O and database hit to locate an instance, followed by sixteen network I/O's and database hits as the getters and setters were called.
Similarly, a search that retrieved fifty cars by name, got the remote for each car and then read two attributes from each to fill a summary display could be performing 100 network I/O's and, without any optimisation, the same number of database writes.
I attach a sample bean for a Sales Product that optimises the operation by :-
using a value object to eliminate individual getters and setters;
using a dirty flag so that ejbStore() only writes when it needs to;
I also use a session bean to optimise finder methods by handling the remote objects within the server and building a collection of Value Objects to return to the client.
//Package: Demonstration
//Class: SalesProductBean
//Version: 0.b
//Copyright: Copyright (c) 2002
//Author: James Strachan
//Company: Strachan Software Components
//Description: The EJB Component holding an instance of a Sales Product.
//Amendment History:-
//Version Date Author Modification
//0.a 17/06/2002 JAS Conception
//0.b 01/01/2003 JAS Convert to Log4J Error Reporting.
package org.milton.demonstration.maintenance;
// Classes required to return summary list of parameters.
import java.util.Collection;
import java.util.ArrayList;
// Database imports
import java.sql.Date;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import javax.sql.*;
// EJB imports.
import java.rmi.RemoteException;
import javax.ejb.RemoveException;
import javax.ejb.EJBException;
import javax.ejb.CreateException;
import javax.ejb.EntityBean;
import javax.ejb.EntityContext;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.ejb.ObjectNotFoundException;
import javax.ejb.FinderException;
import javax.rmi.PortableRemoteObject;
import java.rmi.RemoteException;
import javax.naming.Context;
import java.io.*;
// Money classes.
import org.milton.services.money.MoneyControlBean;
import org.milton.services.money.MoneyControlHome;
import org.milton.services.money.MoneyControlRemote;
import org.milton.services.money.ImmutableMoney;
// Error Log Helper.
import org.apache.log4j.Logger;
import org.milton.services.utilities.InvalidDataException;
import org.milton.demonstration.common.DemoEJBNameFactory;
import org.milton.services.common.EJBNameFactory;
/**
* This class defines an Entity Bean used to implement a Sales Product.
* <p>
* The class has specialised finder methods and therefore uses Bean Managed Persistence.
* @author J.A.Strachan
* @version 0.b
*/
public class SalesProductBean implements EntityBean {
private static final String DATASTORE_ADDRESS = DemoEJBNameFactory.DEMO_DATASTORE_ADDRESS;
private EntityContext ctx;
/**
* The identifier for a Sales Product.
* Must be public to be available for use by the EJB container.
*/
public String productCode;
/**
* Data representing a Sales Product in a lightweight, behaviourless bean.
*/
private SalesProductLightweight salesProduct;
/**
* Flag set when data changes
*/
private boolean dirty;
/**
* Log4J Logger for error logging.
*/
private static Logger logger;
/**
* Parameterless constructor as required by EJB.
*/
public SalesProductBean() {
if ( logger == null ) {
logger = Logger.getLogger( this.getClass() );
}
}
/**
* Get the identifier for a Product Code.
* @return String
*/
public String getProductCode() {
return productCode;
}
/**
* Get the Value Object for a Product Code.
* @return SalesProductLightweight
*/
public SalesProductLightweight getSalesProduct() {
return salesProduct;
}
/**
* Set the identifier for a sequence.
* @param salesProduct
*/
public void setSalesProduct(SalesProductLightweight salesProduct) {
this.salesProduct = salesProduct;
productCode = salesProduct.getProductCode();
dirty = true;
}
/**
* Standard create method.
* @param newSalesProduct
* @return String sequenceId - primary key of created object.
* @throws RemoteException
* @throws CreateException
*/
public String ejbCreate( SalesProductLightweight newSalesProduct )
throws RemoteException, CreateException {
// Set the primary key and the contained data.
this.productCode = newSalesProduct.getProductCode();
this.salesProduct = newSalesProduct;
logger.info( "Create called : " + productCode );
Connection conn = null;
PreparedStatement statement = null;
InitialContext context = null;
DataSource dataSource;
String sqlStatement = "INSERT into salesProducts ( productCode, " +
"productCategory, title, author, leadTime, currencyCode, netPrice, vatCode ) " +
"values ( ?, ?, ?, ?, ?, ?, ?, ? )";
try {
// Get a database connection.
context = new InitialContext();
dataSource = (DataSource) context.lookup( DATASTORE_ADDRESS );
conn = dataSource.getConnection();
// Prepare a statement and set parameters.
statement = conn.prepareStatement( sqlStatement );
statement.setString( 1, productCode );
statement.setString( 2, salesProduct.getProductCategory() );
statement.setString( 3, salesProduct.getTitle() );
statement.setString( 4, salesProduct.getAuthor() );
statement.setInt( 5, salesProduct.getLeadTime() );
statement.setString( 6, salesProduct.getNetPrice().getCurrencyCode() );
statement.setBigDecimal( 7, salesProduct.getNetPrice().getValue() );
statement.setString( 8, salesProduct.getVatCode() );
// Execute the statement.
statement.execute();
dirty = false;
}
// Error in lookup ?
catch( NamingException ne ) {
logger.error( ne.getMessage(), ne );
throw new CreateException( ne.getMessage() );
}
// Error in SQL statement ?
catch( SQLException sqe ) {
try {
// Check if error is caused by duplicate primary key.
ejbFindByPrimaryKey( productCode );
throw new CreateException( "Sales Product " + productCode + "already exists." );
}
catch ( Exception ignore ) {};
logger.error( sqe.getMessage(), sqe );
throw new CreateException( sqe.getMessage() );
}
finally {
try {
if ( statement != null ) {
statement.close();
}
if ( conn != null ) {
conn.close();
}
}
catch ( SQLException e1 ) {}
}
return productCode;
}
/**
* No work required here - but skeleton is required by EJB.
* @param newSalesProduct
*/
public void ejbPostCreate( SalesProductLightweight newSalesProduct ) {}
/**
* This standard EJB method locates a Sales Product using the Primary Key.
* @param productCode - the Key of the requested Sales Product.
* @return String productCode - the Product Code if the Sales Product was located.
* @throws RemoteException
* @throws ObjectNotFoundException
* @throws FinderException
*/
public String ejbFindByPrimaryKey( String productCode ) throws RemoteException,
ObjectNotFoundException, FinderException {
logger.info( "Find by primary key >" + productCode + "<" );
Connection conn = null;
PreparedStatement statement = null;
InitialContext context = null;
DataSource dataSource;
ResultSet rs;
String sqlStatement = "SELECT productCode from SalesProducts " +
"where productCode = ?";
try {
// Get a database connection.
context = new InitialContext();
dataSource = (DataSource) context.lookup( DATASTORE_ADDRESS );
conn = dataSource.getConnection();
// Prepare and execute SQL statement.
statement = conn.prepareStatement( sqlStatement );
statement.setString( 1, productCode );
statement.execute();
// Check the result set. If there is one row, we have successfully located
// the Parameter.
rs = statement.getResultSet();
if ( rs.next() ) {
return productCode;
}
else {
throw new ObjectNotFoundException( "Sales Product " + productCode + "not found." );
}
}
// Lookup Service failed.
catch( NamingException ne ) {
logger.error( ne.getMessage(), ne );
throw new ObjectNotFoundException( ne.getMessage() );
}
// SQL failed somewhere.
catch( SQLException sqe ) {
logger.error( sqe.getMessage(), sqe );
throw new ObjectNotFoundException( sqe.getMessage() );
}
finally {
try {
if ( statement != null ) {
statement.close();
}
if ( conn != null ) {
conn.close();
}
}
catch ( SQLException e1 ) {}
}
}
/**
* This standard EJB method loads a Sales Product. To fit EJB standards, the
* primary key - productCode - is read from the Context.
* <p>
* @throws ObjectNotFoundException
* @throws RemoteException
*/
public void ejbLoad() throws RemoteException {
Connection conn = null;
PreparedStatement statement = null;
InitialContext context = null;
DataSource dataSource;
ResultSet rs;
MoneyControlRemote moneyControl = getMoneyRemote();
String sqlStatement = "SELECT productCategory, title, author, " +
"leadTime, currencyCode, netPrice, vatCode from salesProducts " +
"where productCode = ?";
productCode = (String) ctx.getPrimaryKey();
logger.info( "Load called : " + productCode );
try {
// Get a database connection.
context = new InitialContext();
dataSource = (DataSource) context.lookup( DATASTORE_ADDRESS );
conn = dataSource.getConnection();
// Prepare and execute the statement.
statement = conn.prepareStatement( sqlStatement );
statement.setString( 1, productCode );
statement.execute();
// Get the result set - if there is a row, create an instance of a Parameter.
rs = statement.getResultSet();
if ( rs.next() ) {
salesProduct = new SalesProductLightweight();
salesProduct.setProductCode( productCode );
salesProduct.setProductCategory( rs.getString(1).trim() );
salesProduct.setTitle( rs.getString(2).trim() );
salesProduct.setAuthor( rs.getString(3).trim() );
salesProduct.setLeadTime( rs.getInt(4) );
ImmutableMoney temp = moneyControl.createImmutableMoney( rs.getString(5).trim(),
rs.getBigDecimal(6) );
salesProduct.setNetPrice( temp );
salesProduct.setVatCode( rs.getString(7).trim() );
dirty = false;
}
else {
throw new RemoteException( "Sales Product " + productCode + "not found." );
}
}
// Lookup failed ?
catch( InvalidDataException ive ) {
logger.error( ive.getMessage(), ive );
throw new RemoteException( ive.getMessage() );
}
// Lookup failed ?
catch( NamingException ne ) {
logger.error( ne.getMessage(), ne );
throw new RemoteException( ne.getMessage() );
}
// Something nasty in the woodshed ?
catch( SQLException sqe ) {
logger.error( sqe.getMessage(), sqe );
throw new RemoteException( sqe.getMessage() );
}
finally {
try {
if ( statement != null ) {
statement.close();
}
if ( conn != null ) {
conn.close();
}
}
catch ( SQLException e1 ) {}
}
}
/**
* This standard EJB method physically deletes a Sales Product from the database.
* The key of the Sales Product is obtained from the context.
* @throws RemoteException
* @throws RemoveException
*/
public void ejbRemove() throws RemoteException, RemoveException {
Connection conn = null;
PreparedStatement statement = null;
InitialContext context = null;
DataSource dataSource;
String sqlStatement = "DELETE from SalesProducts where productCode = ?";
int rowCount;
productCode = (String) ctx.getPrimaryKey();
logger.info( "Remove called : " + productCode );
try {
context = new InitialContext();
dataSource = (DataSource) context.lookup( DATASTORE_ADDRESS );
conn = dataSource.getConnection();
statement = conn.prepareStatement( sqlStatement );
statement.setString( 1, productCode );
rowCount = statement.executeUpdate();
if ( rowCount != 1 ) {
throw new RemoveException( "Sales product " + productCode + " already deleted." );
}
}
catch( NamingException ne ) {
logger.error( ne.getMessage(), ne );
throw new RemoveException( ne.getMessage() );
}
catch( SQLException sqe ) {
logger.error( sqe.getMessage(), sqe );
throw new RemoveException( sqe.getMessage() );
}
finally {
try {
if ( statement != null ) {
statement.close();
}
if ( conn != null ) {
conn.close();
}
}
catch ( SQLException e1 ) {}
}
}
/**
* Standard EJB method to store a Sales Product in persistent data.
* @throws RemoteException
*/
public void ejbStore() throws RemoteException {
Connection conn = null;
PreparedStatement statement = null;
InitialContext context = null;
DataSource dataSource;
// Do nothing unless data has chhanged.
if ( dirty == false ) {
return;
}
String sqlStatement = "UPDATE SalesProducts set ProductCategory = ?, " +
"author = ?, title = ?, leadTime = ?, currencyCode = ?, " +
"netPrice = ?, vatCode = ? " +
"WHERE productCode = ?";
int rowCount;
logger.info( "Store : " + productCode );
try {
// Get a database connection.
context = new InitialContext();
dataSource = (DataSource) context.lookup( DATASTORE_ADDRESS );
conn = dataSource.getConnection();
// Prepare statement and parameters.
statement = conn.prepareStatement( sqlStatement );
statement.setString( 1, salesProduct.getProductCategory() );
statement.setString( 2, salesProduct.getTitle() );
statement.setString( 3, salesProduct.getAuthor() );
statement.setInt( 4, salesProduct.getLeadTime() );
statement.setString( 5, salesProduct.getNetPrice().getCurrencyCode() );
statement.setBigDecimal( 6, salesProduct.getNetPrice().getValue() );
statement.setString( 7, salesProduct.getVatCode() );
statement.setString( 8, productCode );
// Execute and check results.
rowCount = statement.executeUpdate();
logger.debug( "rowCount = " + rowCount );
if ( rowCount != 1 ) {
logger.warn( "Sales Product " + productCode + "not found." );
throw new RemoteException( "Sales Product " + productCode + "not found." );
}
}
// Lookup failed.
catch( NamingException ne ) {
logger.error( ne.getMessage(), ne );
throw new RemoteException( ne.getMessage() );
}
// Database error.
catch( SQLException sqe ) {
logger.error( sqe.getMessage(), sqe );
throw new RemoteException( sqe.getMessage() );
}
finally {
try {
if ( statement != null ) {
statement.close();
}
if ( conn != null ) {
conn.close();
}
}
catch ( SQLException e1 ) {}
}
}
/**
* This method is a customised EJB finder method that returns a list of all
* Sales Products stored in persistent data. The data returned is contained
* in a Collection of objects which are Primary Keys of the class. These
* are then encapsulated as EJBObjects by the container.
* <p>
* The primary key for this class is a String so we build an ArrayList of
* Strings.
* @return Collection of String primary keys.
* @throws RemoteException
* @throws FinderException
*/
public Collection ejbFindAll() throws RemoteException, FinderException {
Connection conn = null;
PreparedStatement statement = null;
InitialContext context = null;
DataSource dataSource;
ResultSet rs;
ArrayList result = new ArrayList();
String sqlStatement = "SELECT productCode from SalesProducts " +
"order by productCode";
logger.info( "FindAll called");
try {
// Get a connection/
context = new InitialContext();
dataSource = (DataSource) context.lookup( DATASTORE_ADDRESS );
conn = dataSource.getConnection();
// Execute the statement.
statement = conn.prepareStatement( sqlStatement );
statement.execute();
rs = statement.getResultSet();
// Loop through the SQL result set, building the Collection to be returned.
// Trim the strings so that the key definition is consistent.
while( rs.next() ) {
result.add( rs.getString(1).trim() );
}
}
// Lookup Servicefailed ?
catch( NamingException ne ) {
logger.error( ne.getMessage(), ne );
throw new RemoteException( ne.getMessage() );
}
// Database service failed ?
catch( SQLException sqe ) {
logger.error( sqe.getMessage(), sqe );
throw new RemoteException( sqe.getMessage() );
}
finally {
try {
if ( statement != null ) {
statement.close();
}
if ( conn != null ) {
conn.close();
}
}
catch ( SQLException e1 ) {}
}
return result;
}
/**
* This method is a customised EJB finder method that returns a list of all
* Sales Products in a Product Category.
* @see comments on ejbFindAll
* @param productCategory
* @return Collection of String primary keys.
* @throws RemoteException
* @throws FinderException
*/
public Collection ejbFindByCategory( String productCategory )
throws RemoteException, FinderException {
Connection conn = null;
PreparedStatement statement = null;
InitialContext context = null;
DataSource dataSource;
ResultSet rs;
ArrayList result = new ArrayList();
String sqlStatement = "SELECT productCode from SalesProducts " +
"where productCategory = ? " +
"order by productCode";
logger.info( "FindByCategory called");
try {
// Get a connection/
context = new InitialContext();
dataSource = (DataSource) context.lookup( DATASTORE_ADDRESS );
conn = dataSource.getConnection();
// Execute the statement.
statement = conn.prepareStatement( sqlStatement );
statement.setString( 1, productCategory );
statement.execute();
rs = statement.getResultSet();
// Loop through the SQL result set, building the Collection to be returned.
// Trim the strings so that the key definition is consistent.
while( rs.next() ) {
result.add( rs.getString(1).trim() );
}
}
// Lookup Servicefailed ?
catch( NamingException ne ) {
logger.error( ne.getMessage(), ne );
throw new RemoteException( ne.getMessage() );
}
// Database service failed ?
catch( SQLException sqe ) {
logger.error( sqe.getMessage(), sqe );
throw new RemoteException( sqe.getMessage() );
}
finally {
try {
if ( statement != null ) {
statement.close();
}
if ( conn != null ) {
conn.close();
}
}
catch ( SQLException e1 ) {}
}
return result;
}
/**
* Get the remote interface to the MoneyControlBean in the Application Server.
* @return MoneyControlRemote
* @throws RemoteException
*/
private MoneyControlRemote getMoneyRemote() throws RemoteException {
MoneyControlHome home;
MoneyControlRemote remote = null;
try {
Context context = new InitialContext();
home = (MoneyControlHome) PortableRemoteObject.narrow(
context.lookup(EJBNameFactory.buildName(EJBNameFactory.MONEY_CONTROLS)),
MoneyControlHome.class );
remote = home.findByPrimaryKey( MoneyControlBean.SINGLETON_NAME );
}
catch ( Exception e ) {
logger.error( "Finding money control", e );
throw new RemoteException( "Finding Money Control", e );
}
return remote;
}
public void setEntityContext(EntityContext ctx) throws RemoteException {
this.ctx = ctx;
}
public void unsetEntityContext() throws RemoteException {
ctx = null;
}
public void ejbActivate() throws RemoteException {
}
public void ejbPassivate() throws RemoteException {
}
}