Table of Contents

Capítulo 6: Mapeo objeto/relacional
Mapeo de entidad
Mapeo propiedad
Mapeo de referencia
Mapeo de colección
Mapeo de referencia incrustada
Conversión de tipo
Conversión de propiedad
Conversión con multiples columnas
Conversión de referencia

Capítulo 6: Mapeo objeto/relacional

Con el mapeo objeto relacional declaramos en que tablas y columnas de nuestra base de datos relacional se guarda la información de nuestra entidad.
Las herramientas O/R nos permiten trabajar con objetos, en vez de con tablas y columnas y generan automáticamente el código SQL necesario para leer y actualizar la base de datos. De esta forma no necesitamos acceder directamente a la base de datos con SQL, pero para eso tenemos que definir con precisión como se mapean nuestras clases a nuestras tablas, y eso es lo que se hace en las anotaciones de mapeo JPA.
Las entidades OpenXava son entidades JPA, por lo tanto el mapeo objeto/relacional en OpenXava se hace mediante Java Persistence API (JPA). Este capítulo muestra las técnicas más básicas y algunos casos especiales. Si queremos aprender más sobre JPA podemos consultar la documentación de Hibernate Annotations (la implementación de JPA usada por OpenXava por defecto), o cualquier otro manual de JPA que queramos.

Mapeo de entidad

La anotación @Table especifica la tabla principal para la entidad. Se pueden especificar tablas adicionales usando @SecondaryTable o @SecondaryTables.
Si no se especifica @Table para una entidad se aplicaran los valores por defecto.
Ejemplo:
@Entity
@Table(name="CLI", schema="XAVATEST")
public class Cliente {
 

Mapeo propiedad

La anotación @Column se usa para especificar como mapear una propiedad persistente. Si no se especifica @Column se aplican los valores por defecto.
Un ejemplo sencillo:
@Column(name="DESC", length=512)
private String descripcion;
 
Un ejemplo anotando el getter:
@Column(name="DESC", nullable=false, length=512)
public String getDescripcion() { return descripcion; }
 
Otros ejemplos:
@Column(name="DESC",
    columnDefinition="CLOB NOT NULL",
    table="EMP_DETAIL")
@Lob
private String descripcion;
 
@Column(name="ORDER_COST", updatable=false, precision=12, scale=2)
private BigDecimal coste;

Mapeo de referencia

La anotación @JoinColumn se usa para especificar el mapeo de una columna para una referencia.
Ejemplo:
@ManyToOne
@JoinColumn(name="CLI_ID")
private Cliente cliente;
Si necesitamos definir un mapeo para una clave foranea compuesta hemos de usar @JoinColumns. Esta anotación agrupa anotaciones @JoinColumn para la misma reference.
Cuando se usa la anotación @JoinColumns, tanto el atributo nombre como referencedColumnName tienen que especificarse en cada anotación @JoinColumn.
Ejemplo:
@ManyToOne
@JoinColumns({
    @JoinColumn(name="FAC_AÑO", referencedColumnName="AÑO"),
    @JoinColumn(name="FAC_NUMERO", referencedColumnName="NUMERO")
})
private Factura factura;

Mapeo de colección

Cuando usamos @OneToMany para una colección el mapeo depende de la referencia usada en la otra parte de la asociación, es decir, normalmente no es necesario hacer nada. Pero si estamos usando @ManyToMany, quizás nos sea útil declarar la tabla de unión (@JoinTable), como sigue:
@ManyToMany
@JoinTable(name="CLIENTE_PROVINCIA",
    joinColumns=@JoinColumn(name="CLIENTE"),
    inverseJoinColumns=@JoinColumn(name="PROVINCIA")
)
private Collection<Provincia> provincias;
 
Si omitimos @JoinTable se aplican los valores por defecto.

Mapeo de referencia incrustada

Una referencia incrustada contiene información que en el modelo relacional se guarda en la misma tabla que la entidad principal. Por ejemplo si tenemos un incrustable Direccion asociado a un Cliente, los datos de la dirección se guardan en la misma tabla que los del cliente. ¿Cómo se expresa eso con JPA? Es muy sencillo, usando la anotación @AttributeOverrides, de esta forma:
@Embedded
@AttributeOverrides({
    @AttributeOverride(name="calle", column=@Column("DIR_CALLE")),
    @AttributeOverride(name="codigoPostal", column=@Column("DIR_CP"))
    @AttributeOverride(name="poblacion", column=@Column("DIR_POB")),
    @AttributeOverride(name="pais", column=@Column("DIR_PAIS"))
})
private Direccion direccion;
Si no usamos @AttributeOverrides se asumen valores por defectos.

Conversión de tipo

La conversión de tipos entre Java y la base de datos relacional es un trabajo de la implementación de JPA (OpenXava usa Hibernate por defecto). Normalmente, la conversión de tipos por defecto es buena para la mayoría de los casos, pero si trabajamos con bases de datos legadas quizás necesitemos algunos de los trucos que aquí se muestran.
Dado que OpenXava usa la facilidad de conversión de tipos de Hibernate podemos aprender más en la documentación de Hibernate.

Conversión de propiedad

Cuando el tipo de una propiedad Java y el tipo de su columna correspondiente en la base de datos no coincide necesitamos escribir un Hibernate Type para poder hacer nuestra conversión de tipo personalizada.
Por ejemplo, si tenemos una propiedad de tipo String [], y queremos almacenar su valor concatenándolo en una sola columna de base de datos de tipo VARCHAR. Entonces tenemos que declarar la conversión para nuestra propiedad de esta manera:
@Type(type="org.openxava.test.types.RegionesType")
private String [] regiones;
La lógica de conversión en RegionesType es:
package org.openxava.test.types;
 
import java.io.*;
import java.sql.*;
 
import org.apache.commons.logging.*;
import org.hibernate.*;
import org.hibernate.usertype.*;
import org.openxava.util.*;
 
/**
 *
 * @author Javier Paniza
 */
 
public class RegionesType implements UserType {  // 1
 
