Tema 6: Relaciones entre beans de entidad

Una importante característica de CMP 2.0 es la posibilidad de definir campos de relación entre beans. En este capítulo vamos a ver como ejemplo una relación uno-a-uno unidireccional entre los EJBs Customer y Address. El Customer tendrá una referencia a el Address, pero el Address no podrá referenciar hacia atrás el Customer. Son posibles también otras relaciones. Por ejemplo, cada Address podría referenciar su Customer con lo que tendríamos un ejemplo de una relación bidireccional uno-a-uno, en la que ambos EJBs mantienen referencias al otro. Además de relaciones uno-a-uno, los beans de entidad pueden tener relaciones uno-a-muchos, muchos-a-uno y muchos-a-muchos.

Existen en total siete tipo de relaciones posibles. Por un lado, tenemos cuatro combinaciones de cardinalidad: uno-a-uno, uno-a-muchos, muchos-a-uno y muchos-a-muchos. Y por otro lado tenemos dos posibles direcciones de la relación: unidireccional y bidireccional. Lo cual nos lleva a ocho posibles relaciones. Pero la relación bidireccional uno-a-muchos es obviamente idéntica a la relación muchos-a-uno, lo que nos deja con siete posibles relaciones. La mejor forma de entender estas relaciones es mediante algún ejemplo. Ahí van algunos sacados del dominio de negocio de las agencias de viajes.

Uno-a-uno, unidireccional

La relación entre un cliente y una dirección. Será necesario localizar una dirección de un cliente, pero probablemente no sea necesario localizar el alumno de una dirección.

Uno-a-uno, bidireccional

La relación entre un cliente y un número de tarjeta de crédito de la universidad. Dado un cliente, será necesario buscar su número de tarjeta de crédito. Dado un número de tarjeta de crédito también será necesario buscar el cliente que la tiene.

Uno-a-muchos, unidireccional

La relación entre un cliente y un número de teléfono. Un cliente puede tener muchos números de teléfono. Será necesario localizar un número de teléfono de un cliente, pero no será normal usar alguno de esos números para localizar al cliente.

Uno-a-muchos, bidireccional

La relación entre un crucero y una reserva. Dada una reserva, será necesario localizar el crucero para el que se ha hecho la reserva. Dado un crucero, será necesario buscar todas las reservas para ese crucero.

Muchos-a-uno, unidireccional

La relación entre un crucero y un barco. Será necesario saber qué barco ha sido usado para un crucero en particular. Y muchos cruceros usarán el mismo barco a lo largo del funcionamiento del mismo.

Muchos-a-muchos, unidreccional

La relación entre una reserva y un camarote. Es posible hacer una reserva para múltiples camarotes y va a ser necesario buscar el camarote asignado a una reserva particular.

Muchos-a-muchos, bidireccional

La relación entre un crucero y un cliente. Un cliente puede hacer reservas en muchos cruceros, y cada crucero tiene muchos clientes. Será necesario buscar tanto los cruceros en los que ha embarcado un cliente como los clientes que van en un crucero dado.

En nuestro caso, la relación va a ser entre clientes y direcciones. Va a ser uno-a-uno y será unidreccional.

6.1 Definición del bean dependiente

Los beans de entidad pueden formar relaciones con otros beans de entidad. En la figura siguiente que ya mostramos en el tema anterior, el EJB Customer tiene una relación uno-a-uno con el EJB Address.

El EJB Address es un objeto de negocio de grano fino que debería usarse en el contexto de otro bean, lo que significa que debería tener sólo interfaces locales. Un bean de entidad puede estar relacionado con muchos beans de entidad distintos al mismo tiempo. Por ejemplo, podríamos añadir fácilmente campos de relación para Phone, CreditCard, y otros beans de entidad al EJB Customer. No lo vamos a hacer, sin embargo, para mantener sencillo el ejemplo.

Siguiendo lo indicado en la figura anterior, definimos el EJB Address como sigue:

  
import javax.ejb.EntityContext;
  
public abstract class AddressBean implements javax.ejb.EntityBean {

  public Integer ejbCreateAddress(String street, String city, String state, String zip) {
     setStreet(street);
     setCity(city);
     setState(state);
     setZip(zip);
     return null;
  }
  public void ejbPostCreateAddress(String street, String city, String state, String zip) {
  }
  
  // campos de persistencia
  
  public abstract Integer getId();
  public abstract void setId(Integer id);
  public abstract String getStreet();
  public abstract void setStreet(String street);
  public abstract String getCity();
  public abstract void setCity(String city);
  public abstract String getState();
  public abstract void setState(String state);
  public abstract String getZip();
  public abstract void setZip(String zip);
  
