How to do bean-managed persistence with jBoss

Introduction

What this article is about

This article describes how to author straightforward, bean-managed-persistence entity EJBs. By `straightforward' I mean EJBs in which only simple instance variables are persistent. This article will not even touch on the notion of persistent associations, important though it is. Moreover, the example will only demonstrate techniques compatible with Version 1.1 of the EJB Specification; Version 2.0 introduces some nice features, but it is not well supported by products at the moment.
     We will begin by examining in detail the coding, assembly and deployment of a complete, but naive implementation of an entity EJB. This implementation will be similar to those presented in most elementary courses and textbooks on this subject. Analysis of the limitations of this implementation will lead to an improved version. Because of its focus on writing a `proper' implementation of an EJB, I was going to call this article `How to do BMP properly' (hence the name of the Java package in the source code); this seemed on reflection somewhat arrogant, as there are many right ways to author an EJB. This article presents what I believe is one way to do BMP properly.
     This article uses jBoss, version 2.2.2, as a demonstration platform, and describes the following aspects of the use of jBoss in addition to the development of entity EJBs.

Disclaimer and legal stuff

1. I have taken some trouble to ensure that this article is correct and helpful, but I won't accept responsibility for any adverse consequences arising from its use. There is no warranty of any kind.

2. I receive a lot of e-mail about jBoss (hundreds of messages every week). I do my best to respond to them all, but I don't have the time to provide detailed guidance to people who can't get my examples to work, unless they have tried them on a system compatible with my own. In particular, there are minor version-to-version changes between releases of jBoss, and I don't even attempt to keep track of the implications these have on my tutorials. Sorry, but I have a day-job.

