Introducción a EJB 3.1 (IV)

En los artículos anteriores del tutorial de introducción a EJB 3.1, hemos visto como declarar y trabajar con componentes de lado del servidor (Session Beans y Message-Driven Beans). El último componente que nos queda por ver es Entity Beans (EB - Beans de Entidad; a partir de ahora nos referiremos a ellos como entidades).

4.1 Entidades: conceptos básicos

Las entidades, a diferencia del resto de componentes EJB, son objetos Java reales que son manejados entre componentes (o entre un cliente y un componente) en su forma original, nunca a través de proxys/vistas. Podemos crearlos con sentencias new, pasarlos como parámetros a un Session Bean, etc. Pero el verdadero valor de las entidades reside en que su estado puede ser almacenado en una base de datos, y más tarde recuperado en un nuevo objeto del tipo correspondiente. De manera adicional, los cambios que realicemos en el estado de una entidad serán sincronizados con la información que tenemos almacenada en la base de datos.

Aunque las entidades se consideran componentes EJB, hasta la version JavaEE 1.4 pertenecían a una especificación independiente llamada Java Persistence API (JPA - API de Persistencia en Java). Fue a partir de la versión 5 de JavaEE que la especificación EJB absorvió a la especificación JPA. Sin embargo, podemos trabajar con entidades en una aplicación no-EJB, aunque, tras bambalinas, se seguirán ejecutando dentro de un contenedor EJB. Desde ahora usaremos el término aplicación JPA para referirnos a aplicaciones que realizan persistencia, y el término aplicación EJB cuando exista integración de ambas tecnologías.

En este artículo no vamos a tratar en profundidad la especificación JPA (ni la declaración ni el uso de entidades), si no como integrarla con una aplicación EJB 3.1. Si no conoces JPA, es prácticamente obligatorio que visites el tutorial de JPA publicado en este mismo blog, de manera que puedas comprender todo el material que sigue a continuación.

4.2 Entidades: el ciclo de vida

Como ya se ha mencionado, las entidades no son objetos del lado del servidor. Sin embargo, cuando son usadas dentro del contexto de un contenedor EJB, se convierten en objetos gestionados (gracias al servicio de persistencia, el cual es controlado mediante la interface EntityManager). Esto nos lleva a los dos únicos estados de una entidad cuando se encuentra en el contexto de un contenedor EJB:

  • Gestionada (Attached)
  • No gestionada (Detached)

En el primer estado, la entidad se encuentra gestionada por el servicio de persistencia: cualquier cambio que realicemos en su estado se verá reflejado en la base de datos subyacente. En el segundo estado, la entidad es un objeto Java regular, y cualquier cambio que realicemos en su estado no será sincronizado con la base de datos subyacente. En este último estado, la entidad puede ser, por ejemplo, enviada a traves de una red (mediante serialización).

4.3 Entidades: unidad de persistencia

Una unidad de persistencia (persistence unit) representa un conjunto de entidades que pueden ser mapeadas a una base de datos, así como la información necesaria para que la aplicación JPA pueda acceder a dicha base de datos. Se define mediante un archivo llamado persistence.xml, el cual debe acompañar a la aplicación donde se realizan las tareas de persistencia (recuerda que puede ser una aplicación EJB o una aplicación Java normal):

 
 
     
     
         
         

     

	

Podemos definir más de una unidad de persistencia por aplicación, declarando cada una de ellas mediante el elemento XML <persistence-unit>. Cada unidad de persistencia debe seguir estas dos reglas:

  • Debe proporcionar un nombre (identidad) a través del cual pueda ser llamado
  • Debe conectar a una sola fuente de datos (data source)

Dependiendo del tipo de paquete que estemos construyendo (EAR, JAR, etc), el archivo persistence.xml deberá encontrarse en una localización u otra. En la sección 4.6 veremos un ejemplo completo de este archivo, así como la información necesaria para su correcto despliegue dentro de una aplicación EJB.

4.4 Entidades: contexto de persistencia

Otro concepto que tenemos que tener claro es el de contexto de persistencia. Un contexto de persistencia representa un conjunto de instancias de entidades que se encuentran gestionadas en un momento dado. Existen dos tipos de contextos de persistencia:

  • Limitados a una transacción (Transaction-scoped)
  • Extendidos (Extended)