  // metodos estandard callback
  
  public void setEntityContext(EntityContext ec){}
  public void unsetEntityContext(){}
  public void ejbLoad(){}
  public void ejbStore(){}
  public void ejbActivate(){}
  public void ejbPassivate(){}
  public void ejbRemove(){}
}

La clase AddressBean define un método ejbCreateAddress(), al que se llama cuando se crea un nuevo EJB Address, así como bastantes campos de persistencia (street, city, state, and zip). Los campos de persistencia se representan por métodos de acceso abstractos, lo requerido para los campos de persistencia en todas las clases de bean de entidad. Estos métodos de acceso abstractos se corresponden con un conjunto de elementos XML definidos en el fichero de descripción del despliegue. En tiempo de despliegue la herramienta de despliegue del contenedor hará corresponder los campos de persistencia del EJB Customer y del EJB Address con la base de datos. Esto significa que debe haber una tabla en nuestra base de datos relacional que contenga las columnas que emparejan con los campos de persistencia en el EJB Address. En este ejemplo, usaremos una tabla ADDRESS separada para almacenar la información sobre direcciones, pero los datos podrían haberse declarado fácilmente en la otra tabla:

CREATE TABLE ADDRESS (
  ID INT PRIMARY KEY NOT NULL, 
  STREET CHAR(40), 
  CITY CHAR(20),
  STATE CHAR(2),
  ZIP CHAR(10)
)

Los beans de entidad no tienen por qué definir todas las columnas de tablas correspondientes como campos de persistencia. De hecho, un bean de entidad puede incluso no tener una única tabla correspondiente; puede tener los campos de persistencia distribuidos entre varias tablas. Es la herramienta de despliegue del contenedor la que se encargará de definir el modo de persistencia del bean. En este caso, la columna ID es un campo auto-incrementado, creado automáticamente por el sistema de base de datos o el sistema contenedor. Es la clave primaria del EJB Address.

Una vez que se crea el bean, la clave primaria nunca puede ser modificada. Cuando las claves primarias son valores autogenerados, como la columna ID en la tabla ADDRESS, el contenedor EJB obtendrá el valor de la clave primaria a partir de la base de datos.

Además de la clase bean, definiremos el interfaz local para el EJB Address, lo que nos permitirá hacerlo accesible desde otros beans de entidad (esto es, desde el EJB Customer) dentro del mismo espacio de direcciones o proceso:

// Interfaz local del EJB Address
public interface AddressLocal extends javax.ejb.EJBLocalObject {
   public String getStreet();
   public void setStreet(String street);
   public String getCity();
   public void setCity(String city);
   public String getState();
   public void setState(String state);
   public String getZip();
   public void setZip(String zip);
}
  
// Interfaz local home de Address Address EJB's local home interface
public interface AddressHomeLocal extends javax.ejb.EJBLocalHome {
   public AddressLocal createAddress(String street,String city,
   String state,String zip) throws javax.ejb.CreateException;
   public AddressLocal findByPrimaryKey(Integer primaryKey)
   throws javax.ejb.FinderException;
}

El método ejbCreate() de la clase AddressBean y el método findByPrimaryKey() de la interfaz home definen ambos el tipo de la clave primaria como java.lang.Integer. En este caso la clave primaria es autogenerada. La mayoría de fabricantes EJB 2.0 permitarán hacer corresponder claves primarias de beans de entidad con campos autogenerados. En caso de que la herramienta no lo permitiera, tendríamos que definir el valor de la clave primaria en el método ejbCreate().

6.2. Definición de la relación en el bean que realiza la referencia

6.2.1. Campo de relación en la clase bean

El campo de relación para el EJB Address se define en la clase CustomerBean usando un método de acceso abstracto, de la misma forma que se declaran los campos de persistencias. En el código que sigue, la clase CustomerBean aparece modificada para incluir el EJB Address como un campo de relación

import javax.ejb.EntityContext; 
import javax.ejb.CreateException;
  
public abstract class CustomerBean implements javax.ejb.EntityBean { 
  ...
  
  // relaciones de persistencia
  public abstract AddressLocal getAddress();
  public abstract void setAddress(AddressLocal address);
  