    public int[] sqlTypes() {
        return new int[] { Types.VARCHAR };
    }
 
    public Class returnedClass() {
        return String[].class;
    }
 
    public boolean equals(Object obj1, Object obj2) throws HibernateException {
        return Is.equal(obj1, obj2);
    }
 
    public int hashCode(Object obj) throws HibernateException {
        return obj.hashCode();
    }
 
    public Object nullSafeGet(ResultSet resultSet, String[] names, Object owner)  // 2
        throws HibernateException, SQLException
    {
        Object o = resultSet.getObject(names[0]);
           if (o == null) return new String[0];
           String dbValue = (String) o;
           String [] javaValue = new String [dbValue.length()];
           for (int i = 0; i < javaValue.length; i++) {
               javaValue[i] = String.valueOf(dbValue.charAt(i));
           }
           return javaValue;
    }
 
    public void nullSafeSet(PreparedStatement ps, Object value, int index)  // 3
        throws HibernateException, SQLException
    {
        if (value == null) {
            ps.setString(index, "");
            return;
        }
        String [] javaValue = (String []) value;
        StringBuffer dbValue = new StringBuffer();
        for (int i = 0; i < javaValue.length; i++) {
            dbValue.append(javaValue[i]);
        }
        ps.setString(index, dbValue.toString());
    }
 
    public Object deepCopy(Object obj) throws HibernateException {
        return obj == null?null:((String []) obj).clone();
    }
 
    public boolean isMutable() {
        return true;
    }
 
    public Serializable disassemble(Object obj) throws HibernateException {
        return (Serializable) obj;
    }
 
    public Object assemble(Serializable cached, Object owner) throws HibernateException {
        return cached;
    }
 
    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }
 
}
 
El conversor de tipo ha de implementar org.hibernate.usertype.UserType (1). Los métodos principales son nullSafeGet (2) para leer de la base de datos y convertir a Java, y nullSafeSet (3) para escribir el valor Java en la base de datos.
OpenXava tiene conversores de tipo de Hibernate genéricos en el paquete org.openxava.types listos para usar. Uno de ellos es EnumLetterType, que permite mapear propiedades de tipo enum. Por ejemplo, si tenemos una propiedad como esta:
private Distancia distancia;
public enum Distancia { LOCAL, NACIONAL, INTERNACIONAL };
 
En esta propiedad Java 'LOCAL' es 1, 'NATIONAL' es 2 and 'INTERNATIONAL' es 3 cuando la propiedad se almacena en la base de datos. Pero, ¿qué ocurre, si en la base de datos se almacena una única letra ('L', 'N' or 'I')? En este caso podemos usar EnumLetterType de esta forma:
@Type(type="org.openxava.types.EnumLetterType",
    parameters={
        @Parameter(name="letters", value="LNI"),
        @Parameter(name="enumType", value="org.openxava.test.modelo.Albaran$Distancia")
    }
)
private Distancia distancia;
public enum Distancia { LOCAL, NACIONAL, INTERNACIONAL }
Al poner 'LNI' como valor para letters, hace corresponder la 'L' con 1, la 'N' con 2 y la 'I' con 3. Vemos como el que se puedan configurar propiedades del conversor de tipos nos permite hacer conversores reutilizables.