Cuando trabajamos dentro de un contexto de persistencia limitado a una transancción, todas las entidades gestionadas pasarán a estar no gestionadas cuando dicha transacción finalice. Dicho con otras palabras, los cambios realizados tras finalizar la transacción no serán sincronizados con la base de datos.

Cuando trabajamos dentro de un contexto de persistencia extendido, las cosas funcionan de manera diferente: el contexto de persistencia sobrevivirá a la transacción donde se ejecuta, de manera que los cambios que realicemos en el estado de las entidades gestionadas por el contexto de persistencia se sincronizarán con la base de datos en el momento en que entremos en una nueva transacción. Este comportamiento es útil cuando trabajamos con SFSB, pues permite mantener un estado conversacional y mantener nuestras entidades sincronizadas.

4.5 Entidades: EntityManager

Mediante la interface EntityManager (Gestor de entidades) tenemos acceso al servicio de persistencia de nuestro contenedor. Podemos obtener una instancia de EntityManager en nuestros componentes EJB mediante inyección de dependencias:

import javax.ejb.Stateless; 
import javax.persistence.EntityManager; 
import javax.persistence.PersistenceContext; 

@Stateless 
public class MiSlsb { 

    @PersistenceContext(unitName="introduccionEJB") 
    private EntityManager em; 
     
    // operaciones del SLSB
}
	

El ejemplo anterior inyecta una instancia de EntityManager en el SLSB mediante la anotación @PersistenceContext. A esta anotación hay que proporcionarle como atributo el nombre de la unidad de persistencia que EntityManager usará para realizar la persistencia (y que hemos definido en el archivo persistence.xml). Con los metadatos proporcionados obtendremos por defecto un contexto de persistencia limitado a una transacción (ver sección anterior); si deseamos obtener un contexto de persistencia extendido (solo válido en SFSB por su naturaleza conversacional), debemos añadir el atributo type con el valor correspondiente para este comportamiento:

import javax.persistence.PersistenceContextType; 

// ... 

@PersistenceContext(unitName="introduccionEJB", type=PersistenceContextType.EXTENDED) 
private EntityManager em;
	

En lo que se refiere a este tutorial, la integración de EJB con JPA termina aquí. Por tanto, podemos pasar a ver un ejemplo donde conectaremos todas las piezas para realizar persistencia mediante EJB 3.1.

4.6 Entidades: un sencillo ejemplo

Veamos un sencillo ejemplo de un componente EJB realizando persistencia sobre una base de datos. Al igual que algunos ejemplos anteriores, este consta de varias partes:

  • Una fuente de datos (data source) donde realizar la persistencia
  • Una aplicación EJB donde se realiza las acciones de persistencia
  • Un cliente EJB desde el que intercambiar entidades con la aplicación EJB

Nuestro primer paso va a ser definir una fuente de datos que conectará nuestro contenedor EJB con nuestra base de datos. Siguiendo el entorno de desarrollo configurado en el anexo que acompaña este tutorial, escribimos un archivo llamado derby-ds.xml y lo guardamos en el directorio server\default\deploy de nuestra instalación de JBoss:

 
 
 
     DerbyDS 
     jdbc:derby://localhost:1527/introduccionEJB;create=true 
     org.apache.derby.jdbc.ClientDataSource40 
      
      
 

	

En el archivo XML anterior definimos una fuente de datos (data source) limitado a transacciones locales, esto es, dentro del propio contenedor (existe otro ambito de ejecución de una transacción llamado extendido, capaz de realizar su trabajo a través de múltiples contenedores). A esta fuente de datos le hemos dado un nombre JNDI desde la que poder invocarla, así como los parámetros de conexión con nuestra base de datos subyacente (url, usuario, y password). Si JBoss 6.0.0 Final está arrancado, nada más guardar el archivo anterior la fuente de datos será activada y se producirá la conexión con Derby, así que deberás tener la base de datos iniciada o se producirá un error; nosotros vamos a considerar que en este preciso momento tanto JBoss como Derby están parados.