  // campos de persistencia
  public abstract boolean getHasGoodCredit();
  public abstract void setHasGoodCredit(boolean creditRating);
  ...

Los métodos getAddress() y setAddress() son auto-explicativos; permiten al bean acceder y modificar su relación address. Estos méetodos de acceso representan un campo de relación, que es un campo virtual que referencia a otro bean de entidad. El nombre del método de acceso viene determinado por el nombre del campo de relación, tal y como está definido en el fichero XML de descripción del despliegue. En este caso hemos llamado a la dirección del cliente address, por lo que los nombres de los métodos de acceso correspondientes serán getAddress() y setAddress().

6.2.2 Definición de la relación en la tabla de la base de datos

Para incluir la relación entre el EJB Customer y el EJB Address, añadiremos una clave ajena, ADDRESS_ID, en la tabla CUSTOMER. La clave ajena apuntará al registro ADDRESS.

CREATE TABLE CUSTOMER (
  ID INT PRIMARY KEY NOT NULL, 
  LAST_NAME CHAR(20),
  FIRST_NAME CHAR(20),
  ADDRESS_ID INT
)

Cuando se crea un nuevo EJB Address y se actualiza en una relación address de un EJB Customer, la clave primaria del EJB Address se colocará en la columna ADDRESS_ID de la tabla CUSTOMER, creando una relación en la base de datos:

  // obtener referencia
  AddressLocal address = ... 
  
  // establecer la relacion
  customer.setAddress(address);

6.3 Actualización de la relación desde los clientes

Para dar información al bean Customer una dirección, necesitamos proporcionar al bean la información sobre la dirección. Esto podría parecer tan sencillo como declarar en la interfaz remota del bean Customer una pareja de métodos de acceso correspondientes, tales como setAddress()/getAddress(), pero no es así. Mientras que es sencillo hacer disponible a los clientes remotos campos de persistencia, no es tan sencillo hacer lo mismo con los campos de relación.

La interfaz remota de un bean no puede mostrar sus campos de relación. En el caso del campo address, hemos declarado el tipo como AddressLocal, el cual es una interfaz local, por lo que los métodos de acceso setAddress()/getAddress() no pueden declararse en la interfaz remota del EJB Customer. La razón de esta restricción sobre interfaces remotas es sencilla: el EJBLocalObject, que implementa la interfaz local, está optimizado para ser usado dentro del mismo espacio de direcciones que la instancia del bean y no está preparado para ser usado a través de la red. En otras palabras, las referencias que implementan la interfaz local de un bean no pueden pasarse a través de la red, por lo que una interfaz local no puede declarse como tipo devuelto o como un parámetro de una interfaz remota.

Las interfaces locales (interfaces que extienden javax.ejb.EJBLocalObject), por otro lado, pueden mostrar cualquier tipo de campo de relación. Con interfaces locales, el cliente y el bean que está siendo usado están localizados en el mismo espacio de direcciones, por lo que pueden pasarse referencias locales sin problemas. Por ejemplo, si hubiéramos defenido un interfaz local para el EJB Customer, podría incluir un método que permita a los clientes locales el acceso directo a su campo de relación address:

public interface CustomerLocal extends javax.ejb.EJBLocalObject {
  public AddressLocal getAddress();
  public void setAddress(AddressLocal address);
}

6.3.1 Definición/modificación de un objeto relacionado

En lo que se refiere al EJB Address, es mejor definir una interfaz local sólo debido a que es un bean de grano fino. Para solucionar las restricciones de las interfaces remotas, los métodos de negocio en la clase bean pueden intercambiarse datos, en lugar de referencias a Address. Por ejemplo, podemos declarar un método que permita a un cliente enviar información para crear una dirección para el Customer:

public abstract class CustomerBean implements javax.ejb.EntityBean {
  
  public Integer ejbCreate(Integer id) {
     setId(id);
     return null;
  }

  public void ejbPostCreate(Integer id) {}
 
  // metodos de negocio
  public void setAddress(String street,String city,String state,String zip) {
     try {
        AddressLocal addr = this.getAddress();
        if (addr == null) {   
           // Customer no tiene todavia una direccion. Crear una. 
           InitialContext cntx = new InitialContext();
            AddressHomeLocal addrHome =
                (AddressHomeLocal)cntx.lookup("AddressHomeLocal");
            addr = addrHome.createAddress(street,city,state,zip);
            this.setAddress(addr);
        } else {
           // Customer ya tiene una direccion. Cambiar sus campos.
           addr.setStreet(street);
           addr.setCity(city);
           addr.setState(state);
           addr.setZip(zip);
        }
  
     } catch(Exception e) {
        throw new EJBException(e);
     }
}
  
