|
©1994-2003 Kevin Boone |
| Home Section index K-Zone home | Software development |
|
IntroductionWhat this article is aboutThis 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 stuff1. 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 THISTo 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).
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.
this is \ a long lineshould be reassembled into one line: this is a long lineMost 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 overviewThe 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-stepStep 1: set up the source treeIf 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
Step 2: edit build.xmlIf you plan to useAnt, 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.
Step 3: compile the sample data loaderThis tutorial requires that the database be pre-loaded with data; this is supplied as an SQL scriptinit.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 dataThe 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:
-Djdbc.drivers=org.hsql.jdbcDriverFor 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 installdatawhich 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 EJBHome interfaceThe 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 compulsoryfindByPrimaryKey(), 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 interfaceThe 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 classThe 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.
CompilationFor compilation, thejavac 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/*.javaAlternatively, 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 descriptorThe deployment descriptor is a fileejb-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 JARWe must package the EJB classes and the XML into a JAR file, respecting the directory structure. For example, change to theservertarget directory and
execute:
jar cvf bmp_properly.jar *If you are using Ant:
ant buildjar Step 8: deploy the JARDeployment 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 EJBWe 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:
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 Customer1With Ant, just execute:
ant runclient1You 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 1On 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 EJBThe 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 datasourceNote 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 updatesEntity 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 callsejbStore, 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 updatesIf 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 ejbStoreThe 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
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 `
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 improvementsThe EJB still has some significant limitations, which could be addressed with a bit of extra work.Use of value objectsIn 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 aCustomer 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 proxiesIn the example presented, the client called onlygetXXX() 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.
SummaryThis 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. |