3. Readers who know me, or have looked at my Web site, may be aware that I teach EJB courses for Sun Microsystems. However, this article and others that I have written on EJBs and jBoss have nothing to do with my work for Sun, and should not be taken as an indication of Sun's position on anything. I am no more privy to Sun's long-term plans for EJBs or the J2EE architecture than anyone else (it's not my part of the Company), so please don't ask.

Prerequisites, assumptions and conventions: PLEASE READ THIS

To follow this tutorial step-by-step, you will need (at least) the following software. Of course it may work with other software or other versions than those specified, but I make no guarantees. Please don't call me if it doesn't work with a different set-up, as I don't have time to respond personally to such communications (sorry). In case anyone is interested, I use RedHat Linux version 7.0 for this kind of development, but nothing in this article is platform-specific (but see notes on the class search path below). For maximum portability, this tutorial uses only command-line tools. For simplicity I have included an Ant build script to automate most of the compile operations; if you don't want to use Ant, you can inspect the script build.xml to get full details of the command line operations. Ant is available free-of-charge from the Jakarta Web site; it is written entirely in Java so should work on most platforms.
     In what follows, I will be making some assumptions. These assumptions may not match your system. In that case, you may need to interpret the source code and instructions in view of the differences between your system and mine. Please note the following points in particular. Because some of the commands required are very long, this article follows the convention of indicating a continuation onto the next line with a backslash character. This is a bit of a nuisance, as Windows systems interpret this as part of a filename, but there should be no ambiguity as the `continuation' back-slash only appears at the end of a line. So a command like this:
this is \
a long line
should be reassembled into one line:
this is a long line
Most Unix command-line shells allow the back-slash character to be entered directly on the command line to indicate that the entry continues onto the next line, but many Windows systems don't. In the latter case you should enter one single long line. In both cases, of course, you should not be entering these commands manually, but using a build file, script or batch file according to your preference.

Design overview

The EJB in this article models a customer of some on-line application. For simplicity, the EJB has no business logic, and is intended to provide an object-oriented view of a database table. The table has four variable-length character fields: id, name, address, and email, of which id is the primary key. For simplicity we will use the Java String class to model the primary key, rather than creating a custom primary key class. The persistent properties will be both readable and writable, and clients of the EJB will be able to create and delete new customers. These requirements imply that we must implement ejbStore, ejbCreate, and ejbRemove, as well as ejbLoad and the finder methods.
     The business methods of the EJB are primarily getXXX and setXXX methods by which clients can get and set the persistent properties of the EJB.
     Entity EJBs have a compulsory findByPrimaryKey method on the home interface, and typically a number of other `finder' methods. In this example we will implement a findByNameContaining method that returns a collection of references to EJBs whose `name' property contains the specified string. In reality we would probably need other `finder' methods, but this one will be adequate for demonstration purposes.

The first version, step-by-step

Step 1: set up the source tree

If you uncompress the supplied source code -- telling the uncompressor to maintain directory hierarchy -- it will automatically build the source tree and target directories in the right places. If you want to work from scratch, please bear in mind that the instructions below assume the following directory structure.
[source_root]
  com
    web_tomorrow
      bmp_properly  
        ejb         -- EJB source code
        client      -- test client source code
      ejb_utils     -- general utilities for EJBs
  utils             -- source code for the sample data loader (see below)
  clienttarget      -- directory into which client-side .class files will be generated 
  servertarget      -- directory into which server-side .class files will be generated
  build.xml         -- Ant build script (see below)
  init.sql          -- SQL script to install the initial data

Step 2: edit build.xml

If you plan to use Ant, you will need to edit the build script build.xml to indicate the location of jBoss. Change the line
  <property name="jboss_home" value="../../lib/JBoss-2.2.2" />
to whatever is appropriate on your system.
     The Ant build script sets the class search path appropriately for the build process, using configuration parameters in the `init' section.

I strongly advise disabling any system-wide CLASSPATH settings before using the build script. On Linux, execute the following at the prompt before running ant
     CLASSPATH=
     
The Windows equivalent is
     set CLASSPATH=
     
In both cases, this change only affects the current terminal session; changes to system configuration is necessary to make this permanent.

The reason for this is that many people have a number of different EJB products installed, and they often have classes with the same name. For example, implementations of javax.naming.InitialContext are supplied with both jBoss and Sun's J2EE Reference Implementation, and they are not compatible. I receive a lot of e-mails from people who have had problems with the compile process picking up classes from the wrong place. Disabling the system CLASSPATH allows us to be sure that the class search path in the build script or command line is the only place where classes will be found.

Step 3: compile the sample data loader

This tutorial requires that the database be pre-loaded with data; this is supplied as an SQL script init.sql in the source distribution. This tutorial assumes the use of the Hypersonic database supplied with jBoss. Because the jBoss distribution does not (as far as I know) include a stand-alone SQL client, I have provided a simple data loader that reads SQL from a file and executes it using JDBC calls. This should work with other databases than Hypersonic, although most have their own SQL clients.
     The data loader is a class utils.LoadSQL. This source code is in the file LoadSQL.java in directory [source_root]/utils. It can be compiled at the command line by changing directory to [source_root] and executing:
javac -d clienttarget utils/LoadSQL.java 
(if you are using Windows, remember to change directory separators from `/' to `\').
     If you are using Ant, you should be able to compile the data loader with the command
ant compileutils

Step 4: install the sample data

The data loader compiled in the previous step is a simple SQL client. It expects to be run from the command line, with four command-line arguments, like this:
java -classpath [...] [JDBC options] filename database_url user_id password
filename is the file containing the SQL to execute (init.sql in this case). database_url is the JDBC URL for the database to be modified. Hypersonic URLs all begin jdbc:HypersonicSQL. user_id and password are user credentials that allow writes to the database. Because some command lines have difficulty accepting an empty string as an argument, and this is the standard way to indicate `no password' in JDBC, the data loader interprets a password of `*' as `no password'. This is important, because the user account we will be using with Hypersonic has no password. This account is the built-in user `sa' that should have been created when jBoss was installed.
     To run the loader, the classpath option should be set to enable the JVM to find the following classes and JARs: Finally, `JDBC options' are the settings required to tell the JVM to load the correct database driver to match the specified URL. Typically we do this by setting the system property `jdbc.drivers', like this for Hypersonic:
-Djdbc.drivers=org.hsql.jdbcDriver
For other databases you should be able to find the driver class from the vendor's product information.
     So, to run the data loader we need a command line like this horrendous example (change jboss_root to the appropriate value for your system):
java -Djdbc.drivers=org.hsql.jdbcDriver \
-classpath clienttarget:\[jboss_root]/lib/ext/hsql.jar \
utils/LoadSQL init.sql jdbc:HypersonicSQL:hsql://localhost:1476 \ 
sa *
(don't forget that the `\' at the end of the listing indicates that this is all one long line).
     If you are using Ant, simply execute the command:
ant installdata
which does exactly the same job.
     On running the data loader, you should see messages indicating that a number of database rows have been created. Messages like `0 rows updated' are OK; these simply correspond to SQL statements like `drop table' which do not update rows in the SQL sense. However, examine any exception quite carefully. The first time this utility is executed, you might reasonably expect to see an exception indicating that a `drop table' failed (as there is not table to drop), but most other exceptions are cause for concern.

Step 5: code and compile the EJB

Home interface

The home interface of an entity EJB (in version 1.1 of the EJB Specification) specifies `create' and `finder' methods. In this example, we will have a single create method that sets all the persistent properties in one go. We will also have the compulsory findByPrimaryKey(), and an additional, multi-object finder findByNameContaining(). The home interface therefore looks like this (CustomerHome.java):

package com.web_tomorrow.bmp_properly.ejb;

import javax.ejb.*;
import java.rmi.*;
import java.util.*;

/**
Home interface for `Customer' EJB
(c)2001 Kevin Boone/Web-Tomorrow
*/
public interface CustomerHome extends EJBHome
{
public Customer findByPrimaryKey(final String id) throws FinderException, RemoteException;
public Customer create(final String id, final String name, 
  final String address, final String email) 
    throws CreateException, RemoteException;
public Collection findByNameContaining (final String search)
    throws FinderException, RemoteException;
}

Note that findByPrimaryKey and create both return the primary key type (String), while the multi-object finder findByNameContaining() returns a Collection. As per the specification, all methods are declared to throw RemoteException, the finders throw FinderException, and create() throws CreateException.

Remote interface

The remote interface (Customer.java) exposes the business methods of the EJB which, in this case, are all `getters' and `setters'.

package com.web_tomorrow.bmp_properly.ejb;

import javax.ejb.*;
import java.rmi.*;

/**
Remote interface for `Customer' EJB
(c)2001 Kevin Boone/Web-Tomorrow
*/
public interface Customer extends EJBObject
{
public String getName() throws RemoteException;
public void setName(final String name) throws RemoteException;
public String getEmail() throws RemoteException;
public void setEmail(final String email) throws RemoteException;
public String getAddress() throws RemoteException;
public void setAddress(final String address) throws RemoteException;
public String getId() throws RemoteException;
}


As always, these methods are declared to throw RemoteException.

Implementation class

The first version of the implementation class is listed below (CustomerBeanVersion1.java). Comments on the implementation are below the listing.

package com.web_tomorrow.bmp_properly.ejb;

import javax.ejb.*;
import java.rmi.*;
import java.sql.*;
import java.util.*;
import javax.sql.*;
import javax.naming.*;
import com.web_tomorrow.ejb_utils.*; 

public class CustomerBeanVersion1 implements EntityBean 
{
// Non-persistent instance variables
protected EntityContext entityContext;
protected Logger logger;
protected DataSource datasource;

// Persistent instance variables
protected String id;
protected String name;
protected String address;
protected String email;

/**
Save the entity context for future use, and initialize the datasource so we can
use it to get connections in subsequent method calls
*/
public void setEntityContext (EntityContext entityContext)
  {
  logger = new Logger ("java:comp/env/debug");
  logger.log (2, "Called setEntityContext");
  this.entityContext = entityContext;
  try
    {
    Context c = new InitialContext();
    datasource = (DataSource)c.lookup("java:/DefaultDS");
    }
  catch (NamingException e)
    {
    logger.log (2, "Caught NamingException in setEntityContext: " + e);
    throw new EJBException ("Datasource name lookup failed: " + e);
    }
  }
  
/**
Does nothing in this example
*/
public void unsetEntityContext ()
  {
  logger.log (2, "Called unsetEntityContext");
  }
  
/**
Delete an entry from the database
*/
public void ejbRemove () throws RemoveException
  {
  logger.log (2, "Entering ejbRemove");
  try
    {
    id = (String)entityContext.getPrimaryKey();
    Connection c = datasource.getConnection();
    Statement s = c.createStatement();
    String q = "DELETE FROM BMP_customer WHERE id='" + id + "'";
    logger.log (2, "Executing update: " + q);
    s.executeUpdate(q);
    s.close();
    c.close();
    }
  catch (SQLException e)
    {
    // If we get an SQL exceptino here, convention dicates that we 
    // must throw a RemoveException to indicate to the client that
    // the deletion probably failed 
    logger.log (0, "Caught SQLException in ejbRemove: " + e);
    throw new RemoveException (e.toString());
    }

  logger.log (2, "Leaving ejbRemove");
  }
  
/**
Read data from the database. On entry `id' will be set correctly, because it
was set in ejbActivate(). All other persistent instance variables are
meaningless, and must be read from the database
*/
public void ejbLoad ()
  {
  logger.log (2, "Entering ejbLoad");

  try
    {
    Connection c = datasource.getConnection();
    Statement s = c.createStatement();
    String q = "SELECT * FROM BMP_customer WHERE id='" + id + "'";
    logger.log (2, "Executing query: " + q);
    ResultSet rs = s.executeQuery(q);
    rs.next(); // SHOULD always succeed: the container should never have to
               //  call on a non-existent row
    name = rs.getString ("name");
    address = rs.getString ("address");
    email = rs.getString ("email");
    rs.close();
    s.close();
    c.close();
    }
  catch (SQLException e)
    {
    // If we get an SQL exceptino here then something has gone horribly wrong.
    // Throw EJB exception to alert container
    logger.log (0, "Caught SQLException in ejbLoad: " + e);
    throw new EJBException (e);
    }

  logger.log (2, "Leaving ejbLoad");
  }
  
/**
Write data to the database
*/
public void ejbStore ()
  {
  logger.log (2, "Entering ejbStore");
  try
    {
    Connection c = datasource.getConnection();
    Statement s = c.createStatement();
    String q = "UPDATE BMP_customer " 
      + "SET name='" + name + "'," 
      + "address='" + address + "'," 
      + "email='" + email+ "' " 
      + "WHERE id='" + id + "'";
    logger.log (2, "Executing update: " + q);
    s.executeUpdate(q);
    s.close();
    c.close();
    }
  catch (SQLException e)
    {
    // If we get an SQL exceptino here then something has gone horribly wrong.
    // Throw EJB exception to alert container
    logger.log (0, "Caught SQLException in ejbStore: " + e);
    throw new EJBException (e);
    }

  logger.log (2, "Leaving ejbStore");
  }
  
/**
Gets the primary key from the container. In ejbLoad and ejbStore, assume that
the primary key stored in the `id' field is correct 
*/
public void ejbActivate ()
  {
  logger.log (2, "Called ejbActivate");
  id = (String)entityContext.getPrimaryKey();
  }
  
/**
Does nothing in this implementation
*/
public void ejbPassivate ()
  {
  logger.log (2, "Called ejbPassivate");
  }
  
/**
ejbFindByPrimaryKey simply checks whether the data exists in the database; it
does not load it or set instance variables. The container will call ejbLoad
when it wants this to happen. According to the EJB Specification, we must throw
ObjectNotFoundException if the database entry does not exist
*/
public String ejbFindByPrimaryKey(final String id)
    throws FinderException
  {
  try
    {
    logger.log (2, "Entering ejbFindByPrimaryKey");
    boolean ok = false;
    Connection c = datasource.getConnection();
    Statement s = c.createStatement();
    String q = "SELECT * FROM BMP_customer WHERE id='" + id + "'";
    logger.log (2, "Executing query: " + q);
    ResultSet rs = s.executeQuery(q);
    if (rs.next()) ok = true;
    rs.close();
    s.close();
    c.close();
    if (ok)
      {
      logger.log (2, "Leaving ejbFindByPrimaryKey");
      return id;
      }
    logger.log (2, "Leaving ejbFindByPrimaryKey, by throwing exception");
    throw new ObjectNotFoundException ("No Customer with id=" + id);
    }
  catch (SQLException e)
    {
    // If we get an SQL exceptino here then something has gone horribly wrong.
    // Throw EJB exception to alert container
    logger.log (0, "Caught SQLException in ejbFindByPrimaryKey: " + e);
    throw new EJBException (e);
    }
  } 

/**
ejbFindByNameContaining returns a Collection of primary keys, one for each
row of the customer table whose `name' field contains the supplied string. 
If the String is empty, then a collection containing all EJBs is returned
to the client. Clearly this may well be very inefficient if the customer
table has many entries, and this action should be discouraged.
*/
public Collection ejbFindByNameContaining (final String search)
    throws FinderException
  {
  try
    {
    logger.log (2, "Entering ejbFindByNameContaining");
    Connection c = datasource.getConnection();
    Statement s = c.createStatement();
    String q = "SELECT * FROM BMP_customer WHERE name like '%" + search + "%'";
    logger.log (2, "Executing query: " + q);
    ResultSet rs = s.executeQuery(q);
    ArrayList al = new ArrayList();
    while (rs.next())
      {
      al.add (rs.getString("id"));
      }
    rs.close();
    s.close();
    c.close();
    logger.log (2, "Leaving ejbFindByNameContaining");
    return al;
    }
  catch (SQLException e)
    {
    // If we get an SQL exceptino here then something has gone horribly wrong.
    // Throw EJB exception to alert container
    logger.log (0, "Caught SQLException in ejbFindByNameContaining: " + e);
    throw new FinderException (e.toString());
    }
  } 

/**
ejbCreate must inspect the supplied arguments, create a database entry, and
return the primary key to the container. In this example this is
straightforward, since the primary key is just the `id' field, which is
supplied as a parameter by the client
*/
public String ejbCreate(final String id, final String name, 
  final String address, final String email) 
    throws CreateException
  {
  try
    {
    logger.log (2, "Entering ejbCreate");
    boolean ok = false;
    // Set the instance variables from the arguments
    this.id = id;
    this.name = name;
    this.address = address;
    this.email = email;
    Connection c = datasource.getConnection();
    Statement s = c.createStatement();
    String q = "INSERT INTO BMP_customer (id,name,address,email) VALUES ("
        + "'" + id + "',"
        + "'" + name + "',"
        + "'" + address + "',"
        + "'" + email + "')";
    logger.log (2, "Executing query: " + q);
    s.execute(q);
    s.close();
    c.close();
    logger.log (2, "Leaving ejbCreate");
    return id;
    }
  catch (SQLException e)
    {
    // If we get an SQL exceptino here then something has gone horribly wrong.
    // Throw EJB exception to alert container
    logger.log (0, "Caught SQLException in ejbCreate: " + e);
    throw new EJBException (e);
    }
  }

public void ejbPostCreate(final String id, final String name, 
  final String address, final String email) 
  {
  logger.log (2, "Called ejbPostCreate");
  }

// Business methods: only `set' and `get' in this example

public String getId() 
  {
  logger.log (2, "Called getId");
  return id;
  }

// Note no `setId()' : primary key can't be set

public String getName() 
  {
  logger.log (2, "Called getName");
  return name;
  }

public void setName(final String name) 
  {
  logger.log (2, "Called setName");
  this.name = name;
  }

public String getAddress() 
  {
  logger.log (2, "Called getAddress");
  return address;
  }

public void setAddress(final String address)
  {
  logger.log (2, "Called setAddress");
  this.address = address;
  }

public String getEmail() 
  {
  logger.log (2, "Called getEmail");
  return email;
  }

public void setEmail(final String email) 
  {
  logger.log (2, "Called setEmail");
  this.email = email;
  }
}



This implementation is fairly naive, for reasons that will be discussed later. However, it is fully functional. Here are a few other points to note about the implementation.

Compilation

For compilation, the javac program needs to have a class path that will enable it to find the EJB classes (in javax.ejb) and the JDBC-2.0 classes (in javax.sql). On jBoss, the following should do the trick:
javac -classpath \
.:[jboss_root]/lib/ext/ejb.jar:[jboss_root]/lib/ext/jdbc2_0-stdext.jar \
com/web_tomorrow/bmp_properly/ejb/*.java
Alternatively, if you are using Ant you can do this: ant compileejb Note that this will also compile the other versions of the EJB as well, but this won't cause any problems.

Step 6: create the deployment descriptor

The deployment descriptor is a file ejb-jar.xml in a directory META-INF in the EJB JAR file. There are various graphical tools available for creating deployment descriptors, but in the simple example a text editor will be adequate if you know the format of the deployment descriptor well enough. Note that jBoss does some fairly thorough testing of the deployment descriptor at deployment time, so you'll soon find out if you've got it wrong. Here is a suitable deployment descriptor for version 1 of the Customer EJB.
<?xml version="1.0" encoding="ISO-8859-1"?>
<ejb-jar>
  <description>BMPProperly</description>
  <display-name>BMPProperly</display-name>
  <enterprise-beans>
    <entity>
      <description>Version 1</description>
      <display-name>Customer1</display-name>
      <ejb-name>Customer1</ejb-name>
      <home>com.web_tomorrow.bmp_properly.ejb.CustomerHome</home>
      <remote>com.web_tomorrow.bmp_properly.ejb.Customer</remote>
      <ejb-class>com.web_tomorrow.bmp_properly.ejb.CustomerBeanVersion1</ejb-class>
      <persistence-type>Bean</persistence-type>
      <prim-key-class>java.lang.String</prim-key-class>
      <reentrant>False</reentrant>
      <env-entry>
        <env-entry-name>debug</env-entry-name>
        <env-entry-type>java.lang.Integer</env-entry-type>
        <env-entry-value>2</env-entry-value>
      </env-entry>
    </entity>
  </enterprise-beans>
  <assembly-descriptor>
    <container-transaction>
      <method>
        <ejb-name>Customer1</ejb-name>
        <method-name>"*"</method-name>
      </method>
      <trans-attribute>Required</trans-attribute>
    </container-transaction>
  </assembly-descriptor>
</ejb-jar>
(Please note that the version in the source code package contains entries for the other version of the EJB as well, so it won't look exactly like this listing).
     This deployment descriptor sets the transaction attributes of all methods to Required, which is a sensible default. It also defines an environment variable `debug', which is used by the debug logger. The value `2' means `maximum output' in this example, which should lead to a whole heap of debugging output when the EJB is invoked. The tag
<ejb-name>Customer1</ejb-name>
becomes the JNDI name of the EJB, which is what its clients will look up.
     In this example, the EJB JAR will be built from files rooted in the directory [source_root]/servertarget, so the XML file needs to be saved as [source_root]/servertarget/META-INF/ejb-jar.xml.

Step 7: package the EJB into a JAR

We must package the EJB classes and the XML into a JAR file, respecting the directory structure. For example, change to the servertarget directory and execute:
jar cvf bmp_properly.jar *
If you are using Ant:
ant buildjar

Step 8: deploy the JAR

Deployment on jBoss is very straightforward: simply copy the JAR file into the directory [jboss_root]/deploy. jBoss does some checks on the structure of the JAR file and the classes it contains and, if successful, you will see an output similar to the following in the console log:
[J2EE Deployer Default] Deploy J2EE application: file:/home/kevin/lib/JBoss-2.2.2/deploy/bmp_properly.jar 
[J2EE Deployer Default] Create application bmp_properly.jar
[J2EE Deployer Default] install module bmp_properly.jar
[Container factory] Deploying:file:/home/kevin/lib/JBoss-2.2.2/tmp/deploy/Default/bmp_properly.jar
[Verifier] Verifying file:/home/kevin/lib/JBoss-2.2.2/tmp/deploy/Default/bmp_properly.jar/ejb1002.jar
[Container factory] Deploying Customer1
[Container factory] Deployed application: file:/home/kevin/lib/JBoss-2.2.2/tmp/deploy/Default/bmp_properly.jar
[J2EE Deployer Default] J2EE application: file:/home/kevin/lib/JBoss-2.2.2/deploy/bmp_properly.jar is deployed.
If you are using Ant, just do this:
ant deploy
(Please note that this deploys the other versions of the EJB as well, but that shouldn't cause any problems).

Step 9: Test the EJB

We need a test client that can exercise all the functionality of the EJB. The client shown in the listing below carries out the following operations on the EJB:

package com.web_tomorrow.bmp_properly.client;

import javax.ejb.*;
import java.rmi.*;
import javax.rmi.*;
import javax.naming.*;
import java.util.*;
import com.web_tomorrow.bmp_properly.ejb.*;

/**
Test client for the `Customer' EJB
(c)2001 Kevin Boone/Web-Tomorrow
*/
public class Test 
{
public static void main (String[] args) throws Exception
  {
  if (args.length != 1) 
    {
    System.err.println ("Specify EJB JNDI name on command line");
    System.exit(-1);
    }

  System.setProperty("java.naming.factory.initial",
            "org.jnp.interfaces.NamingContextFactory");
  System.setProperty("java.naming.provider.url",
            "localhost:1099");

  String ejbName = args[0];

  Context c = new InitialContext();
  Object o = c.lookup (ejbName);
  CustomerHome h = (CustomerHome) PortableRemoteObject.narrow 
    (o, CustomerHome.class); 
  System.out.println ("Locating customer '1'");
  try
    {
    Customer customer = h.findByPrimaryKey ("1");
    System.out.println ("Customer exists: deleting");
    customer.remove();
    }
  catch (ObjectNotFoundException e)
    {
    System.out.println ("Customer does not exist");
    }
  System.out.println ("Creating customer 1");
  Customer customer = h.create ("1", "Kevin Boone", "Palmers Green, London", "kb@kevinboone.com");
  System.out.println ("Properties of new Customer EJB:");
  dumpCustomer (customer);
  System.out.println ("Finding Customer EJBs matching name `Boone'");
  Collection cc = h.findByNameContaining("Boone");
  Iterator customers = cc.iterator();
  while (customers.hasNext())
    {
    customer = (Customer)customers.next();
    System.out.println ("Found customer with ID " + customer.getId());
    }
  }

// Dumps the properties of a Customer EJB
public static void dumpCustomer (Customer customer) throws RemoteException
  {
  String customerId = customer.getId();
  String customerName = customer.getName();
  String customerAddress = customer.getAddress();
  String customerEmail = customer.getEmail();
  System.out.println ("id=" + customerId);
  System.out.println ("name=" + customerName);
  System.out.println ("address=" + customerAddress);
  System.out.println ("email=" + customerEmail);
  }
}

To enable the test client to be used against different versions of the same EJB, I have coded it to take the JNDI name of the EJB on the command line. To run the test client the Java runtime needs to be able to find the following on its class search path: A suitable invocation might be:
java -classpath \
clienttarget:\
servertarget:\
[jboss_root]/lib/ext/jnpserver.jar:\
[jboss_root]/lib/ext/ejb.jar:\
[jboss_root]/lib/ext/jta-spec1_0_1.jar:\
[jboss_root]/lib/jboss-jaas.jar \
com.web_tomorrow.bmp_properly.client.Test Customer1
With Ant, just execute:
ant runclient1
You should see output similar to the following on the console where the client is executed:
Locating customer '1'
Customer exists: deleting
Creating customer 1
Properties of new Customer EJB:
id=1
name=Kevin Boone
address=Palmers Green, London
email=kb@kevinboone.com
Finding Customer EJBs matching name `Boone'
Found customer with ID 1
On the jBoss server console you should see a huge volume of output, showing all interactions between jBoss and the Customer EJB. Here is a typical trace; we will discuss this in more detail later.
[Customer1] Called setEntityContext
[Customer1] Entering ejbFindByPrimaryKey
[Customer1] Executing query: SELECT * FROM BMP_customer WHERE id='1'
[Customer1] Leaving ejbFindByPrimaryKey
[Customer1] Called ejbActivate
[Customer1] Entering ejbLoad
[Customer1] Executing query: SELECT * FROM BMP_customer WHERE id='1'
[Customer1] Leaving ejbLoad
[Customer1] Entering ejbRemove
[Customer1] Executing update: DELETE FROM BMP_customer WHERE id='1'
[Customer1] Leaving ejbRemove
[Customer1] Entering ejbCreate
[Customer1] Executing query: INSERT INTO BMP_customer (id,name,address,email) VALUES ('1','Kevin Boone','Palmers Green, London','kb@kevinboone.com')
[Customer1] Leaving ejbCreate
[Customer1] Called ejbPostCreate
[Customer1] Called getId
[Customer1] Entering ejbStore
[Customer1] Executing update: UPDATE BMP_customer SET name='Kevin Boone',address='Palmers Green, London',email='kb@kevinboone.com' WHERE id='1'
[Customer1] Leaving ejbStore
[Customer1] Called getName
[Customer1] Entering ejbStore
tomer1] Executing update: UPDATE BMP_customer SET name='Kevin Boone',address='Palmers Green, London',email='kb@kevinboone.com' WHERE id='1'
[Customer1] Leaving ejbStore
[Customer1] Called getName
[Customer1] Entering ejbStore
[Customer1] Executing update: UPDATE BMP_customer SET name='Kevin Boone',address='Palmers Green, London',email='kb@kevinboone.com' WHERE id='1'
[Customer1] Leaving ejbStore
[Customer1] Called getAddress
[Customer1] Entering ejbStore
[Customer1] Executing update: UPDATE BMP_customer SET name='Kevin Boone',address='Palmers Green, London',email='kb@kevinboone.com' WHERE id='1'
[Customer1] Leaving ejbStore
[Customer1] Called getEmail
[Customer1] Entering ejbStore
[Customer1] Executing update: UPDATE BMP_customer SET name='Kevin Boone',address='Palmers Green, London',email='kb@kevinboone.com' WHERE id='1'
[Customer1] Leaving ejbStore
[Customer1] Entering ejbFindByNameContaining
[Customer1] Executing query: SELECT * FROM BMP_customer WHERE name like '%Boone%'
[Customer1] Leaving ejbFindByNameContaining
[Customer1] Called getId
[Customer1] Entering ejbStore
[Customer1] Executing update: UPDATE BMP_customer SET name='Kevin Boone',address='Palmers Green, London',email='kb@kevinboone.com' WHERE id='1'
[Customer1] Leaving ejbStore
[Container factory] Called ejbPassivate

Limitations of the simple EJB

The EJB described so far works (I hope) perfectly well, but has a number of limitations that would make it unsuitable for a real application. We will discuss these limitations starting from the simplest to the most complex, then present a version of the EJB implementation class that does the job better.

Hard-coded JDBC datasource

Note how the EJB gets access to the JDBC datasource:
datasource = (DataSource)c.lookup("java:/DefaultDS");
The name `java:DefaultDS' is specific to jBoss, and indeed to a specific database in the jBoss default set-up. The use of a name like this would make the whole EJB non-portable. Of course, there are various common-sense ways we could get around this problem, such as having the EJB extract the datasource name form its environment, which could be manipulated through the deployment descriptor. However, the J2EE Specification sets out a standard way of doing name indirection in J2EE components: the use of `java:comp/env/' names. Names that begin with this prefix are not treated as plain JNDI names, but as logical names that can be redirected to real JNDI names at deployment time. So we should really code the lookup like this:
datasource = (DataSource)c.lookup("java:/comp/env/jdbc/bmp_properly");
The part of the string after the `java:comp/env/' prefix is technically known as the coded name. This is that part that must be mapped to the real JNDI name.
     To use coded names, we must declare their presence in the deployment descriptor; but note that the actual mapping is not in the same deployment descriptor. So the relevant descriptor entry for the example above is as follows:
<resource-ref>
  <res-ref-name>jdbc/bmp_properly</res-ref-name>
  <res-type>javax.sql.DataSource</res-type>
  <res-auth>Container</res-auth>
</resource-ref>
So where is the real JNDI name (java:/DefaultDS in this case)? This is vendor-specific; many developers find this a bit odd, but it's the case nonetheless. jBoss keeps this information in a separate file, called jboss.xml which needs to be deployed in the META-INF directory along with the real ejb-jar.xml. Here is a suitable XML file:
<jboss>
  <enterprise-beans>
    <entity>
      <ejb-name>Customer2</ejb-name>
      <resource-ref>
        <res-ref-name>jdbc/bmp_properly</res-ref-name>
        <resource-name>java:/DefaultDS</resource-name>
      </resource-ref>
    </entity>
  </enterprise-beans>
</jboss>
Since this will accompany a new version of the EJB which I will call Customer2, the ejb-name references Customer2. This file needs to be created in the directory [source_root]/servertarget/META-INF in this example.

Over-optimistic updates

Entity EJBs are always transactional, that is, they always run in some kind of transactional context. When using BMP, the container does not make any assumptions about what an EJB has to do to synchronize its internal state to the persistent store, it simply calls ejbStore, etc., at appropriate times with respect to transaction boundaries. Suppose that the client of an EJB application calls a number of methods on different EJBs, all as part of the same transaction. One of these methods may have carried out a database operation which failed, but the failure is only known to the container at this time. The client will continue to call other methods on the other EJBs. Now, when it comes to transaction completion time the container knows that it has to roll back the transaction. This means that any work done after the original failure is redundant, as it will get rolled back.
     In summary, it is good practice for an EJB that does database operations to check the transactional state before beginning work. If the transaction is already marked for rollback (by the container), then there is no value in carrying out the database operation. In this example, we can use a simple test like this at the start of every method that updates the database:
if (entityContext.getRollbackOnly())
  {
  logger.log (1, "Rollback flagged by container: giving up");
  return;
  }

Avoiding unnecessary updates

If you look closely at the debug output from the previous execution, you will see a number of container-EJB interactions like this:
[Customer1] Called getName
[Customer1] Entering ejbStore
tomer1] Executing update: UPDATE BMP_customer SET name='Kevin Boone',address='Palmers Green, London',email='kb@kevinboone.com' WHERE id='1'
[Customer1] Leaving ejbStore
The client has called getName(), and the container has delegated that call to the EJB instance. So far, so good. However, the container then goes on to call ejbStore(), despite the fact the the method getName() did not do anything the requires a database update. This is normal behaviour. The EJB container has no (straightforward) way of knowing whether the method updated any instance variables in such a way that a database update is required. Therefore it calls ejbStore anyway. This leads to a very important principle:
The container calls ejbStore() to give the EJB an opportunity to write its state to the database, not to compel it to do so. It is the developer's responsibility to author the EJB in such a way that redundant writes are avoided.
One way to do this is to maintain a flag in an instance variable that indicates whether a write will be required on the next call to ejbStore(). For example, we can implement the setXXX methods like this:
public void setName(final String name)
  {
  this.name = name;
  needWrite = true;
  }
The flag needWrite can then be checked on entry to ejbStore():
if (!needWrite)
    {
    logger.log (1, "Abandon ejbStore() because needWrite flag not set");
    return;
    }
and then reset on exit from ejbStore() because, we hope, the instance variables are synchronized to the database by that point.
     Where else must needWrite be set and reset? Well, it will need to be reset after ejbLoad() as, again, the database and the instance variables are in sync at that point. Similar logic applies to ejbCreate(). Finder methods do not change the state of the instance, so the needWrite flag is neither set nor reset here.
     Clearly it is profoundly important that anything that updates persistent instance variables also sets the needWrite flag. In this simple example it isn't difficult to assure this, but mistakes can easily arise in more complex EJBs. To reduce the likelihood of such errors, I recommend the following:
all methods that update any persistent instace variable do so by calling a `set' method, and that method in turn sets the write flag.
This applies not just be business methods that are called by clients, but to private methods within the EJB implementation as well. If all modifications of instance variables takes place through a `set' method, there is little scope for error.
     With these changes in mind, we can implement an improved EJB as shown in the listing below (CustomerBeanVersion1.java).

package com.web_tomorrow.bmp_properly.ejb;

import javax.ejb.*;
import java.rmi.*;
import java.sql.*;
import java.util.*;
import javax.sql.*;
import javax.naming.*;
import com.web_tomorrow.ejb_utils.*; 

public class CustomerBeanVersion2 implements EntityBean 
{
// Non-persistent instance variables
protected EntityContext entityContext;
protected Logger logger;
protected DataSource datasource;
protected boolean needWrite;

// Persistent instance variables
protected String id;
protected String name;
protected String address;
protected String email;

/**
Save the entity context for future use, and initialize the datasource so we can
use it to get connections in subsequent method calls
*/
public void setEntityContext (EntityContext entityContext)
  {
  logger = new Logger ("java:comp/env/debug");
  logger.log (2, "Called setEntityContext");
  this.entityContext = entityContext;
  try
    {
    Context c = new InitialContext();
    datasource = (DataSource)c.lookup("java:/comp/env/jdbc/bmp_properly");
    }
  catch (NamingException e)
    {
    logger.log (2, "Caught NamingException in setEntityContext: " + e);
    throw new EJBException ("Datasource name lookup failed: " + e);
    }
  }
  
/**
Does nothing in this example
*/
public void unsetEntityContext ()
  {
  logger.log (2, "Called unsetEntityContext");
  }
  
/**
Delete an entry from the database
*/
public void ejbRemove () throws RemoveException 
  {
  logger.log (2, "Entering ejbRemove");
  if (entityContext.getRollbackOnly())
    {
    logger.log (1, "Abandon ejbRemove() because rollback flagged by container");
    return;
    }
  try
    {
    id = (String)entityContext.getPrimaryKey();
    Connection c = datasource.getConnection();
    Statement s = c.createStatement();
    String q = "DELETE FROM BMP_customer WHERE id='" + id + "'";
    logger.log (2, "Executing update: " + q);
    s.executeUpdate(q);
    s.close();
    c.close();
    }
  catch (SQLException e)
    {
    // If we get an SQL exceptino here, convention dicates that we 
    // must throw a RemoveException to indicate to the client that
    // the deletion probably failed 
    logger.log (0, "Caught SQLException in ejbRemove: " + e);
    throw new RemoveException (e.toString());
    }

  logger.log (2, "Leaving ejbRemove");
  }
  
/**
Read data from the database. On entry `id' will be set correctly, because it
was set in ejbActivate(). All other persistent instance variables are
meaningless, and must be read from the database
*/
public void ejbLoad ()
  {
  logger.log (2, "Entering ejbLoad");
  // Whether ejbLoad() succeeds or fails, the needWrite flag should be reset.
  // After ejbLoad(), the instance variables will either be in sync with the
  //  database (success) or garbage (failure). In either case, a database update
  //  will not be required
  needWrite = false;
  try
    {
    Connection c = datasource.getConnection();
    Statement s = c.createStatement();
    String q = "SELECT * FROM BMP_customer WHERE id='" + id + "'";
    logger.log (2, "Executing query: " + q);
    ResultSet rs = s.executeQuery(q);
    rs.next(); // SHOULD always succeed: the container should never have to
               //  call on a non-existent row
    name = rs.getString ("name");
    address = rs.getString ("address");
    email = rs.getString ("email");
    rs.close();
    s.close();
    c.close();
    }
  catch (SQLException e)
    {
    // If we get an SQL exceptino here then something has gone horribly wrong.
    // Throw EJB exception to alert container
    logger.log (0, "Caught SQLException in ejbLoad: " + e);
    throw new EJBException (e);
    }

  logger.log (2, "Leaving ejbLoad");
  }
  
/**
Write data to the database
*/
public void ejbStore ()
  {
  logger.log (2, "Entering ejbStore");
  if (entityContext.getRollbackOnly())
    {
    logger.log (1, "Abandon ejbStore() because rollback flagged by container");
    return;
    }
  if (!needWrite)
    {
    logger.log (1, "Abandon ejbStore() because needWrite flag not set");
    return;
    }
  try
    {
    Connection c = datasource.getConnection();
    Statement s = c.createStatement();
    String q = "UPDATE BMP_customer " 
      + "SET name='" + name + "'," 
      + "address='" + address + "'," 
      + "email='" + email+ "' " 
      + "WHERE id='" + id + "'";
    logger.log (2, "Executing update: " + q);
    s.executeUpdate(q);
    s.close();
    c.close();
    needWrite = false;
    }
  catch (SQLException e)
    {
    // If we get an SQL exceptino here then something has gone horribly wrong.
    // Throw EJB exception to alert container
    logger.log (0, "Caught SQLException in ejbStore: " + e);
    throw new EJBException (e);
    }

  logger.log (2, "Leaving ejbStore");
  }
  
/**
Gets the primary key from the container. In ejbLoad and ejbStore, assume that
the primary key stored in the `id' field is correct 
*/
public void ejbActivate ()
  {
  logger.log (2, "Called ejbActivate");
  id = (String)entityContext.getPrimaryKey();
  }
  
/**
Does nothing in this implementation
*/
public void ejbPassivate ()
  {
  logger.log (2, "Called ejbPassivate");
  }
  
/**
ejbFindByPrimaryKey simply checks whether the data exists in the database; it
does not load it or set instance variables. The container will call ejbLoad
when it wants this to happen. According to the EJB Specification, we must throw
ObjectNotFoundException if the database entry does not exist
*/
public String ejbFindByPrimaryKey(final String id)
    throws FinderException
  {
  try
    {
    logger.log (2, "Entering ejbFindByPrimaryKey");
    boolean ok = false;
    Connection c = datasource.getConnection();
    Statement s = c.createStatement();
    String q = "SELECT * FROM BMP_customer WHERE id='" + id + "'";
    logger.log (2, "Executing query: " + q);
    ResultSet rs = s.executeQuery(q);
    if (rs.next()) ok = true;
    rs.close();
    s.close();
    c.close();
    if (ok)
      {
      logger.log (2, "Leaving ejbFindByPrimaryKey");
      return id;
      }
    logger.log (2, "Leaving ejbFindByPrimaryKey, by throwing exception");
    throw new ObjectNotFoundException ("No Customer with id=" + id);
    }
  catch (SQLException e)
    {
    // If we get an SQL exceptino here then something has gone horribly wrong.
    // Throw EJB exception to alert container
    logger.log (0, "Caught SQLException in ejbFindByPrimaryKey: " + e);
    throw new EJBException (e);
    }
  } 

/**
ejbFindByNameContaining returns a Collection of primary keys, one for each
row of the customer table whose `name' field contains the supplied string. 
If the String is empty, then a collection containing all EJBs is returned
to the client. Clearly this may well be very inefficient if the customer
table has many entries, and this action should be discouraged.
*/
public Collection ejbFindByNameContaining (final String search)
    throws FinderException
  {
  try
    {
    logger.log (2, "Entering ejbFindByNameContaining");
    Connection c = datasource.getConnection();
    Statement s = c.createStatement();
    String q = "SELECT * FROM BMP_customer WHERE name like '%" + search + "%'";
    logger.log (2, "Executing query: " + q);
    ResultSet rs = s.executeQuery(q);
    ArrayList al = new ArrayList();
    while (rs.next())
      {
      al.add (rs.getString("id"));
      }
    rs.close();
    s.close();
    c.close();
    logger.log (2, "Leaving ejbFindByNameContaining");
    return al;
    }
  catch (SQLException e)
    {
    // If we get an SQL exceptino here then something has gone horribly wrong.
    // Throw EJB exception to alert container
    logger.log (0, "Caught SQLException in ejbFindByNameContaining: " + e);
    throw new FinderException (e.toString());
    }
  } 

/**
ejbCreate must inspect the supplied arguments, create a database entry, and
return the primary key to the container. In this example this is
straightforward, since the primary key is just the `id' field, which is
supplied as a parameter by the client
*/
public String ejbCreate(final String id, final String name, 
  final String address, final String email) 
    throws CreateException
  {
  // We won't need a database update after this method: the instance variables
  //  and the database will be in sync
  needWrite = false;
  try
    {
    logger.log (2, "Entering ejbCreate");
    if (entityContext.getRollbackOnly())
      {
      logger.log (1, "Abandon ejbCreate() because rollback flagged by container");
      throw new CreateException("Create abandonned owing to prior rollback");
      }
    boolean ok = false;
    // Set the instance variables from the arguments
    this.id = id;
    this.name = name;
    this.address = address;
    this.email = email;
    Connection c = datasource.getConnection();
    Statement s = c.createStatement();
    String q = "INSERT INTO BMP_customer (id,name,address,email) VALUES ("
        + "'" + id + "',"
        + "'" + name + "',"
        + "'" + address + "',"
        + "'" + email + "')";
    logger.log (2, "Executing query: " + q);
    s.execute(q);
    s.close();
    c.close();
    logger.log (2, "Leaving ejbCreate");
    return id;
    }
  catch (SQLException e)
    {
    // If we get an SQL exceptino here then something has gone horribly wrong.
    // Throw EJB exception to alert container
    logger.log (0, "Caught SQLException in ejbCreate: " + e);
    throw new EJBException (e);
    }
  }

public void ejbPostCreate(final String id, final String name, 
  final String address, final String email) 
  {
  logger.log (2, "Called ejbPostCreate");
  }

// Business methods: only `set' and `get' in this example

public String getId() 
  {
  logger.log (2, "Called getId");
  return id;
  }

// Note no `setId()' : primary key can't be set

public String getName() 
  {
  logger.log (2, "Called getName");
  return name;
  }

public void setName(final String name) 
  {
  logger.log (2, "Called setName");
  this.name = name;
  needWrite = true;
  }

public String getAddress() 
  {
  logger.log (2, "Called getAddress");
  return address;
  }

public void setAddress(final String address) 
  {
  logger.log (2, "Called setAddress");
  this.address = address;
  needWrite = true;
  }

public String getEmail() 
  {
  logger.log (2, "Called getEmail");
  return email;
  }

public void setEmail(final String email) 
  {
  logger.log (2, "Called setEmail");
  this.email = email;
  needWrite = true;
  }
}



To run this new EJB, just specify Customer2 as the argument to the test client; if you are using Ant, just do
ant runclient2

Further improvements

The EJB still has some significant limitations, which could be addressed with a bit of extra work.

Use of value objects

In some cases the client of the EJB will need to get a remote reference to the EJB so that it can update it. This is what the client in the example does. Having got a reference to a Customer object, it can call getXXX and setXXX methods and expect the EJB to maintain itself in sync with the database.
     The problem with this technique is that a method call like getName() is a network operation. It requires the set-up of a TCP/IP connection, calls through the stub and skeleton, and marshalling and unmarshalling of the return value from the method, all of which are overheads. The client in the example above made four similar method calls on EJB instance, and in no case did it attempt to update any data. Large performance gains can be achieved by recognizing that often clients merely want to retrieve data from the EJB, and not set it. To support this mode of operation it makes sense to provide a single method call that returns all the relevant data items in one method. Typically we will use a separate class for this purpose. In J2EE jargon, such a class is called a value object. In the example described in this article we could create a class called, for example, CustomerData with instance variables id, name, address, etc. The EJB would then have a business method getCustomerData() that returns all the instance variables of the EJB in a single CustomerData instance.
     Using value objects reduces the network communication overhead, but caution is required as they contain only a snapshot of the data at any given time. Unless we are careful with the transaction attributes of the EJBs, it is easy for the data in the database to get out of sync with respect to the value object (even though not with respect to the EJB). In addition, if the client modifies a value in the value object, that change does not get propagated back to the EJB. There is thus increasing interest in `smart' value objects, that maintain their association with the EJB that generated them. When the client modifies the value object, it automatically propagates the change back to the EJB and therefore to the database. Thus a smart value object is a proxy for the EJB on the client side. Although interesting, these techniques are beyond the scope of this article.

Session-layer proxies

In the example presented, the client called only getXXX() methods on the EJB, and some performance gain could be expected by using value objects to carry multiple data elements in one method call. However, suppose that the client carried out a number of operations, some of which updated the EJB and some of which retrieved values? This is far from being an uncommon situation. The use of value objects won't help much here. For every setXXX and getXXX method the container is going to call one or more of the methods ejbLoad(), ejbStore(), ejbActivate() and ejbPassivate() in an attempt to keep the database and the EJB in sync. Or is it? This was the behaviour in the example because client had not specifically demarcated transaction boundaries. In other words, the container had to begin a new database transaction for each business method call, and commit the transaction at the end of the method call. This was necessary because the container had no better information about the scope of the current database transaction. Because a single transaction did not span multiple methods, the container could not assume that any of the data in the database was locked, so frequent calls to ejbLoad() and ejbStore() were required. But suppose the client initiated a new transaction before calling a large number of EJB operations, and then committed the transaction at the end. A relatively smart container will recognize that it only has to call ejbLoad() once (after beginning the transaction), and ejbStore() once (just before committing the transaction). This discussion should make it evident that:
getting good performance from entity EJBs requires a detailed understanding of the transactional properties of the application, and of techniques that containers use to synchronize EJB load/store operations to transaction boundaries.
It is difficult, if not impossible, for a stand-alone client to interact with the EJB container's transaction manager in such a way as to get this effect. However, there is a simple solution:
wrap up multiple entity EJB operations within a session EJB. Ensure that the session EJB begins a transaction that encompasses the entity operations, or has transaction attributes that cause the container to do so.
This is one of the reasons that most authorities recommend the use of session EJBs as a facade to an entity EJB system. Other reasons include the ability to cache entity data in the session EJBs, and the provision of a `coarse-grained' interface to clients.
     Demarcating a transaction in a session EJB can be done using a scheme something like this:
UserTransaction ut = sessionContext.getUserTransaction();
ut.begin()
// Entity EJB operation 1...
// Entity EJB operation 2...
// Entity EJB operation 3...
ut.commit();
Alternatively we can specify container-managed transactions for the session EJB method, and set the transaction attribute to Requires or RequiresNew as appropriate.

Summary

This article has demonstrated the principles of constructing an entity EJB using bean-managed persistence. While it is relatively simple to implement an EJB that is basically functional, it is more difficult to achieve good performance. Adequate results are unlikely to be achieved with the naive implementations found in elementary textbooks and courses. This article has demonstrated some of the techniques that a `proper' implementation may use, including the use of value objects, avoidance of unnecessary database operations, and consideration of the effects of transaction boundaries.

©1994-2003 Kevin Boone, all rights reserved