  ...

El método de negocio setAddress() en la clase CustomerBean también se declara en la interfaz remota del EJB Customer, por lo que podrá ser llamado por los clientes remotos:

public interface Customer extends javax.ejb.EJBObject {
  public void setAddress(String street,String city,String state,String zip);
  public Name getName() throws RemoteException;
  public void setName(Name name) throws RemoteException;

  public boolean getHasGoodCredit() throws RemoteException;
  public void setHasGoodCredit(boolean creditRating) throws RemoteException;
}

Cuando el método de negocio CustomerRemote.setAddress() se invoca en el CustomerBean, se usan los argumentos del método para crear un nuevo EJB Address y para definirlo como el campo de relación address, en el caso de que no exista ya ninguno. Si el EJB Customer ya tiene una relación address, el EJB Address se modifica para reflejar la nueva información.

Cuando creamos un nuevo EJB Address, el objeto home se obtiene a través del JNDI ENC (environment naming context) y se llama a su método createAdress(). Esto tiene como resultado la creación de un nuevo EJB Address y la inserción de el correspondiente registro ADDRESS en la base de datos. Después de haber creado el EJB Address, se usa en el método setAddress(). La clase CustomerBean debe llamar explícitamente al método setAddress(), o la nueva dirección no se asignará al cliente. De hecho, si creamos simplemente un EJB Address sin asignárselo al cliente con el método setAddress() estaremos creando un EJB Address desconectado. Detallando más, tendrá como efecto la creación de un registro ADDRESS de la base de datos que no será referenciado por ningún registro CUSTOMER.

La viabilidad de entidades desconectadas depende, en parte, de la integridad referencial de la base de datos. Por ejemplo, si la integridad referencial de la base de datos permite sólo valores no nulos para la columna de clave ajena, la creación de una entidad desconectada puede resultar en un error de la base de datos.

Cuando se invoca el método local setAddress(), el contenedor enlaza automáticamnte el registro ADDRESS y el registro CUSTOMER. En este caso, coloca la clave primaria ADDRESS en el campo ADDRESS_ID del registro CUSTOMER y crea una referencia desde el registro CUSTOMER al registro ADDRESS.

Si el EJB Customer ya tiene un address, queremos cambiar sus valores en lugar de crear un nuevo EJB Address. No necesitamos usar el método local setAddress() si vamos a actualizar los valores de un EJB Address ya existente, porque el EJB Address que modifiquemos ya tiene una relación con el bean de entidad.

6.3.2. Obtención de la información del objeto relacionado

También queremos proporcionar a los clientes un método de negocio para obtener la información de la dirección de un EJB Customer. Ya que no podemos enviar directamente una instancia del EJB Address (porque es una interfaz local), debemos empaquetar los datos de la dirección de alguna otra forma para enviárselos al cliente. Hay dos soluciones a este problema: adquirir la interfaz remota del EJB Address y devolverla o devolver los datos como un objeto valor dependiente (dependent value object).

6.3.2.1 Devolución de una interfaz remota

Podemos obtener la interfaz remota del EJB Address sólo se ha sido definida. Los beans de entidad pueden tener un conjunto de interfaces remotas, locales o ambas. En la situación actual, el EJB Address tiene un grano demasiado fino para justificar la creación de una interfaz remota, pero en muchas otras circunstancias podemos tener realmente una interfaz remota. Por ejemplo, si el EJB Customer referenciara a un EJB SalesPerson, el CustomerBean podría convertir la referencia local en una referencia remota. Esto se haría accediendo al objeto EJB local, obteniendo su clave primaria (EJBLocalObject.getPrimaryKey()), obteniendo el home remoto del EJB SalesPerson a partir del JNDI ENC, y luego usando la clave primaria y la referencia home remota paa encontrar una referencia a la interfaz remota:

public SalesRemote getSalesRep(){
  SalesLocal local = getSalesPerson();
  Integer primKey = local.getPrimaryKey();
  
  Object ref = jndiEnc.lookup("SalesHomeRemote");
  SalesHomeRemote home = (SalesHomeRemote)
  PortableRemoteObject.narrow(ref, SalesHomeRemote.class);
  
  SalesRemote remote = home.findByPrimaryKey( primKey );
  return remote;
}

6.3.2.2 Devolución de un objeto de valor dependiente

La otra opción es usar un valor dependiente para pasar los datos del EJB Address entre el EJB Customer y los clientes remotos. Este es el enfoque recomendado para los beans de grano fino como el EJB Address. El siguiente código muestra como se usa la clase de valor dependiente AddressDO junto con las componentes locales del EJB Address (el sufijo “DO” en AddressDO es una convención que se suele usar para denominar a las clases que definen valores dependientes; significa “dependent object”):

public abstract class CustomerBean implements javax.ejb.EntityBean {
  