El siguiente paso es crear un proyecto EJB 3.1 en Eclipse, y continuación levantar la base de datos Derby desde el propio IDE. Para ello, haz click con el botón derecho sobre el nombre del proyecto EJB en la pestaña Project Explorer y selecciona:

Apache Derby > Add Apache Derby nature
	

La operación anterior añade a nuestro proyecto las librerias del servidor embebido Derby, de manera que podamos conectar con él. A continuación levantamos la base de datos haciendo click con el botón derecho sobre el nombre del proyecto EJB en la pestaña Project Explorer y seleccionando:

Apache Derby > Start Derby Network Server
	

Nos aparecerá una ventana donde se nos informa que la base de datos está siendo levantada, haz click en el botón OK para finalizar este proceso. En un entorno en producción, todas estas operaciones serían innecesarias, pues teóricamente tendríamos una base de datos externa funcionando de manera continua.

Ahora vamos a crear un componente SLSB remoto, de manera que podamos llamarlo desde un cliente Java normal. Como recordarás, un componente remoto requiere de manera obligatoria implementar una interface:

package es.davidmarco.ejb.slsb; 

import es.davidmarco.ejb.entidad.Cuenta; 

public interface OperacionesConCuentas { 
    public void crearCuenta(Cuenta cuenta);     
    public Cuenta obtenerCuenta(Long id); 
    public void borrarCuenta(Cuenta cuenta); 
} 
	

Ahora ya podemos implementar el componente remoto:

package es.davidmarco.ejb.slsb; 

import javax.ejb.Remote; 
import javax.ejb.Stateless; 
import javax.persistence.EntityManager; 
import javax.persistence.PersistenceContext; 
import es.davidmarco.ejb.entidad.Cuenta; 

@Remote 
@Stateless 
public class OperacionesConCuentasRemote implements OperacionesConCuentas { 

    @PersistenceContext(unitName="introduccionEJB") 
    private EntityManager em; 
     
    @Override 
    public void crearCuenta(Cuenta nuevaCuenta) { 
        em.persist(nuevaCuenta); 
    } 

    @Override 
    public Cuenta obtenerCuenta(Long id) { 
        return em.find(Cuenta.class, id); 
    } 

    @Override 
    public void borrarCuenta(Cuenta cuenta) { 
        em.remove(cuenta);         
    } 
}
	

El componente anterior es extremadamente sencillo: en él se inyecta una instancia de EntityManager, la cual es usada en los métodos del Session Bean para realizar las tareas de persistencia. Estos métodos usan como parámetros o tipos de retorno instancias de la clase Cuenta (la cual define cuentas bancarias) y que será nuestra entidad:

package es.davidmarco.ejb.entidad; 

import java.io.Serializable; 

import javax.persistence.Entity; 
import javax.persistence.GeneratedValue; 
import javax.persistence.Id; 

@Entity 
public class Cuenta implements Serializable { 
    private static final long serialVersionUID = 1L; 
     
    @Id 
    @GeneratedValue 
    private Long id; 
    private String numeroDeCuenta; 
    private String nombreDelTitular; 
    private Double saldo; 



    public Long getId() { 
        return id; 
    } 

    public void setId(Long id) { 
        this.id = id; 
    } 

    public String getNumeroDeCuenta() { 
        return numeroDeCuenta; 
    } 

    public void setNumeroDeCuenta(String numeroDeCuenta) { 
        this.numeroDeCuenta = numeroDeCuenta; 
    } 

    public String getNombreDelTitular() { 
        return nombreDelTitular; 
    } 

    public void setNombreDelTitular(String nombreDelTitular) { 
        this.nombreDelTitular = nombreDelTitular; 
    } 

    public Double getSaldo() { 
        return saldo; 
    } 

    public void setSaldo(Double saldo) { 
        this.saldo = saldo; 
    } 

    public void aumentarSaldo(Double cantidad) { 
        saldo += cantidad; 
    } 

    public void reducirSaldo(Double cantidad) { 
        saldo -= cantidad; 
    } 
}
	

