Tema 4: Un ejemplo completo de bean de entidad con persistencia gestionada por el bean

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:

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:

4.1 Clase EntityBean

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:

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:

Además, un bean de entidad con persistencia gestionada por el bean debe cumplir los siguientes requisitos:

4.1.1 La interfaz EntityBean

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.

4.1.2 El método ejbCreate

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:

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.

4.1.3 El método ejbPostCreate

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:

La cláusula throws puede incluir una excepción javax.ejb.CrateException y excepciones que son específicas a la aplicación cliente.

4.1.4 El método ejbRemove

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.

4.1.5 Los métodos ejbLoad y ejbStore

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());
   }
}

4.1.6 Los métodos de búsqueda

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:

4.1.7 Los métodos de negocio

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:

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.

4.1.8 Los métodos Home

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:

La cláusula trhows puede incluir excepciones específicas de la aplicación y no debe arrojar la excepción java.rmi.RemoteException.

4.1.9 Llamadas a la base de datos

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();
}

4.2 Interfaz Home

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;
  }

4.2.1 Definición de métodos create

Cada método create en la interfaz home debe cumplir los siguientes requisitos:

4.2.2 Definición de los métodos de búsqueda

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:

4.2.3 Definiciones de métodos Home

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).

4.3 Interfaz remota

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:

Una interfaz local tiene los mismos requisitos, con las siguientes excepciones:

4.4 El descriptor de despliegue

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>