Conversión con multiples columnas

Con CompositeUserType podemos hacer que varias columnas de la tabla de base de datos correspondan a una propiedad en Java. Esto es útil, por ejemplo cuando tenemos propiedades cuyo tipo Java son clases definidas por nosotros que tienen a su vez varias propiedades susceptibles de ser almacenadas, y también se usa mucho cuando nos enfrentamos a esquemas de bases de datos legados.
Un ejemplo típico sería usar el conversor genérico Date3Type, que permite almacenar en la base de datos 3 columnas y en Java una propiedad java.util.Date.
@Type(type="org.openxava.types.Date3Type")
@Columns(columns = {
    @Column(name="AÑOENTREGA"),
    @Column(name="MESENTREGA"),
    @Column(name="DIAENTREGA")
})
private java.util.Date fechaEntrega;
DIAENTREGA, MESENTREGA y AÑOENTREGA son las tres columnas que en la base de datos guardan la fecha de entrega. Y aquí Date3Type:
package org.openxava.types;
 
import java.io.*;
import java.sql.*;
 
import org.hibernate.*;
import org.hibernate.engine.*;
import org.hibernate.type.*;
import org.hibernate.usertype.*;
import org.openxava.util.*;
 
/**
 * In java a <tt>java.util.Date</tt> and in database 3 columns of
 * integer type. <p>
 *
 * @author Javier Paniza
 */
 
public class Date3Type implements CompositeUserType {  // 1
 
    public String[] getPropertyNames() {
        return new String[] { "year", "month", "day" };
    }
 
    public Type[] getPropertyTypes() {
        return new Type[] { Hibernate.INTEGER, Hibernate.INTEGER, Hibernate.INTEGER };
    }
 
    public Object getPropertyValue(Object component, int property) throws HibernateException {  // 2
        java.util.Date date = (java.util.Date) component;
        switch (property) {
            case 0:
                return Dates.getYear(date);
            case 1:
                return Dates.getMonth(date);
            case 2:
                return Dates.getYear(date);
        }
        throw new HibernateException(XavaResources.getString("date3_type_only_3_properties"));
    }
 
    public void setPropertyValue(Object component, int property, Object value)
        throws HibernateException    // 3
    {
        java.util.Date date = (java.util.Date) component;
        int intValue = value == null?0:((Number) value).intValue();
        switch (property) {
            case 0:
                Dates.setYear(date, intValue);
            case 1:
                Dates.setMonth(date, intValue);
            case 2:
                Dates.setYear(date, intValue);
        }
        throw new HibernateException(XavaResources.getString("date3_type_only_3_properties"));
    }
 
    public Class returnedClass() {
        return java.util.Date.class;
    }
 
    public boolean equals(Object x, Object y) throws HibernateException {
        if (x==y) return true;
        if (x==null || y==null) return false;
        return !Dates.isDifferentDay((java.util.Date) x, (java.util.Date) y);
    }
 
    public int hashCode(Object x) throws HibernateException {
        return x.hashCode();
    }
 
    public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner)
        throws HibernateException, SQLException  // 4
    {
        Number year = (Number) Hibernate.INTEGER.nullSafeGet( rs, names[0] );
        Number month = (Number) Hibernate.INTEGER.nullSafeGet( rs, names[1] );
        Number day = (Number) Hibernate.INTEGER.nullSafeGet( rs, names[2] );
 
        int iyear = year == null?0:year.intValue();
        int imonth = month == null?0:month.intValue();
        int iday = day == null?0:day.intValue();
 
        return Dates.create(iday, imonth, iyear);
    }
 
    public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session)
        throws HibernateException, SQLException  // 5
    {
        java.util.Date d = (java.util.Date) value;
        Hibernate.INTEGER.nullSafeSet(st, Dates.getYear(d), index);
        Hibernate.INTEGER.nullSafeSet(st, Dates.getMonth(d), index + 1);
        Hibernate.INTEGER.nullSafeSet(st, Dates.getDay(d), index + 2);
    }
 
    public Object deepCopy(Object value) throws HibernateException {
        java.util.Date d = (java.util.Date) value;
        if (value == null) return null;
        return (java.util.Date) d.clone();
    }
 
    public boolean isMutable() {
        return true;
    }
 
    public Serializable disassemble(Object value, SessionImplementor session)
        throws HibernateException
    {
        return (Serializable) deepCopy(value);
    }
 
    public Object assemble(Serializable cached, SessionImplementor session, Object owner)
        throws HibernateException
    {
        return deepCopy(cached);
    }
 
    public Object replace(Object original, Object target, SessionImplementor session, Object owner)
        throws HibernateException
    {
        return deepCopy(original);
    }
 
}
Como se ve el conversor de tipo implementa CompositeUserType (1). Los métodos clave son getPropertyValue (2) y setPropertyValue (3) para coger y poner valores en las propiedades del objeto del tipo compuesto, y nullSafeGet (4) y nullSafeSet (5) para leer y grabar este objeto en la base de datos.