Algunos detalles de la entidad definida en el ejemplo anterior merecen una pequeña explicación: para empezar, nuestra entidad implementa la interface Serializable, necesaría cuando nuestra entidad va a viajar a través de una red (en nuestro caso entre el cliente Java y el contenedor EJB). Dentro de la entidad definimos 4 propiedades: una para la identidad de la entidad, y tres para representar su estado (numeroDeCuenta, nombreDelTitular, y saldo), más sus correspondientes métodos getter/setter. De manera adicional, hemos añadido algunas operaciones de lógica de negocio (aumentarSaldo() y reducirSaldo) dentro de la entidad; es una buena práctica que las operaciones relacionadas con cuentas estén dentro de la clase que representa dichas cuentas.

Ahora que tenemos un componente EJB y una entidad, vamos a crear la unidad de persistencía asociada a la fuente de datos y nuestra entidad. Crea un archivo llamado persistence.xml en el directorio META-INF del proyecto EJB:

 
 
     
        org.hibernate.ejb.HibernatePersistence         
        java:/DerbyDS 
        es.davidmarco.ejb.entidad.Cuenta 
         
         
             
             
         
     

	

En el archivo XML anterior hemos declarado una unidad de persistencia con nombre introduccionEJB (que fue el que usamos como parámetro de la anotación @PersistenceContext en el componente SLSB que definimos previamente), así como transacciones de tipo JTA (gestionadas por el contenedor; los tipos de transacción se explicaron en las secciones 3.2 y 3.3 del tercer artículo del tutorial de JPA).

Ya dentro de la declaración de la unidad de persistencia, hemos declarado el proveedor de persistencia que usaremos (HibernatePersistence), la dirección JNDI de la fuente de datos que declaramos en el archivo derby-ds.xml, y las entidades que gestionaremos en esta unidad de persistencia (en nuestro caso solamente Cuenta). Por último, hemos configurado algunos detalles relativos a la fuente de datos dentro del elemento <properties>: el dialecto que usará el contenedor para construir sentencias SQL adecuadas a nuestra base de datos, y la creación automática de los esquemas necesarios en base a los metadatos de nuestras entidades (de esta manera nos evitamos crear manualmente tanto las tablas como sus columnas, incluyendo la definición del tipo de dato de cada columna, restricciones de cada columna, etc). Ahora ya podemos desplegar la aplicación EJB en el contenedor.

El último paso necesario para probar nuestro ejemplo es crear un cliente Java que pasará una instancia ya inicializada de la entidad Cliente al componente EJB. Para ello, y para mantener las cosas sencillas, creamos un proyecto Java en Eclipse y le añadimos las librerias del proyecto EJB haciendo click con el botón derecho del ratón en el nombre del proyecto Java y seleccionando:

Build Path > Configure Build Path
	

En la ventana que nos aparece, vamos a la pestaña Projects, hacemos click en el botón Add, seleccionamos el proyecto EJB donde esta nuestro componente SLSB y nuestra entidad, hacemos click en el botón OK, y de nuevo hacemos click en el botón OK. Ahora ya podemos escribir el cliente:

package es.davidmarco.ejb.cliente; 

import java.util.Properties; 
import javax.naming.Context; 
import javax.naming.InitialContext; 
import javax.naming.NamingException; 
import es.davidmarco.ejb.entidad.Cuenta; 
import es.davidmarco.ejb.slsb.OperacionesConCuentas; 

public class Cliente { 
    private static final String JNDI_BEAN = "OperacionesConCuentasRemote/remote"; 

    public static void main(String[] args) throws NamingException { 
        Properties properties = new Properties(); 
        properties.put("java.naming.factory.initial", "org.jnp.interfaces.NamingContextFactory"); 
        properties.put("java.naming.factory.url.pkgs", "org.jboss.naming:org.jnp.interfaces"); 
        properties.put("java.naming.provider.url", "jnp://localhost:1099"); 
        Context context = new InitialContext(properties); 
         
        Cuenta cuenta = new Cuenta();         
        cuenta.setNumeroDeCuenta("0000-0001"); 
        cuenta.setNombreDelTitular("Nuevo cliente"); 
        cuenta.setSaldo(2500D); */ 
         
        OperacionesConCuentas occ = (OperacionesConCuentas)context.lookup(JNDI_BEAN); 
        occ.crearCuenta(cuenta); 
    } 
}
	