  public Integer ejbCreate(Integer id) {
     setId(id);
     return null;
  }

  public void ejbPostCreate(Integer id) {
  }

  // metodos de negocio
  
  public AddressDO getAddress() {
     AddressLocal addrLocal = getHomeAddress();
     if(addrLocal == null) return null;
     String street = addrLocal.getStreet();
     String city = addrLocal.getCity();
     String state = addrLocal.getState();
     String zip = addrLocal.getZip();
     AddressDO addrValue = new AddressDO(street,city,state,zip);
     return addrValue;
  }
  
  public void setAddress(AddressDO addrValue) 
     throws EJBException {
     String street = addrValue.getStreet();
     String city = addrValue.getCity();
     String state = addrValue.getState();
     String zip = addrValue.getZip();
  
     AddressLocal addr = getHomeAddress();
  
     try {
  
     if(addr == null) {
        // Customer no tiene una direccion. Crear una nuave.
        InitialContext cntx = new InitialContext();
        AddressHomeLocal addrHome = (AddressHomeLocal)cntx.lookup
           ("AddressHomeLocal");
        addr = addrHome.createAddress(street, city, state, zip);
        this.setHomeAddress(addr);
     } else {
        // Customer ya tiene una relacion. Modificar sus campos.
        addr.setStreet(street);
        addr.setCity(city);
        addr.setState(state);
        addr.setZip(zip);
     }
  } catch(NamingException ne) {
  throw new EJBException(ne);
  } catch(CreateException ce) {
  throw new EJBException(ce);
  }
}
  ...

He aquí la definición de una clase de valor dependiente AddressDO, que usa el enterprise bean para enviar información de la dirección al cliente:

public class AddressDO implements java.io.Serializable {
  private String street;
  private String city;
  private String state;
  private String zip;
  
  public AddressDO(String street, String city, String state, String zip ) {
     this.street = street;
     this.city = city;
     this.state = state;
     this.zip = zip;
  }

  public String getStreet() {
     return street;
  }
  
  public String getCity() {
     return city;
  }
  
  public String getState() {
    return state;
  }
  
  public String getZip() {
    return zip;
  }
}

El valor dependiente AddressDO es inmutable, lo que significa que no puede ser alterado una vez que ha sido creado. La inmutabilidad ayuda a reforzar el hecho de que la clase de valor dependiente es una copia, no una referencia remota.

Ahora podemos usar una aplicación cliente para probar la relación del EJB Customer con el EJB Address. A continuación se encuentra el código del cliente que crea un nuevo cliente, le proporciona una dirección y luego le cambia la dirección.

import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;
import javax.naming.Context;
import javax.naming.NamingException;
import java.util.Properties;
  
public class Client {
  public static void main(String [] args) throws Exception {
  
     // obtener CustomerHome
     Context jndiContext = getInitialContext();
     Object obj=jndiContext.lookup("CustomerHomeRemote");
     CustomerHome home = (CustomerHomeRemote) 
        javax.rmi.PortableRemoteObject.narrow(obj, CustomerHomeRemote.class);
  
     // crear un Customer
     Integer primaryKey = new Integer(1);
     Customer customer = home.create(primaryKey);
  
     // crear un AddressDO
     AddressDO address = new AddressDO("1010 Colorado","Austin", 
                                       "TX", "78701");
     
	 // establecer la direccion 
     customer.setAddress(address);
     address = customer.getAddress();
     System.out.print(primaryKey+" = ");
     System.out.println(address.getStreet());
     System.out.println(address.getCity()+","+ 
                        address.getState()+" "+
                        address.getZip());
  
     // crear una nueva direccion
     address = new AddressDO("1600 Pennsylvania Avenue NW", 
                             "DC", "WA", "20500");
  
     // cambiar la direccion del customer
     customer.setAddress(address);
  
     address = customer.getAddress();
  
     System.out.print(primaryKey+" = ");
     System.out.println(address.getStreet());
     System.out.println(address.getCity()+","+
                        address.getState()+" "+
                        address.getZip());
  
     // borrar Customer
     customer.remove();
  } 
  
  public static Context getInitialContext() 
     throws javax.naming.NamingException {
     Properties p = new Properties();
     // ... Specify the JNDI properties specific to the vendor.
     //return new javax.naming.InitialContext(p);
     return null;
  }
}