Conversión de referencia

La conversión de referencias no se soporta directamente por Hibernate. Pero en alguna circunstancias extremas puede ser que necesitemos hacer conversión de referencias. En esta sección se explica como hacerlo.
Por ejemplo, puede que tengamos una referencia a permiso de conducir usando dos columnas, PERMISOCONDUCIR_NIVEL y PERMISOCONDUCIR_TIPO, y la columna PERMISOCONDUCIR_TIPO no admita nulos, pero es posible que el objeto puede no tener permiso de conducir, en cuyo caso la columna PERMISOCONDUCIR_TIPO almacena una cadena vacía. Esto no es algo normal si nosotros diseñamos la base de datos usando claves foráneas, pero si la base de datos fue diseñada por un programador RPG, por ejemplo, esto se habrá hecho de esta forma, porque los programadores RPG no están acostumbrados a lidiar con nulos.
Es decir, necesitamos una conversión para PERMISOCONDUCIR_TIPO, para transformar el nulo en una cadena vacía. Esto se puede conseguir con un código como este:
// Aplicamos conversión (nulo en una cadena vacía) a la columna PERMISOCONDUCIR_TIPO
// Para hacerlo, creamos permisoConducir_nivel y permisoConducir_tipo
// Hacemos JoinColumns no insertable ni modificable, modificamos el método get/setPermisoConducir
// y creamos un método conversionPermisoConducir().
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumns({  // 1
    @JoinColumn(name="PERMISOCONDUCIR_NIVEL", referencedColumnName="NIVEL",
         insertable=false, updatable=false),
    @JoinColumn(name="PERMISOCONDUCIR_TIPO", referencedColumnName="TIPO",
         insertable=false, updatable=false)
})
private PermisoConducir permisoConducir;
private Integer permisoConducir_nivel;  // 2
private String permisoConducir_tipo;    // 2
 
public PermisoConducir getPermisoConducir() {  // 3
    // De esta manera porque la columna tipo de permiso de conducir no admite nulos
    try {
        if (permisoConducir != null) permisoConducir.toString(); // para forzar la carga
        return permisoConducir;
    }
    catch (EntityNotFoundException ex) {
        return null;
    }
}
 
public void setPermisoConducir(PermisoConducir permiso) {  // 4
    // De esta manera porque la columna tipo de permiso de conducir no admite nulos
    this.permisoConducir = permiso;
    this.permisoConducir_nivel = permiso==null?null:permiso.getNivel();
    this.permisoConducir_tipo = permiso==null?null:permiso.getTipo();
}
 
@PrePersist @PreUpdate
private void conversionPermisoConducir() {  // 5
    if (this.permisoConducir_tipo == null) this.permisoConducir_tipo = "";
}
 
Lo primero poner @JoinColumns con insertable=false y updatable=false en todas las @JoinColumn (1), de esta manera la referencia es leida de la base de datos, pero no escrita. También tenemos que definir propiedades planas para almacenar la clave foránea de la referencia (2).
Ahora tenemos que escribir un getter, getPermisoConducir() (3), para devolver nulo cuand la referencia no se encuentre, y un setter, setPermisoConducir() (4), para asignar la clave de la referencia a las propiedades planas correspondientes.
Finalmente, hemos de escribir un método de retrollamada, conversionPermisoConducir() (5), para hacer el trabajo de conversión. Este método será automáticamente ejecutado al crear y actualizar.
Este ejemplo shows como es posible envolver bases de datos legadas simplemente usando un poco de programación y algunos recursos básicos de JPA.