En el ejemplo anterior, creamos e inicializamos una cuenta, obtenemos un proxy/vista al componente EJB, e invocamos su método crearCuenta() pasándole como parámetro la cuenta. Esta entidad será serializada, viajará a traves de la red hasta el contenedor, será deserializada, y dentro del componente EJB será persistida en la base de datos. Podemos comprobarlo ejecutando una sentencia SQL contra la base de datos: para ello, haz click con el botón derecho sobre el nombre del proyecto EJB en la pestaña Project Explorer y selecciona:

Apache Derby > ij (Interactive SQL)
	

La consola de ij se abrirá en la pestaña Console de Eclipse, y desde el prompt de ij nos conectamos a la base de datos mediante el comando:

connect 'jdbc:derby://localhost:1527/introduccionEJB;'
	

Cuando la conexión se haya establecido, volveremos a ver el prompt de ij. Ahora ya podemos realizar la consulta SQL mediante el comando:

select * from cuenta;
	

La consola de ij nos mostrará un registro en la tabla Cuenta:

1    |Nuevo cliente    |0000-0001    |2500.0 
	

Esto demuestra que nuestra entidad (un POJO Java) ha sido almacenado en una base de datos relacional (tablas y columnas) de manera transparente para nosotros. Misión cumplida. Si deseáramos realizar el proceso inverso (obtener un objeto Java desde la información almacenada en la base de datos):

// ... 

public class Cliente { 
    private static final String JNDI_BEAN = "OperacionesConCuentasRemote/remote"; 

    public static void main(String[] args) throws NamingException { 
        Properties properties = new Properties(); 
        properties.put("java.naming.factory.initial", "org.jnp.interfaces.NamingContextFactory"); 
        properties.put("java.naming.factory.url.pkgs", "org.jboss.naming:org.jnp.interfaces"); 
        properties.put("java.naming.provider.url", "jnp://localhost:1099"); 
        Context context = new InitialContext(properties); 
         
        // Cuenta cuenta = new Cuenta();         
        // cuenta.setNumeroDeCuenta("0000-0001"); 
        // cuenta.setNombreDelTitular("Nuevo cliente"); 
        // cuenta.setSaldo(2500D); */ 
         
        OperacionesConCuentas occ = (OperacionesConCuentas)context.lookup(JNDI_BEAN); 
        // occ.crearCuenta(cuenta); 
        Cuenta cuenta = occ.obtenerCuenta(1L); 
        System.out.println("Titular de la cuenta " 
                + cuenta.getNumeroDeCuenta() 
                + " con saldo " 
                + cuenta.getSaldo() 
                + ": " 
                + cuenta.getNombreDelTitular()); 
    } 
}
	

En el ejemplo anterior hemos comentado las lineas que crean y persisten un cliente (de otra manera se insertará un nuevo registro con los misma información en la base de datos pero con ID con valor 2), y en su lugar hemos llamado al método obtenerCuenta() del SLSB, para así obtener una cuenta desde la base de datos en base a su ID. Si una cuenta con ese ID no existe, obtendremos como respuesta un valor null. Te invito a que, a modo de práctica, elimines de la base de datos la cuenta que hemos creado llamando al método borrarCuenta() del SLSB; para verificarlo, una vez hayas ejecutado esta operación vuelve a realizar la consulta SQL contra la base de datos a través de la consola de ij:

select * from cuenta;
	

Si has realizado correctamente el ejercicio, la tabla Cuenta no deberá mostrar ninguna entidad (salvo que hayas insertado entidades adicionales).

Resumen

Como hemos visto en este artículo, podemos realizar persistencia de manera extremadamente sencilla en nuestras aplicaciones EJB. Las entidades son simples POJO's, la configuración con la base de datos se configura mediante archivos XML, y en nuestros componentes EJB solo tenemos que inyectar una unidad de persistencia y ejecutar sus métodos.

En este punto ya hemos visto todos los componentes EJB (Session Beans, Message-Driven Beans, y Entities). Los dos próximos artículos estarán dedicados a los diversos servicios que ofrece el contenedor, servicios que nuestros componentes pueden usar para realizar taréas más complejas (y que son necesarias en aplicaciones reales).