El bean de entidad presentado en este ejemplo representa una sencilla cuenta
de banco. Los beans de entidad guardan su estado en algún dispositivo
secundario (fichero o base de datos). En este caso, el estado de las instancias
del bean SavingsAccountEJB
se almacenan en la tabla savingsaccount
de una base de datos relacional. Esta tabla se crea con la siguiente sentencia
SQL
CREATE TABLE savingsaccount (id VARCHAR(3) CONSTRAINT pk_savingsaccount PRIMARY KEY, firstname VARCHAR(24), lastname VARCHAR(24), balance NUMERIC(10,2));
El bean incluye métodos mediante los cuales las aplicaciones clientes pueden obtener instancias del bean (cuentas) que cumplan unos requisitos de balance, o cuentas de personas con un determinado apellido. También incluye un ejemplo de un método que se puede invocar desde la aplicación cliente para que se realice una determinada operación de mantenimiento sobre todas las instancias del bean.
Los beans de entidad tienen un conjunto de métodos que permiten el acceso a operaciones de bases de datos. La siguiente tabla resume los mismos:
La implementación remota del bean SavingsAccountEJB
requiere
las siguientes clases e interfaces:
SavingsAccountBean
)
SavingsAccountHome
)
SavingsAccountRemote
)
La lista de clases es la misma que para los beans de sesión, con la
excepción de la clase EntityBean, que en lugar de implementar la interfaz
SessionBean
deberá implementar la interfaz EntityBean
.
A continuación se detallan estas clases, junto con alguna clase auxiliar
y la aplicación ejemplo que va a usar el bean. En concreto, estas últimas
son:
InsufficientBalanceException
SavingsAccountClient
La clase EntityBean se llama SavingsAccountBean
. Esta clase define
los métodos a los que va a llamar el contenedor EJB cuando se requiera
el cliente realice una invocación a un método de la interfaz remota,
de la interfaz home o cuando haya que realizar alguna operación de gestión
del bean.
La diferencia fundamental con un bean de sesión son los métodos relacionados con con la persistencia de las instancias del bean. Entre ellas:
ejbLoad
y ejbStore
, que son métodos llamados por el contenedor
EJB cuando es necesario.
ejbFind
y permiten la búsqueda de una instancia concreta
o de una colección de instancias que cumplen una condición.
ejbHome
y permiten realizar una operación que afecta
a todas las instancias del bean.
La clase define los métodos públicos que agrupados en la siguiente tabla:
Métodos relacionados con la interfaz remota de las instancias del bean
public void debit(BigDecimal amount) public void credit(BigDecimal amount) public String getFirstName() public String getLastName() public BigDecimal getBalance() |
Métodos relacionados con la interfaz home
public void ejbHomeChargeForLowBalance(BigDecimal minimumBalance, BigDecimal charge) throws InsufficientBalanceException public String ejbCreate(String id, String firstName, String lastName, BigDecimal balance) throws CreateException public String ejbFindByPrimaryKey(String primaryKey) throws FinderException public Collection ejbFindByLastName(String lastName) throws FinderException public Collection ejbFindInRange(BigDecimal low, BigDecimal high) throws FinderException |
Métodos relacionados con el ciclo de vida del bean
public void ejbRemove() public void setEntityContext(EntityContext context) public void unsetEntityContext() public void ejbActivate() public void ejbPassivate() public void ejbLoad() public void ejbStore() public void ejbPostCreate(String id, String firstName, String lastName, BigDecimal balance) |
Resumiendo, la clase implementa:
EntityBean
ejbCreate
y ejbPostCreate
Además, un bean de entidad con persistencia gestionada por el bean debe cumplir los siguientes requisitos:
public
abstract
o
final
finalize
La interfaz EntityBean
extiende la interfaz EnterpriseBean
,
la cual a su vez extiende la interfaz Serializable
. La interfaz
EntityBean
declara una lista de métodos, como ejbActivate
y ejbLoad
, que deben implementarse en la clase bean entidad que
estemos definiendo (SavingsAccountBean
, en este caso). Estos métodos
se explican más adelante.
Cuando el cliente invoca un método create con unos determinados argumentos,
el contenedor EJB invoca el método correspondiente ejbCreate
.
Normalmente, un método ejbCreate
en un bean de entidad realiza
las siguientes tareas:
El método ejbCreate
de SavingsAccountBean
inserta el estado de la entidad en la base de datos invocando al método
privado insertRow
, el cual lanza una sentencia SQL INSERT. A continuación
se encuentra el código fuente del método ejbCreate
public String ejbCreate(String id, String firstName, String lastName, BigDecimal balance) throws CreateException { if (balance.signum() == -1) { throw new CreateException ("A negative initial balance is not allowed."); } try { insertRow(id, firstName, lastName, balance); } catch (Exception ex) { throw new EJBException("ejbCreate: " + ex.getMessage()); } this.id = id; this.firstName = firstName; this.lastName = lastName; this.balance = balance; return id; }
De la misma forma que ya vimos en el tema anterior, con el bean de sesión
CartEJB
, un bean de entidad puede tener múltiples métodos
ejbCreate
con distintas signaturas.
Cuando se escribe un método ejbCreate
de un bean de entidad
deben seguirse las siguientes reglas:
public
String
y el objeto corresponde
a la clave primaria
static
o final
La cláusula throws puede incluir la excepción javax.ejb.CrateException
y excepciones que sean específicas a la aplicación. Un método
ejbCreate
normalmente arroja una excepción CrateException
si un parámetro de entrada es inválido. Si un método ejbCrate
no puede crear una entidad a causa de que ya existe otra entidad con la misma
clave primaria, debería arrojar una excepción javax.ejbDuplicteKeyException
(una subclase de CreateException
). Si un cliente recibe una excepción
CreateException
o DuplicateKeyException
, debería
asumir que la entidad no ha sido creada.
La clase de clave primaria se especifica en el descriptor de despliegue del
bean de entidad. En la mayoría de los casos, la clase de clave primaria
será un String
, un Integer o alguna otra clase que pertenece
a las librerías estándar de J2SE o J2EE. Sin embargo, en algunos
casos será necesario definir una clase de clave primaria propia. Por
ejemplo, si el bean tiene una clave primaria compuesta (esto es, una compuesta
de múltiples campos), entonces es necesario crear una clase de clave
primaria.
Por último, el estado de un bean de entidad puede insertarse en la base
de datos directamente, por una aplicación desconocida al servidor J2EE.
Por ejemplo, un script SQL podría insertar una fila en la tabla savingsaccount
.
Aunque el bean de entidad correspondiente a esta fila no ha sido creado por
un método ejbCrate
, el bean puede ser obtenido y usado por
un programa cliente.
Para cada método ejbCreate
, se debe escribir un método
ejbPostCreate
en la clase bean de entidad. El contenedor EJB invoca
el método ejbPostCreate
inmediatamente después de
que llama a ejbCreate
. A diferencia de ejbCreate
,
el método ejbPostCreate
puede invocar los métodos
getPrimaryKey
y getEJBObject
de la interfaz EntityContext
.
En el tema anterior se comentó brevemente el método getEJBObject
en el apartado Cómo pasar una referencia al objeto enterprise bean.
A menudo, los métodos ejbPostCreate
se dejan vacíos.
La signatura de un método ejbPostCrate
debe conformar los
siguientes requisitos:
ejbCreate
equivalente
public
.
final
o static
.
void
La cláusula throws
puede incluir una excepción javax.ejb.CrateException
y excepciones que son específicas a la aplicación cliente.
Un cliente borra un bean de entidad invocando el método remove. Esta
invocación causa que el contenedor EJB llame al método ejbRemove
,
el cual borra el estado de la entidad de la base de datos. en la clase SavingsAccountBean
,
el método ejbRemove
invoca un método privado llamado
deleteRow
, el cual envía una sentencia SQL DELETE. El método
ejbRemove
es corto:
public void ejbRemove() { try { deleteRow(id); } catch (Exception ex) { throw new EJBException("ejbRemove: " + ex.getMessage()); } }
Si el método ejbRemove
encuentra un problema del sistema,
debería arrojar la excepción javax.ejb.EJBException
.
Si encuentra un error de aplicación debería arrojar una excepción
javax.ejb.RemoveException
.
Un bean de entidad también puede ser borrado directamente mediante un borrado de la base de datos. Por ejemplo, si un script SQL borra una fila que contiene un bean entidad, entonces el bean de entidad es eliminado.
El contenedor EJB invoca a los métodos ejbLoad
y ejbStore
cuando necesita sincronizar las variables de instancia de los beans de entidad
con los valores correspondientes almacenados en la base de datos. El métod
ejbLoad
actualiza el valor de las variables de instancia con los
valores de la base de datos, y el método ejbStore
escribe
las variables en la base de datos. El cliente no puede llamar directamente a
estos métodos.
Si un método de negocio está asociado con una transacción,
el contenedor invoca ejbLoad
antes de que el método de negocio
se ejecute. Inmediatamente después de que el método de negocio
se ejecuta, el contenedor llama a ejbStore
.
Si los métodos ejbLoad
y ejbStore
no pueden
localizar una entidad en la base de datos, deberían arrojar la excepción
javax.ejb.NoSuchEntityException
. Esta excepción es una subclase
de EJBException
. Debido a que EJBException
es una
subclase de RuntimeException
no es necesario incluirla en la cláusula
throws
. Cuando se lanza una excepción NoSuchEntityException
,
el contenedor la envuelve en una RemoteException
antes de devolverla
al cliente.
En la clase SavingsAccountBean
, ejbLoad
invoca el
método loadRow
, el cual lanza una sentencia SQL SELECT y
asigna los datos devueltos a las variables de instancia. El método ejbStore
llama al método privado storeRow
, que a su vez almacena
las variables de instancia en la base de datos con una sentencia SQL UPDATE.
A continuación se muestra el código fuente de ejbLoad
y ejbStore
:
public void ejbLoad() { try { loadRow(); } catch (Exception ex) { throw new EJBException("ejbLoad: " + ex.getMessage()); } } public void ejbStore() { try { storeRow(); } catch (Exception ex) { throw new EJBException("ejbStore: " + ex.getMessage()); } }
Los métodos de búsqueda (Finder) permiten a los clientes localizar
beans de entidad. El programa SavingsAccountClient
localiza beans
de entidad con tres métodos de búsqueda:
SavingsAccount jones = home.findByPrimaryKey("836"); ... Collection c = home.findByLastName("Smith"); ... Collection c = home.findInRange(20.00, 99.00);
Por cada método de búsqueda disponible para un cliente, la clase
de bean de entidad debe implementar un método correspondiente con el
prefijo ejbFind
. La clase SavingsAccountBean
, por
ejemplo, implementa el método ejbFindByLastName
como sigue:
public Collection ejbFindByLastName(String lastName) throws FinderException { Collection result; try { result = selectByLastName(lastName); } catch (Exception ex) { throw new EJBException("ejbFindByLastName " + ex.getMessage()); } return result; }
Los métodos de búsqueda específicos de la aplicación,
como ejbFindByLastName
y ejbFindInRange
, son opcionales.
Pero el método ejbFindByPrimaryKey
es obligatorio. Como
su nombre implica, el método ejbFindByPrimaryKey
acepta
como argumento la clave primaria, que usa después para localizar el bean.
En la clase SavingsAccountBean
, la clave primaria es la variable
id
. Este es el código del método ejbFindByPrimaryKey
:
public String ejbFindByPrimaryKey(String primaryKey) throws FinderException { boolean result; try { result = selectByPrimaryKey(primaryKey); } catch (Exception ex) { throw new EJBException("ejbFindByPrimaryKey: " + ex.getMessage()); } if (result) { return primaryKey; } else { throw new ObjectNotFoundException ("Row for id " + primaryKey + " not found."); } }
El método ejbFindByPrimaryKey
puede parecer extraño
a primera vista, debido a que usa una clave primaria como argumento y como valor
devuelto. Sin embargo, hay que recordar que el cliente no llama a ejbFindByPrimaryKey
directamente, sino que es el contenedor EJB el que llama al método. El
cliente invoca el método findByPrimaryKey
, que está
definido en la interfaz home.
La siguiente lista resume las reglas que deben seguir los métodos finder que se implementan en un bean de entidad con persistencia gestionada por el bean:
ejbFindByPrimaryKey
debe implementarse
obligatoriamente.
ejbFind
.
final
o static
.
Los métodos de negocio contienen la lógica de negocio que es
necesario encapsular dentro del bean de entidad. Normalmente, los métodos
de negocio no acceden a la base de datos permitiendo separar la lógica
de negocio de la implementación. La clase SavingsAccountBean
contiene los siguientes métodos de negocio:
public void debit(BigDecimal amount) throws InsufficientBalanceException { if (balance.compareTo(amount) == -1) { throw new InsufficientBalanceException(); } balance = balance.subtract(amount); } public void credit(BigDecimal amount) { balance = balance.add(amount); } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public BigDecimal getBalance() { return balance; }
El programa cliente SavingsAccountClient
invoca los métodos
de negocio de la siguiente forma:
BigDecimal zeroAmount = new BigDecimal("0.00"); SavingsAccount duke = home.create("123", "Duke", "Earl", zeroAmount); ... duke.credit(new BigDecimal("88.50")); duke.debit(new BigDecimal("20.25")); BigDecimal balance = duke.getBalance();
Los requesitos de las signaturas de un método de negocio son los mismos tanto para beans de sesión como para los beans de entidad:
ejbCreate
o
ejbActivate
.
public
.
final
o static
.
La cláusula throws puede incluir las excepciones propias de la aplicación.
El método debit
, por ejemplo, arroja la excepción
InsufficientBalanceException
. Para indicar un problema de nivel
de sistema, un método de negoci debería arrojar la excepción
javax.ejb.EJBException
.
Un método home contiene la lógica de negocio que se aplica a todos los beans de entidad de una clase particular. Sin embargo, la lógica en un método de negocio se aplica a un bean de entidad particular, una instancia con una identidad única. Durante la invocación de un método home, la instancia o bien no tiene una identidad única o bien no tiene un estado con el que representar un objeto de negocio. Por ello, un método home no debe acceder a las variables de instancia del bean. En el caso de la persistencia gestionada por el contenedor, un método home no puede tampoco acceder a relaciones.
Normalmente, un método home localiza una colección de instancias
de bean e invoca métodos de negocio de forma iterativa a todas las instancias.
En el caso de la clase SavingsAccountBean
, un ejemplo de método
home es ejbHomeChargeLowBalance
. Este método aplica un cargo
por servicio a todas las cuentas con balances menores de una determinada cantidad.
El método localiza estas cuentas invocando el método findInRange
.
Al tiempo que itera a travéss de la colección de instancias SavingAccounts
,
el método ejbHomeChargeForLowBalance
chequea el balance
e invoca el método debit
. He aquí el código
fuente de este método:
public void ejbHomeChargeForLowBalance( BigDecimal minimumBalance, BigDecimal charge) throws InsufficientBalanceException { try { SavingsAccountHome home = (SavingsAccountHome)context.getEJBHome(); Collection c = home.findInRange(new BigDecimal("0.00"), minimumBalance.subtract(new BigDecimal("0.01"))); Iterator i = c.iterator(); while (i.hasNext()) { SavingsAccount account = (SavingsAccount)i.next(); if (account.getBalance().compareTo(charge) == 1) { account.debit(charge); } } } catch (Exception ex) { throw new EJBException("ejbHomeChargeForLowBalance: " + ex.getMessage()); } }
La interfaz home define un método correspondiente llamado chargeForLowBalance
.
Ya que esta interfaz home es la que proporciona la vista del cliente, el programa
SavingsAccountClient
invoca el método home como sigue:
SavingsAccountHome home; ... home.chargeForLowBalance(new BigDecimal("10.00"), new BigDecimal("1.00"));
En una clase de bean de entidad, la implementación de un método home debe cumplir estas reglas:
ejbHome
public
.
static
.
La cláusula trhows puede incluir excepciones específicas de la
aplicación y no debe arrojar la excepción java.rmi.RemoteException
.
La siguiente tabla resume las llamadas de acceso a la base de datos en la clase
SavingsAccountBean
. Los métodos de negocio de la clase SavingsAccountBean
no aparecen en la tabla porque no accede a la base de datos. En su lugar, estos
métodos de negocio actualizan las variables de instancias y la modificación
de la base de datos sucede cuando el contenedor EJB llama a ejbStore
.
Se podría haber escogido otro enfoque, en el que los propios métodos
de negocio acceden a la base de datos. La decisión de hacerlo de una
forma u otra tiene que ver con la política de uso de transacciones.
Método | Sentencia SQL |
ejbCreate | INSERT un registro nuevo Devuelve la clave primaria al contenedor |
ejbFindByPrimaryKey | SELECT el registro con una clave primaria específica Devuelve la clave primaria al contenedor |
ejbFindXXX (ByLastName, InRange) |
SELECT uno o más registros Devuelve al contenedor la clave primaria o una colección de claves primarias |
ejbHomeXXX (ChargeForLowBalance) |
SELECT o UPDATE no ligado a una clave primaria específica |
ejbLoad | SELECT (refresca variables de instancia con valores de base de datos) |
ejbRemove | DELETE un registro |
ejbStore | UPDATE (almacena variables de instancia en base de datos) |
Antes de acceder a la base de datos hay que conectarse a ella. Eso se hace
en el método setEntityContext
. El nombre de la base de datos
a la que conectarse se define como una referencia mediante nombre JNDI ENC.
En el fichero de configuración de despliegue se define a qué nombre
JNDI real corresponde esta referencia. El método unsetEntityContext
se encarga de cerrar la conexión con la base de datos.
private String dbName = "java:comp/env/jdbc/SavingsAccountDB"; public void setEntityContext(EntityContext context) { this.context = context; try { makeConnection(); } catch (Exception ex) { throw new EJBException("Unable to connect to database. " + ex.getMessage()); } } private void makeConnection() throws NamingException, SQLException { InitialContext ic = new InitialContext(); DataSource ds = (DataSource) ic.lookup(dbName); con = ds.getConnection(); } public void unsetEntityContext() try { con.close(); } catch (SQLException ex) { throw new EJBException("unsetEntityContext: " + ex.getMessage()); } }
Para acceder a la base de datos se han definido unos métodos privados, como insertRow o deleteRow. Por ejemplo, el código fuente de insertRow es el siguiente:
private void insertRow (String id, String firstName, String lastName, BigDecimal balance) throws SQLException { String insertStatement = "insert into savingsaccount values ( ? , ? , ? , ? )"; PreparedStatement prepStmt = con.prepareStatement(insertStatement); prepStmt.setString(1, id); prepStmt.setString(2, firstName); prepStmt.setString(3, lastName); prepStmt.setBigDecimal(4, balance); prepStmt.executeUpdate(); prepStmt.close(); }
La interfaz home define los métodos que permiten a un cliente crear y encontrar un bean de entidad.
La interfaz SavingsAccountHome
sigue a continuación:
import java.util.Collection; import java.math.BigDecimal; import java.rmi.RemoteException; import javax.ejb.*; public interface SavingsAccountHome extends EJBHome { public SavingsAccount create(String id, String firstName, String lastName, BigDecimal balance) throws RemoteException, CreateException; public SavingsAccount findByPrimaryKey(String id) throws FinderException, RemoteException; public Collection findByLastName(String lastName) throws FinderException, RemoteException; public Collection findInRange(BigDecimal low, BigDecimal high) throws FinderException, RemoteException; public void chargeForLowBalance(BigDecimal minimumBalance, BigDecimal charge) throws InsufficientBalanceException, RemoteException; }
Cada método create
en la interfaz home debe cumplir los
siguientes requisitos:
ejbCreate
en la clase bean enterprise.
throws
en los métodos correspondientes
ejbCreate
y ejbPostCreate
.
javax.ejb.CreateException
.
RemoteException
.
Cada método de búsqueda en la interfaz home corresponde con un
método de búsqueda en la clase de bean de entidad. El nombre del
método finder en la interfaz home comienza con find
, mientras
que el correspondiente nombre en la clase de bean de entidad comienza por ejbFind
.
Por ejemplo, la clase SavingsAccountHome
define el método
findByLastName
, y la clase SavingsAccountBean implementa el método
ejbFindByLastName
. Las reglas para definir las signaturas de los
métodos finder son similares a las de los métodos home:
javax.ejb.FinderException
.
RemoteException
.
Cada definición de método home en la interfaz home corresponde
a un método home en la clase de bean de entidad. En la interfaz home,
el nombre del método es arbitrario, siempre que no comience por create
o find. En la clase bean, el nombre del método correspondiente es el
mismo precedido por ejbHome
. Por ejemplo, en la clase SavingsAccountBean
,
el nombre es ejbHomeChargeForLowBalance
, mientras que en la interfaz
SavingsAccountHome
el nombre es chargeForLowBalance
.
La signatura del método home debe seguir las misma reglas especificadas
para métodos finder en la sección previa (excepto que un método
home no arroja una excpción FinderException
).
La interfaz remota extiende javax.ejb.EJBObject
y define los métodos
de negocio que un cliente remoto puede invocar. He aquí la interfaz remota
SavingsAccountRemote
:
import javax.ejb.EJBObject; import java.rmi.RemoteException; import java.math.BigDecimal; public interface SavingsAccountRemote extends EJBObject { public void debit(BigDecimal amount) throws InsufficientBalanceException, RemoteException; public void credit(BigDecimal amount) throws RemoteException; public String getFirstName() throws RemoteException; public String getLastName() throws RemoteException; public BigDecimal getBalance() throws RemoteException; }
Los requisitos para las definiciones de métodos en una interfaz remota son los mismos que hemos visto en beans de sesión y beans de entidad:
throws
en los métodos correspondientes.
javax.ejb.FinderException
.
throws
incluye la excepción
RemoteException
.
Una interfaz local tiene los mismos requisitos, con las siguientes excepciones:
java.rmi.RemoteException
En la primera parte del fichero de despcripción del despliegue se definen las características básicas del despliegue del bean
<enterprise-beans> <entity> <display-name>SavingsAccountEJB</display-name> <ejb-name>SavingsAccountEJB</ejb-name> <home>SavingsAccountHome</home> <remote>SavingsAccount</remote> <ejb-class>SavingsAccountBean</ejb-class> <persistence-type>Bean</persistence-type> <prim-key-class>java.lang.String</prim-key-class> <reentrant>False</reentrant> <security-identity> <description></description> <use-caller-identity></use-caller-identity> </security-identity> <resource-ref> <res-ref-name>jdbc/SavingsAccountDB</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> <res-sharing-scope>Shareable</res-sharing-scope> </resource-ref> </entity> </enterprise-beans>
La segunda parte del fichero de descripción del despliegue contiene las características de seguridad y transacciones de cada uno de los métodos del EJB
<assembly-descriptor> <method-permission> <method> <ejb-name>SavingsAccountEJB</ejb-name> <method-intf>Home</method-intf> <method-name>create</method-name> <method-params> <method-param>java.lang.String</method-param> <method-param>java.lang.String</method-param> <method-param>java.lang.String</method-param> <method-param>java.math.BigDecimal</method-param> </method-params> </method> ... <container-transaction> <method> ... </method> <trans-attribute>Required</trans-attribute> </assembly-drescriptor>