A partir de v6.6 puedes usar
anotaciones Java específicas en lugar de estereotipos (es decir puedes
usar @File en lugar de @Stereotype("ARCHIVO")), así el
compilador asegura que el código está bien escrito y además se pueden usar
atributos específicos para cada anotación.
A partir de v6.6 puedes usar una
anotación Java en lugar de un estereotipo, de esta forma:
O el estereotipo ARCHIVOS si usas
una versión anterior a la 6.6. Así:
Enums con icono (nuevo en
v6.3)
Puedes asociar un icono a cada opción de un
enum usando
org.openxava.model.IIconEnum:
public enum Prioridad implements IIconEnum {
BAJA("transfer-down"), MEDIA("square-medium"), ALTA("transfer-up");
private String icon;
private Priority(String icon) {
this.icon = icon;
}
public String getIcon() {
return icon;
}
};
private Prioridad prioridad;
Simplemente haz que tu
enum implemente
IIconEnum
que fuerza a que tengas un método
getIcon(). Este método ha de
devolver un identificador de icono de
Material
Design Icons. OpenXava puede usar estos iconos en varias partes de
la interfaz de usuario, por ejemplo en la lista:
Propiedades
calculadas
Las propiedades calculadas son de solo lectura (solo tienen
getter)
y no persistentes (no se almacenan en ninguna columna de la tabla de base
de datos).
Una propiedad calculada se define de esta manera:
@Depends("precioUnitario") // 1
@Max(9999999999L) // 2
public BigDecimal getPrecioUnitarioEnPesetas() {
if (precioUnitario == null) return null;
return precioUnitario.multiply(new BigDecimal("166.386"))
.setScale(0, BigDecimal.ROUND_HALF_UP);
}
De acuerdo con esta definición ahora podemos usar el código de esta
manera:
Producto producto = ...
producto.setPrecioUnitario(2);
BigDecimal resultado = producto.getPrecioUnitarioEnPesetas();
Y
resultado contendrá 332,772.
Cuando la propiedad
precioUnitarioEnPesetas se visualiza al
usuario no es editable, y su editor tiene una longitud de 10, indicado
usando
@Max(9999999999L) (2). También, dado que usamos
@Depends("precioUnitario")
(1) cuando el usuario cambie la propiedad
precioUnitario en la
interfaz de usuario la propiedad
precioUnitarioEnPesetas será
recalculada y su valor será refrescado de cara al usuario.
Desde una propiedad calculada tenemos acceso a conexiones JDBC. Un
ejemplo:
@Max(999)
public int getCantidadLineas() {
// Un ejemplo de uso de JDBC
Connection con = null;
try {
con = DataSourceConnectionProvider.getByComponent("Factura").getConnection(); // 1
String tabla = MetaModel.get("LineaFactura").getMapping().getTable();
PreparedStatement ps = con.prepareStatement("select count(*) from " + tabla +
" where FACTURA_AÑO = ? and FACTURA_NUMERO = ?");
ps.setInt(1, getAño());
ps.setInt(2, getNumero());
ResultSet rs = ps.executeQuery();
rs.next();
Integer result = new Integer(rs.getInt(1));
ps.close();
return result;
}
catch (Exception ex) {
log.error("Problemas al calcular cantidad de líneas de una Factura", ex);
// Podemos lanzar cualquier RuntimeException aquí
throw new SystemException(ex);
}
finally {
try {
con.close();
}
catch (Exception ex) {
}
}
}
Es verdad, el código JDBC es feo y complicado, pero a veces puede ayudar a
resolver problemas de rendimiento. La clase
DataSourceConnectionProvider
nos permite obtener la conexión asociada a la misma fuente de datos que la
entidad indicada (en este caso
Factura). Esta clase es para
nuestra conveniencia, también podemos acceder a una conexión JDBC usando
JNDI o cualquier otro medio que queramos. De hecho, en una propiedad
calculada podemos escribir cualquier código que Java nos permita.
Si estamos usando acceso basado en propiedades, es decir si anotamos los
getters
o
setters, entonces hemos de añadir la anotación
@Transient a nuestra propiedad calculada, de
esta forma:
private long codigo;
@Id @Column(length=10) // Anotamos el getter,
public long getCodigo() { // por tanto JPA usará acceso basado en propiedades para nuestra clase
return codigo;
}
public void setCodigo(long codigo) {
this.codigo = codigo;
}
@Transient // Hemos de anotar como Transient nuestra propiedad calculada
public String getZoneOne() { // porque usamos acceso basado en propiedades
return "En ZONA 1";
}
Fórmula
(nuevo en v3.1.4)
Usando
@Formula de
Hibernate Annotations podemos definir un cálculo para
nuestra propiedad. Este cálculo se expresa usando SQL, y es ejecutado en
la propia base de datos, en vez de por Java. Simplemente hemos de escribir
un fragmento válido de SQL:
@org.hibernate.annotations.Formula("PRECIOUNITARIO * 1.16")
private BigDecimal precioUnitarioConIVA;
public BigDecimal getPrecioUnitarioConIVA() {
return precioUnitarioConIVA;
}
El uso es simple. Hemos de poner el cálculo como lo hariamos si lo
tuvieramos que poner en una sentencia SQL.
Normalmente las propiedades con
@Formula son propiedades de solo
lectura, es decir, solo tienen
getter, no tienen
setter.
Cuando el objeto es leído de la base de datos se hace el cálculo por la
misma base de datos y se rellena la propiedad con el resultado.
Esto es una alternativa a las propiedades calculadas. Tiene la ventaja de
que el usuario puede filtrar por esta propiedad en modo lista, y la
desventaja de que hemos de usar SQL en vez de Java, y no podemos usar
@Depends para recalcular el valor en vivo.
Calculador
valor por defecto
Con
@DefaultValueCalculator podemos asociar lógica
a una propiedad, en este caso la propiedad es lectura y escritura. Este
calculador se usa para calcular el valor inicial. Por ejemplo:
@DefaultValueCalculator(CurrentYearCalculator.class)
private int año;
En este caso cuando el usuario intenta crear una nueva factura (por
ejemplo) se encontrará con que el campo de año ya tiene valor, que él
puede cambiar si quiere. La lógica para generar este valor está en la
clase
CurrentYearCalculator class, así:
package org.openxava.calculators;
import java.util.*;
/**
* @author Javier Paniza
*/
public class CurrentYearCalculator implements ICalculator {
public Object calculate() throws Exception {
Calendar cal = Calendar.getInstance();
cal.setTime(new java.util.Date());
return new Integer(cal.get(Calendar.YEAR));
}
}
Es posible personalizar el comportamiento de un calculador poniendo el
valor de sus propiedades, como sigue:
@DefaultValueCalculator(
value=org.openxava.calculators.StringCalculator.class,
properties={ @PropertyValue(name="string", value="BUENA") }
)
private String relacionConComercial;
En este caso para calcular el valor por defecto OpenXava instancia
StringCalculator
y entonces inyecta el valor "BUENA" en la propiedad
string de
StringCalculator,
y finalmente llama al método
calculate() para obtener el valor
por defecto para
relacionConComercial. Como se ve, el uso de la
anotación
@PropertyValue permite crear calculadores
reutilizable.
@PropertyValue permite inyectar valores desde otras propiedades
visualizadas, de esta forma:
@DefaultValueCalculator(
value=org.openxava.test.calculadores.CalculadorObservacionesTransportista.class,
properties={
@PropertyValue(name="tipoPermisoConducir", from="permisoConducir.tipo")
}
)
private String observaciones;
En este caso antes de ejecutar el calculador OpenXava llena la propiedad
permisoConducir
de
CalculadorObservacionesTransportista con el valor de la
propiedad visualizada
tipo de la referencia
permisoConducir.
Como se ve el atributo
from soporta propiedades calificadas
(referencia.propiedad). Además, cada ve que
permisoConducir.tipo
cambia
observaciones se recalcula (
nuevo en v5.1, con
versiones anteriores se recalculaba solo la primera vez).
Además podemos usar
@PropertyValue sin
from ni
value:
@DefaultValueCalculator(value=CalculadorPrecioDefectoProducto.class, properties=
@PropertyValue(name="codigoFamilia")
)
En este caso OpenXava coge el valor de la propiedad visualizada
codigoFamilia
y lo inyecta en la propiedad
codigoFamilia del calculador, es
decir
@PropertyValue(name="codigoFamilia") equivale a
@PropertyValue(name="codigoFamilia",
from="codigoFamilia").
Desde un calculador tenemos acceso a conexiones JDBC, he aquí un ejemplo:
@DefaultValueCalculator(value=CalculadorCantidadLineas.class,
properties= {
@PropertyValue(name="año"),
@PropertyValue(name="numero"),
}
)
private int cantidadLineas;
Y la clase del calculador:
package org.openxava.test.calculadores;
import java.sql.*;
import org.openxava.calculators.*;
import org.openxava.util.*;
/**
* @author Javier Paniza
*/
public class CalculadorCantidadLineas implements IJDBCCalculator { // 1
private IConnectionProvider provider;
private int año;
private int numero;
public void setConnectionProvider(IConnectionProvider provider) { // 2
this.provider = provider;
}
public Object calculate() throws Exception {
Connection con = provider.getConnection();
try {
PreparedStatement ps = con.prepareStatement(
"select count(*) from XAVATEST.LINEAFACTURA “ +
“where FACTURA_AÑO = ? and FACTURA_NUMERO = ?");
ps.setInt(1, getAño());
ps.setInt(2, getNumero());
ResultSet rs = ps.executeQuery();
rs.next();
Integer result = new Integer(rs.getInt(1));
ps.close();
return result;
}
finally {
con.close();
}
}
public int getAño() {
return año;
}
public int getNumero() {
return numero;
}
public void setAño(int año) {
this.año = año;
}
public void setNumero(int numero) {
this.numero = numero;
}
}
Para usar JDBC nuestro calculador tiene que implementar
IJDBCCalculator
(1) y entonces recibirá un
IConnectionProvider (2) que podemos
usar dentro de
calculate().
OpenXava dispone de un conjunto de calculadores incluidos de uso genérico,
que se pueden encontrar en
org.openxava.calculators.
Valores
por defecto al crear
Podemos indicar que el valor sea calculado justo antes de crear (insertar
en la base de datos) un objeto por primera vez.
Usualmente para las claves usamos el estándar JPA. Por ejemplo, si
queremos usar una columna
identity (auto incremento) como clave:
@Id @Hidden
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
Podemos usar otras técnicas de generación, por ejemplo, una
sequence
de base de datos puede ser definida usando el estándar JPA de esta manera:
@SequenceGenerator(name="SIZE_SEQ", sequenceName="SIZE_ID_SEQ", allocationSize=1 )
@Hidden @Id @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SIZE_SEQ")
private Integer id;
Si queremos generar un identificador único de tipo String y 32 caracteres,
podemos usar una extensión de Hibernate de JPA:
@Id @GeneratedValue(generator="system-uuid") @Hidden
@GenericGenerator(name="system-uuid", strategy = "uuid")
private String oid;
Ver la sección 9.1.9 de la especificación JPA 1.0 (parte de JSR-220) para
aprender más sobre
@GeneratedValues.
Si queremos usar nuestra propia lógica para generar el valor al crear, o
bien queremos generar un nuevo valor para propiedades que no son clave
entonces no podemos usar el
@GeneratedValue de JPA, aunque es
fácil resolver estos casos con JPA. Solo necesitamos añadir este código a
nuestra clase:
@PrePersist
private void calcularContador() {
contador = new Long(System.currentTimeMillis()).intValue();
}
La anotación JPA
@PrePersist hace que este método se ejecute
antes de insertar datos por primera vez en la base de datos, en este
método podemos calcular el valor para nuestra clave o incluso para
propiedades no clave con nuestra propia lógica.
Validador
de propiedad
Un validador de propiedad (
@PropertyValidator) ejecuta la lógica de
validación sobre el valor que se vaya a asignar a esa propiedad antes de
grabar. Una propiedad puede tener varios validadores:
@PropertyValidator(value=ValidadorExcluirCadena.class, properties=
@PropertyValue(name="cadena", value="MOTO")
)
@PropertyValidator(value=ValidadorExcluirCadena.class, properties=
@PropertyValue(name="cadena", value="COCHE"),
onlyOnCreate=true
)
private String descripcion;
Con un OpenXava anterior a 6.1 has de usar
@PropertyValidators
para englobar las anotaciones, así:
@PropertyValidators ({ // Sólo necesario hasta v6.0.2
@PropertyValidator(value=ValidadorExcluirCadena.class, properties=
@PropertyValue(name="cadena", value="MOTO")
),
@PropertyValidator(value=ValidadorExcluirCadena.class, properties=
@PropertyValue(name="cadena", value="COCHE"),
onlyOnCreate=true
)
})
private String descripcion;
La forma de configurar el validador (con los
@PropertyValue, aunque el atributo
from
no funciona, hay que usar
value siempre) es exactamente igual
como en los
calculadores.
Con el atributo
onlyOnCreate=”true” se puede definir que esa
validación solo se ejecute cuando se crea el objeto, y no cuando se
modifica.
El código del validador es:
package org.openxava.test.validadores;
import org.openxava.util.*;
import org.openxava.validators.*;
/**
* @author Javier Paniza
*/
public class ValidadorExcluirCadena implements IPropertyValidator { // 1
private String cadena;
public void validate(
Messages errores, // 2
Object valor, // 3
String nombreObjecto, // 4
String nombrePropiedad) // 5
throws Exception {
if (valor==null) return;
if (valor.toString().indexOf(getCadena()) >= 0) {
errores.add("excluir_cadena",
nombrePropiedad, nombreObjeto, getCadena());
}
}
public String getCadena() {
return cadena==null?"":cadena;
}
public void setCadena(String cadena) {
this.cadena = cadena;
}
}
Un validador ha de implementar
IPropertyValidator (1), esto le
obliga a tener un método
validate() en donde se ejecuta la
validación de la propiedad. Los argumentos del método
validate()
son:
- Messages errores: Un objeto de tipo Messages
que representa un conjunto de mensajes (una especie de colección
inteligente) y es donde podemos añadir los problemas de validación que
encontremos.
- Object valor: El valor a validar.
- String nombreObjeto: Nombre del objeto al que
pertenece la propiedad a validar. Útil para usarlo en los mensajes de
error.
- String nombrePropiedad: Nombre de la propiedad a
validar. Útil para usarlo en los mensajes de error.
Como se ve cuando encontramos un error de validación solo tenemos que
añadirlo (con
errores.add()) enviando un identificador de
mensaje y los argumentos. Para que este validador produzca un mensaje
significativo tenemos que tener en nuestro archivo de mensajes i18n la
siguiente entrada:
excluir_cadena={0} no puede contener {2} en {1}
Si el identificador que se envía no está en el archivo de mensajes, sale
tal cual al usuario; pero lo recomendado es siempre usar identificadores
del archivo de mensajes.
La validación es satisfactoria si no se añaden mensajes y se supone
fallida si se añaden. El sistema recolecta todos los mensajes de todos los
validadores antes de grabar y si encuentra los visualiza al usuario y no
graba.
A partir de v4.6.1 también es posible usar en el validador el mensaje de
@PropertyValidator.
Es decir, podemos escribir:
@PropertyValidator(value=ValidadorTituloLibro.class, message="{libro_rpg_no_permitido}")
private String titulo;
Si el mensaje está entre llaves se obtiene de los archivos i18n, si no se
usa tal cual.
Además, hemos de implementar la interfaz
IWithMessage en el validador:
public class ValidadorTituloLibro implements IPropertyValidator, IWithMessage {
private String message;
public void setMessage(String message) throws Exception {
this.message = message; // Este es message de @PropertyValidator
}
public void validate(Messages errors, Object value, String propertyName, String modelName) {
if (((String)value).contains("RPG")) {
errors.add(message); // Podemos añadir el mensaje directamente
}
}
}
El mensaje especificado en la anotación
@PropertyValidator,
libro_rpg_no_permitido,
se inyecta en el validador llamando a
setMessage(). Este mensaje
puede ser añadido directamente como un error.
El paquete
org.openxava.validators contiene algunos
validadores de uso común.
@PropertyValidator está definida como una restriccion de
Bean Validation a
partir de v5.3 y como una restricción de
Hibernate
Validator hasta v5.2.x
.
Si necesitas usar JPA en tu validador, mira
Usar
JPA en un validador o método de retrollamada.
Validador
por defecto (nuevo en v2.0.3)
Podemos definir validadores por defecto para las propiedades de cierto
tipo o estereotipo. Para esto se usa el archivo
validadores.xml
en
src/main/resources/xava (simplemente
xava antes v7)
de nuestro proyecto para definir en él los validadores por defecto.
Por ejemplo, podemos definir en nuestro
xava/validadores.xml lo
siguiente:
<validadores>
<validador-defecto>
<clase-validador
clase="org.openxava.test.validadores.ValidadorNombrePersona"/>
<para-estereotipo stereotipo="NOMBRE_PERSONA"/>
</validador-defecto>
</validadores>
En este caso estamos asociando el validador
ValidadorNombrePersona
al estereotipo NOMBRE_PERSONA. Ahora si definimos una propiedad como la
siguiente:
@Required @Stereotype("NOMBRE_PERSONA")
private String nombre;
Esta propiedad será validada usando
ValidadorNombrePersona
aunque la propiedad misma no defina ningun validador.
ValidadorNombrePersona
se aplica a todas las propiedades con el estereotipo NOMBRE_PERSONA.
Podemos también asignar validadores por defecto a un tipo.
En el archivo
validadores.xml podemos definir también los
validadores para determinar si un valor requerido está presente (ejecutado
cuando usamos
@Required). Además podemos asignar nombre (alias)
a las clases de los validadores.
Podemos aprender más sobre los validadores examinando
openxava/src/main/resources/xava/default-validators.xml
y
openxavatest/src/main/resources/xava/validators.xml.
Los validadores por defecto no se aplican cuando grabamos nuestras
entidades directamente con la api de JPA.
Cálculo
(nuevo en v5.7)
Con
@Calculation podemos definir una expresión aritmética para
hacer el cálculo de la propiedad. La expresión puede contener +, -, *, /,
(), valores numéricos y nombres de propiedades de la misma entidad. Por
ejemplo:
@Calculation("((horas * trabajador.precioHora) + desplazamiento - descuento) * porcentajeIVA / 100")
private BigDecimal total;
Fíjate como
trabajador.precioHora se usa para obtener el valor
de una referencia.
El cálculo se ejecuta y visualiza cuando el usuario cambia cualquier valor
de las propiedades usadas en la expresión en la interfaz de usuario, sin
embargo el valor no se graba hasta que el usuario no pulsa en el botón de
grabar. Todas las propiedades usadas en
@Calculation (los
operandos) tienen que estar visualizadas en la interfaz de usuario para
que
@Calculation funcione, si no es el caso deberías usar una
propiedad calculada convencional en su lugar.
Referencias
Una referencia hace que desde una entidad o agregado se pueda acceder otra
entidad o agregado. Una referencia se traduce a código Java como una
propiedad (con su
getter y su
setter) cuyo tipo es el
del modelo al que se referencia. Por ejemplo un
Cliente puede
tener una referencia a su
Comercial, y así podemos escribir
código Java como éste:
Cliente cliente = ...
cliente.getComercial().getNombre();
para acceder al nombre del comercial de ese cliente.
La sintaxis para definir referencias es:
@Required // 1
@Id // 2
@SearchKey // 3 Nuevo en v3.0.2
@DefaultValueCalculator // 4
@ManyToOne( // 5
optional=false // 1
)
private tipo nombreReferencia; // 5
public tipo getNombreReferencia() { ... } // 5
public void setNombreReferencia(tipo nuevoValor) { ... } // 5
- @ManyToOne(optional=false) (JPA), @Required (OX) (opcional, el JPA es el preferido): Indica si
la referencia es requerida. Al grabar OpenXava comprobará si las
referencias requeridas están presentes, si no lo están no se producirá
la grabación y se devolverá una lista de errores de validación.
- @Id (JPA, opcional): Para indicar si la referencia
forma parte de la clave. La combinación de propiedades y referencias
clave se debe mapear a un conjunto de campos en la base de datos que
no tengan valores repetidos, típicamente con la clave primaria.
- @DefaultValueCalculator
(OX, one, opcional): Para implementar la lógica
para calcular el valor inicial de la referencia. Este calculador ha de
devolver el valor de la clave, que puede ser un dato simple (solo si
la clave del objeto referenciado es simple) o un objeto clave (un
objeto especial que envuelve la clave primaria).
- @SearchKey (OX, optional): (Nuevo en v3.0.2) Las
referencias clave de búsqueda se usan por los usuarios para buscar los
objetos. Son editables en la interfaz de usuario de las referencias
permitiendo al usuario teclear su valor para buscar. OpenXava usa los
miembros clave (@Id) para buscar por defecto, y si los
miembros clave (@Id) están ocultos usa la primera propiedad
en la vista. Con @SearchKey podemos elegir referencias para
buscar explícitamente.
- Declaración de la referencia: Una declaración de
referencia convencional de Java con sus getters y setters.
La referencia se marca con @ManyToOne (JPA) y el tipo ha de ser otra
entidad.
Un pequeño ejemplo de referencias:
@ManyToOne
private Comercial comercial; // 1
public Comercial getComercial() {
return comercial;
}
public void setComercial(Comercial comercial) {
this.comercial = comercial;
}
@ManyToOne(fetch=FetchType.LAZY)
private Comercial comercialAlternativo; // 2
public Comercial getComercialAlternativo() {
return comercialAlternativo;
}
public void setComercialAlternativo(Comercial comercialAlternativa) {
this.comercialAlternativo = comercialAlternativo;
}
- Una referencia llamada comercial a la entidad Comercial.
- Una referencia llamada comercialAlternativo a la entidad Comercial.
En este caso usamos fetch=FetchType.LAZY, de esta manera los
datos son leidos de la base de datos bajo demanda. Este es el enfoque
más eficiente, pero no es el valor por defecto en JPA, por tanto es
aconsejable usar siempre fetch=FetchType.LAZY
al declarar las referencias.
Si asumimos que esto está en una entidad llamada
Cliente,
podemos escribir:
Cliente cliente = ...
Comercial comercial = cliente.getComercial();
Comercial comercialAlternativo = cliente.getComercialAlternativo();
Calculador
valor por defecto en referencias
En una referencia
@DefaultValueCalculator funciona
como
en una propiedad, solo que hay que devolver el valor de la clave de
la referencia.
Por ejemplo, en el caso de una referencia con clave simple podemos poner:
@ManyToOne(optional=false, fetch=FetchType.LAZY) @JoinColumn(name="FAMILY")
@DefaultValueCalculator(value=IntegerCalculator.class, properties=
@PropertyValue(name="value", value="2")
)
private Familia familia;
El método
calculate() de este calculador es:
public Object calculate() throws Exception {
return new Integer(value);
}
Como se puede ver se devuelve un entero, es decir, el valor para familia
por defecto es la familia cuyo código es el 2.
En el caso de clave compuesta sería así:
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumns({
@JoinColumn(name="ZONA", referencedColumnName="ZONA"),
@JoinColumn(name="ALMACEN", referencedColumnName="CODIGO")
})
@DefaultValueCalculator(CalculadorDefectoAlmacen.class)
private Almacen almacen;
Y el código del calculador:
package org.openxava.test.calculadores;
import org.openxava.calculators.*;
/**
* @author Javier Paniza
*/
public class CalculadorDefectoAlmacen implements ICalculator {
public Object calculate() throws Exception {
Almacen clave = new Almacen();
clave.setNumber(4);
clave.setZoneNumber(4);
return clave;
}
}
Devuelve un objeto de tipo
Almacen pero rellenando sólo las
propiedades clave.
Usar
referencias como clave
Podemos usar referencias como clave, o como parte de la clave. Hemos de
declarar la referencia como
@Id, y usar una clase clave, como sigue:
@Entity
@IdClass(DetalleAdicionalKey.class)
public class DetalleAdicional {
// JoinColumn se especifica también en DetalleAdicionalKey por un
// bug de Hibernate, ver http://opensource.atlassian.com/projects/hibernate/browse/ANN-361
@Id @ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="SERVICIO")
private Servicio servicio;
@Id @Hidden
private int contador;
...
}
Además, necesitamos escribir la clase clave:
public class DetalleAdicionalKey implements java.io.Serializable {
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="SERVICIO")
private Servicio servicio;
@Hidden
private int contador;
// equals, hashCode, toString, getters y setters
...
}
Necesitamos escribir la clase clave aunque la clave sea solo una
referencia con una sola columna clave.
Es mejor usar esta característica sólo cuando estemos trabajando contra
bases de datos legadas, si tenemos control sobre el esquema es mejor usar
un id autogenerado.
Referencias
incrustadas
Podemos referenciar a una
clase
incrustable usando la anotación
@Embedded. Por ejemplo, en la entidad
principal podemos escribir:
@Embedded
private Direccion direccion;
Y hemos de definir la clase
Direccion como incrustable:
package org.openxava.test.model;
import javax.persistence.*;
import org.openxava.annotations.*;
/**
*
* @author Javier Paniza
*/
@Embeddable
public class Direccion implements IConPoblacion {
@Required @Column(length=30)
private String calle;
@Required @Column(length=5)
private int codigoPostal;
@Required @Column(length=20)
private String poblacion;
// ManyToOne dentro de un Embeddable no está soportado en JPA 1.0 (ver en 9.1.34),
// pero la implementación de Hibernate lo soporta.
@ManyToOne(fetch=FetchType.LAZY, optional=false) @JoinColumn(name="STATE")
private Provincia provincia;
public String getPoblacion() {
return poblacion;
}
public void setPoblacion(String poblacion) {
this.poblacion = poblacion;
}
public String getCalle() {
return calle;
}
public void setCalle(String calle) {
this.calle = calle;
}
public int getCodigoPostal() {
return codigoPostal;
}
public void setCodigoPostal(int codigoPostal) {
this.codigoPostal = codigoPostal;
}
public Provincia getProvincia() {
return provincia;
}
public void setProvincia(Provincia provincia) {
this.provincia = provincia;
}
}
Como se ve una clase incrustable puede implementar una interfaz (1) y
contener referencias (2), entre otras cosas, pero no puede usar métodos de
retrollamada de JPA.
Este código se puede usar así, para leer:
Cliente cliente = ...
Direccion direccion = cliente.getDireccion();
direccion.getCalle(); // para obtener el valor
O así para establecer una nueva dirección
// para establecer una nueva dirección
Direccion direccion = new Direccion();
direccion.setCalle(“Mi calle”);
direccion.setCodigoPostal(46001);
direccion.setMunicipio(“Valencia”);
direccion.setProvincia(provincia);
cliente.setDireccion(direccion);
En este caso que tenemos una referencia simple, el código generado es un
simple JavaBean, cuyo ciclo de vida esta asociado a su objeto contenedor,
es decir, la
Direccion se borrará y creará junto al
Cliente,
jamas tendrá vida propia ni podrá ser compartida por otro
Cliente.
Colecciones
Colecciones
de entidades
Podemos definir colecciones de referencias a entidades. Una colección es
una propiedad Java que devuelve
java.util.Collection.
Aquí la sintaxis para definir una colección:
@Size // 1
@Condition // 2
@OrderBy // 3
@XOrderBy // 4
@OrderColumn // 5
@OneToMany/@ManyToMany // 6
private Collection<TuEntidad> nombreColeccion; // 5
public Collection<TuEntidad> getNombreColeccion() { ... } // 5
public void setNombreColeccion(Collection<TuEntidad> nuevoValor) { ... } // 5
- @Size (BV, HV, opcional): Cantidad mínima (min) y/o
máxima (max) de elementos esperados. Esto se valida antes de
grabar.
- @Condition (OX, opcional): Para restringir los elementos que
aparecen en la colección. No funciona en relaciones @ManyToMany.
- @OrderBy (JPA, opcional): Para que los elementos de la
colección aparezcan en un determinado orden.
- @XOrderBy (OX, opcional): @OrderBy de JPA no
permite usar propiedades calificadas (propiedades de referencias). @XOrderBy
sí lo permite.
- @OrderColumn
(JPA, opcional): (Nuevo en v5.3) El
orden de los elementos en la colección se guarda en la base de datos.
Una columna especial se crea en la tabla para mantener este orden. La
colección ha de ser una java.util.List. La interfaz de
usuario permite al usuario reordenar los elementos de la colección.
- Declaracion de la colección: Una declaración de
colección convencional de Java con sus getters y setters.
La colección se marca con @OneToMany (JPA) o @ManyToMany (JPA) y el tipo ha de ser otra
entidad.
Vamos a ver algunos ejemplos. Empecemos por uno simple:
@OneToMany (mappedBy="factura")
private Collection<Albaran> albaranes;
public Collection<Albaran> getAlbaranes() {
return albaranes;
}
public void setAlbaranes(Collection<Albaran> albaranes) {
this.albaranes = albaranes;
}
Si ponemos esto dentro de una
Factura, estamos definiendo una
colección de los
albaranes asociados a esa
Factura. La
forma de relacionarlo se hace en la parte del
mapeo
objeto-relacional. Usamos
mappedBy="factura" para indicar
que la referencia
factura de
Albaran se usa para
mapear esta colección.
Ahora podemos escribir código como este:
Factura factura = ...
for (Albaran albaran: factura.getAlbaranes()) {
albaran.hacerAlgo();
}
Para hacer algo con todos los albaranes asociados a una factura.
Vamos a ver otro ejemplo más complejo, también dentro de
Factura:
@OneToMany (mappedBy="factura", cascade=CascadeType.REMOVE) // 1
@OrderBy("tipoServicio desc") // 2
@org.hibernate.validator.Size(min=1) // 3
private Collection<LineaFactural> facturas;
- Usar REMOVE como tipo de cascadaas cascade type hace que cuando el
usuario borra una factura sus líneas también se borran.
- Con @OrderBy obligamos a que las lineas se devuelvan
ordenadas por tipoServicio.
- La restricción de @Size(min=1) hace que sea obligado que
haya al menos una línea para que la factura sea válida.
Tenemos libertad completa para definir como se obtienen los datos de una
colección, con
@Condition podemos sobreescribir la condición por
defecto:
@Condition(
"${almacen.codigoZona} = ${this.almacen.codigoZona} AND " +
"${almacen.codigo} = ${this.almacen.codigo} AND " +
"NOT (${codigo} = ${this.codigo})"
)
public Collection<Transportista> getCompañeros() {
return null;
}
Si ponemos esta colección dentro de
Transportista, podemos
obtener todos los transportista del mismo almacén menos él mismo, es
decir, la lista de sus compañeros. Es de notar como podemos usar
this
en la condición para referenciar al valor de una propiedad del objeto
actual.
@Condition solo aplica a la interfaz de usuario generada
por OpenXava, si llamamos directamente a
getFellowCarriers()
retornará null.
Si con esto no tenemos suficiente, podemos escribir completamente la
lógica que devuelve la colección. La colección anterior también se podría
haber definido así:
public Collection<Transportista> getCompañeros() {
Query query = XPersistence.getManager().createQuery("from Transportista t where " +
"t.almacen.codigoZona = :zona AND " +
"t.almacen.codigo = :codigoAlmacen AND " +
"NOT (t.codigo = :codigo) ");
query.setParameter("zona", getAlmacen().getCodigoZona());
query.setParameter("codigoAlmacen", getAlmacen().getCodigo());
query.setParameter("codigo", getCodigo());
return query.getResultList();
}
Como se ve es un método
getter. Obviamente ha de devolver una
java.util.Collection
cuyos elementos sean de tipo
Transportista.
Las referencias de las colecciones se asumen bidireccionales, esto quiere
decir que si en un
Comercial tengo una colección
clientes,
en
Cliente tengo que tener una referencia a
Comercial.
Pero puede ocurrir que en
Cliente tenga más de una referencia a
Comercial (por ejemplo,
comercial y
comercialAlternativo)
y entonce JPA no sabe cual escoger, por eso tenemos el atributo
mappedBy
de
@OneToMany. En este caso pondríamos:
@OneToMany(mappedBy="comercial")
private Collection<Cliente> clientes;
Para indicar que es la referencia
comercial y no
comercialAlternativo
la que vamos a usar para esta colección.
La anotación
@ManyToMany (JPA) permite definir una
colección con una multiciplidad de muchos-a-muchos. Como sigue:
@Entity
public class Cliente {
...
@ManyToMany
private Collection<Provincia> provincias;
...
}
En este caso un cliente tiene una colección de provincias, pero una misma
provincia puede estar presente en varios clientes.
Colecciones
incrustadas
Las colecciones de objetos incrustados no se soportaban en las primeras
versiones de JPA, por eso con OpenXava las simulabamos usando colecciones
a entidades con tipo de cascada REMOVE o ALL. OpenXava trata estas
colecciones de una manera especial y seguimos llamando a estas colecciones
colecciones incrustadas.
Ahora un ejemplo de una colección incrustada. En la entidad principal (por
ejemplo de
Factura) podemos poner:
@OneToMany (mappedBy="factura", cascade=CascadeType.REMOVE)
private Collection<LineaFactura> lineas;
Es de notar que usamos
CascadeType.REMOVE y
LineaFactura
es una entidad y no una clase incrustable:
package org.openxava.test.model;
import java.math.*;
import javax.persistence.*;
import org.hibernate.annotations.Columns;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.Parameter;
import org.hibernate.annotations.GenericGenerator;
import org.openxava.annotations.*;
import org.openxava.calculators.*;
import org.openxava.test.validators.*;
/**
*
* @author Javier Paniza
*/
@Entity
@EntityValidator(value=ValidadorLineaFactura.class,
properties= {
@PropertyValue(name="factura"),
@PropertyValue(name="oid"),
@PropertyValue(name="producto"),
@PropertyValue(name="precioUnitario")
}
)
public class LineaFactura {
@ManyToOne // 'Lazy fetching' produce un falla al borrar una linea desde la factura
private Factura factura;
@Id @GeneratedValue(generator="system-uuid") @Hidden
@GenericGenerator(name="system-uuid", strategy = "uuid")
private String oid;
private TipoServicio tipoServicio;
public enum TipoServicio { ESPECIAL, URGENTE }
@Column(length=4) @Required
private int cantidad;
@Stereotype("DINERO") @Required
private BigDecimal precioUnitario;
@ManyToOne(fetch=FetchType.LAZY, optional=false)
private Producto producto;
@DefaultValueCalculator(CurrentDateCalculator.class)
private java.util.Date fechaEntrega;
@ManyToOne(fetch=FetchType.LAZY)
private Comercial vendidoPor;
@Stereotype("MEMO")
private String observaciones;
@Stereotype("DINERO") @Depends("precioUnitario, cantidad")
public BigDecimal getImporte() {
return getPrecioUnitario().multiply(new BigDecimal(getCantidad()));
}
public boolean isGratis() {
return getImporte().compareTo(new BigDecimal("0")) <= 0;
}
@PostRemove
private void postRemove() {
factura.setComentario(factura.getComentario() + "DETALLE BORRADO");
}
public String getOid() {
return oid;
}
public void setOid(String oid) {
this.oid = oid;
}
public TipoServicio getTipoServicio() {
return tipoServicio;
}
public void setTipoServicio(TipoServicio tipoServicio) {
this.tipoServicio = tipoServicio;
}
public int getCantidad() {
return cantidad;
}
public void setCantidad(int cantidad) {
this.cantidad = cantidad;
}
public BigDecimal getPrecioUnitario() {
return precioUnitario==null?BigDecimal.ZERO:precioUnitario;
}
public void setPrecioUnitario(BigDecimal precioUnitario) {
this.precioUnitario = precioUnitario;
}
public Product getProducto() {
return producto;
}
public void setProducto(Producto producto) {
this.producto = producto;
}
public java.util.Date getFechaEntrega() {
return fechaEntrega;
}
public void setFechaEntrega(java.util.Date fechaEntrega) {
this.fechaEntrega = fechaEntrega;
}
public Comercial getVendidoPor() {
return vendidoPor;
}
public void setVendidoPor(Comercial vendidoPor) {
this.vendidoPor = vendidoPor;
}
public String getObservaciones() {
return observaciones;
}
public void setObservaciones(String observaciones) {
this.observaciones = observaciones;
}
public Invoice getFactura() {
return factura;
}
public void setFactura(Factura factura) {
this.factura = factura;
}
}
Como se ve esto es una entidad compleja, con calculadores, validadores,
referencias y así por el estilo. También hemos de definir una referencia a
su clase contenedora (
factura). En este caso cuando una factura
se borre todas sus líneas se borrarán también. Además hay diferencias a
nivel de interface gráfica (podemos aprender más en el capítulo de la
vista).
Colecciones
de elementos (nuevo en v5.0)
A partir de JPA 2.0 puedes definir una colección de auténticos
objetos is.
Llamamos a estas colecciones
colecciones de elementos.
Esta es la sintaxis para las colecciones de elementos:
@Size // 1
@OrderBy // 2
@OrderColumn // 3 Nuevo en v5.3
@ElementCollection // 4
private Collection<TuClaseIncrustable> nombreColeccion; // 3
public Collection<TuClaseIncrustable> getNombreColeccion() { ... } // 3
public void setNombreColeccion<TuClaseIncrustable> nuevoValor) { ... } // 3
- @Size (BV, HV, opcional): Cantidad
mínima (min) y/o máxima (max) de elementos
esperados. Esto se valida antes de grabar.
- @OrderBy (JPA, opcional): Para
que los elementos de la colección aparezcan en un determinado orden.
- @OrderColumn
(JPA, opcional): (Nuevo en v5.3) El
orden de los elementos en la colección se guarda en la base de datos.
Una columna especial se crea en la tabla para mantener este orden. La
colección ha de ser una java.util.List. La interfaz de
usuario permite al usuario reordenar los elementos de la colección.
- Collection declaration: Una declaración de
colección Java convencional con sus getters y setters.
La colección se marca con @ElementCollection (JPA). Los elementos
tienes que ser clases
incrustables.
Los elementos en la colección se graban todos a la vez al mismo tiempo que
la entidad principal. Además, la interfaz de usuario generada permite
modificar todos los elementos de la colección al mismo tiempo.
Una clase incrustable usada en una colección de elementos no puede
contener colecciones de ningún tipo.
Veamos un ejemplo. Primero hemos de definir la colección en la entidad
principal:
@Entity
public class Presupuesto extends Identifiable {
...
@ElementCollection
private Collection<LineaPresupuesto> lineas;
public Collection<LineaPresupuesto> getLineas() {
return lineas;
}
public void setLineas(Collection<LineaPresupuesto> lineas) {
this.lineas = lineas;
}
...
}
Ahora definimos nuestra clase incrustada:
@Embeddable
public class LineaPresupuesto {
@ManyToOne(fetch=FetchType.LAZY, optional=false) // 1
private Producto producto;
@Required // 2
private BigDecimal precioUnitario;
@Required
private int cantidad;
private Date fechaDisponibilidad;
@Column(length=30)
private String comentarios;
@Column(precision=10, scale=2)
@Depends("precioUnitario, cantidad")
public BigDecimal getImporte() { // 3
return getPrecioUnitario().multiply(new BigDecimal(getCantidad()));
}
...
}
Como se puede ver, una clase incrustable usada en una colección de
elementos puede contener referencias(1), validaciones(2) y propiedades
calculadas(3) entre otras cosas.
Listas
con @OrderColumn (nuevo en v5.3)
Para tener una colección que mantenga el orden de sus elementos se ha de
usar
java.util.List en lugar de
java.util.Collection y
hay que anotar la colección con
@OrderColumn. Es decir, si definimos una
colección de esta forma:
@OneToMany(mappedBy="proyecto", cascade=CascadeType.ALL)
@OrderColumn
private List<TareaProyecto> tareas;
La interfaz de usuario permitirá al usuario cambiar el orden de los
elementos y este orden se almacenará en la base de datos. Además, si se
cambia el orden de los elementos por código este orden también se
persistirá en la base de datos.
Para almacenar el orden, JPA usa una columna especial en la tabla de la
base de datos, esta columna es para uso interno exclusivamente y no hay
una propiedad para poder acceder a ella desde el código. Podemos usar
@OrderColumn(name="MYCOLUMN")
para especificar el nombre de la columna si lo necesitamos, si
name
no se especifica se asume el nombre de la colección más "_ORDER". Si se
usa
updateSchema, será la herramienta la que cree la columna
automáticamente. Si no, es decir, si controlamos el esquema de la base de
datos nosotros mismos, deberiamos añadir la columna a la tabla, para la
colección de arriba sería así:
ALTER TABLE TAREAPROYECTO
ADD TAREAS_ORDER INTEGER
En la implementación actual el usuario cambia el orden arrastrando y
soltando, con colecciones
@OneToMany el orden se almacena justo
después de soltar, mientras que en las colecciones
@ElementCollection
el orden se almacen al grabar la entidad contenedora.
Métodos
Los métodos se definen en una entidad OpenXava (mejor dicho, en una
entidad JPA) como una clase de Java convencional. Por ejemplo:
public void incrementarPrecio() {
setPrecioUnitario(getPrecioUnitario().multiply(new BigDecimal("1.02")).setScale(2));
}
Los métodos son la salsa de los objetos, sin ellos solo serían caparazones
tontos alrededor de los datos. Cuando sea posible es mejor poner la lógica
de negocio en los métodos (capa del modelo) que en las acciones (capa del
controlador).
Buscadores
Un buscador es método estático especial que nos permite buscar un objeto o
una colección de objetos que sigue algún criterio.
Algunos ejemplos:
public static Cliente findByCodigo(int codigo) throws NoResultException {
Query query = XPersistence.getManager().createQuery(
"from Cliente as o where o.codigo = :codigo");
query.setParameter("codigo", codigo);
return (Cliente) query.getSingleResult();
}
public static Collection findTodos() {
Query query = XPersistence.getManager().createQuery("from Cliente as o");
return query.getResultList();
}
public static Collection findByNombreLike(String nombre) {
Query query = XPersistence.getManager().createQuery(
"from Cliente as o where o.nombre like :nombre order by o.nombre desc");
query.setParameter("nombre", nombre);
return query.getResultList();
}
Estos métodos se pueden usar de esta manera:
Cliente cliente = Cliente.findByCodigo(8);
Collection javieres = Cliente.findByNombreLike(“%JAVI%”);
Como se ve, usar método buscadores produce un código más legible que
usando la verbosa API de JPA. Pero esto es solo una recomendación de
estilo, podemos escoger no escribir métodos buscadores y usar directamente
consultas de JPA.
Validador de
entidad
Este validador (
@EntityValidator) permite poner una validación
a nivel de modelo. Cuando necesitamos hacer una validación sobre varias
propiedades del modelo, y esta validación no corresponde lógicamente a
ninguna de ellas se puede usar este tipo de validación.
Su sintaxis es:
@EntityValidator(
value=clase, // 1
onlyOnCreate=(true|false), // 2
properties={ @PropertyValue ... } // 3
)
- value (opcional, obligada si no se especifica
nombre): Clase que implementa la validación. Ha de ser del tipo IValidator.
- onlyOnCreate (opcional): Si true el validador es
ejecutado solo cuando estamos creando un objeto nuevo, no cuando
modificamos uno existente. El valor por defecto es false.
- properties (varios @PropertyValue, opcional): Para establecer
valor a las propiedades del validador antes de ejecutarse.
Un ejemplo:
@EntityValidator(value=org.openxava.test.validadores.ValidadorProductoBarato.class, properties= {
@PropertyValue(name="limite", value="100"),
@PropertyValue(name="descripcion"),
@PropertyValue(name="precioUnitario")
})
public class Producto {
Y el código del validador:
package org.openxava.test.validadores;
import java.math.*;
/**
* @author Javier Paniza
*/
public class ValidadorProductoBarato implements IValidator { // 1
private int limite;
private BigDecimal precioUnitario;
private String descripcion;
public void validate(Messages errores) { // 2
if (getDescripcion().indexOf("CHEAP") >= 0 ||
getDescripcion().indexOf("BARATO") >= 0 ||
getDescripcion().indexOf("BARATA") >= 0) {
if (getLimiteBd().compareTo(getPrecioUnitario()) < 0) {
errors.add("producto_barato", getLimiteBd()); // 3
}
}
}
public BigDecimal getPrecioUnitario() {
return precioUnitario;
}
public void setPrecioUnitario(BigDecimal decimal) {
precioUnitario = decimal;
}
public String getDescripcion() {
return descripcion==null?"":descripcion;
}
public void setDescripcion(String string) {
descripcion = string;
}
public int getLimite() {
return limite;
}
public void setLimite(int i) {
limite = i;
}
private BigDecimal getLimiteBd() {
return new BigDecimal(Integer.toString(limite));
}
}
Este validador ha de implementar
IValidator (1), lo que le
obliga a tener un método
validate(Messages messages) (2). En
este método solo hay que añadir identificadores de mensajes de error (3)
(cuyos textos estarán en los archivos i18n), si en el proceso de
validación (es decir en la ejecución de todos los validadores) hubiese al
menos un mensaje de error, OpenXava no graba la información y visualiza
los mensajes al usuario.
En este caso vemos como se accede a
descripcion y
precioUnitario,
por eso la validación se pone a nivel de módelo y no a nivel de propiedad
individual, porque abarca más de una propiedad.
A partir de v4.6.1 el validador puede implementar
IWithMessage para inyectar el mensaje desde
@EntityValidator,
funciona como en
el
caso del validador de propiedad.
Podemos definir más de un validador por entidad como sigue:
@EntityValidator(value=org.openxava.test.validadores.ValidadorProductoBarato.class, properties= {
@PropertyValue(name="limite", value="100"),
@PropertyValue(name="descripcion"),
@PropertyValue(name="precioUnitario")
})
@EntityValidator(value=org.openxava.test.validadores.ValidadorProductoCaro.class, properties= {
@PropertyValue(name="limite", value="1000"),
@PropertyValue(name="descripcion"),
@PropertyValue(name="precioUnitario")
})
@EntityValidator(value=org.openxava.test.validadores.ValidadorPrecioProhibido.class,
properties= {
@PropertyValue(name="precioProhibido", value="555"),
@PropertyValue(name="precioUnitario")
},
onlyOnCreate=true
)
public class Product {
Con un OpenXava anterior a 6.1 has de usar
@EntityValidators
para poder aplicar varios validadores:
@EntityValidators({ // Sólo necesario hasta v6.0.2
@EntityValidator(value=org.openxava.test.validadores.ValidadorProductoBarato.class, properties= {
@PropertyValue(name="limite", value="100"),
@PropertyValue(name="descripcion"),
@PropertyValue(name="precioUnitario")
}),
@EntityValidator(value=org.openxava.test.validadores.ValidadorProductoCaro.class, properties= {
@PropertyValue(name="limite", value="1000"),
@PropertyValue(name="descripcion"),
@PropertyValue(name="precioUnitario")
}),
@EntityValidator(value=org.openxava.test.validadores.ValidadorPrecioProhibido.class,
properties= {
@PropertyValue(name="precioProhibido", value="555"),
@PropertyValue(name="precioUnitario")
},
onlyOnCreate=true
)
})
public class Product {
@EntityValidator está definida como una restriccion de
Bean Validation a partir de v5.3 y
como una restricción de
Hibernate
Validator hasta v5.2.x
.
Si necesitas usar JPA en tu validador, mira
Usar
JPA en un validador o método de retrollamada.
Validador al
borrar
El
@RemoveValidator también es un validador a
nivel de modelo, la diferencia es que se ejecuta antes de borrar el
objeto, y tiene la posibilidad de vetar el borrado.
Su sintaxis es:
@RemoveValidator(
value=clase, // 1
properties={ @PropertyValue ... } // 2
)
- clase (obligada): Clase que implementa la
validación. Ha de ser del tipo IRemoveValidator.
- properties (varios @PropertyValue, opcional): Para establecer
valor a las propiedades del calculador antes de ejecutarse.
Un ejemplo puede ser:
@RemoveValidator(value=ValidadorBorrarTipoAlbaran.class,
properties=@PropertyValue(name="codigo")
)
public class TipoAlbaran {
Y el validador:
package org.openxava.test.validadores;
import org.openxava.test.model.*;
import org.openxava.util.*;
import org.openxava.validators.*;
/**
* @author Javier Paniza
*/
public class ValidadorBorrarTipoAlbaran implements IRemoveValidator { // 1
private TipoAlbaran tipoAlbaran;
private int codigo; // Usamos esto (en vez de obtenerlo de tipoAlbaran)
// para probar @PropertyValue con propiedades simples
public void setEntity(Object entidad) throws Exception { // 2
this.tipoAlbaran = (TipoAlbaran) entidad;
}
public void validate(Messages errores) throws Exception {
if (!tipoAlbaran.getAlbaranes().isEmpty()) {
errores.add("no_borrar_tipo_albaran_si_albaranes", new Integer(getCodigo())); // 3
}
}
public int getCodigo() {
return codigo;
}
public void setCodigo(int codigo) {
this.codigo = codigo;
}
}
Como se ve tiene que implementar
IRemoveValidator (1) lo que le
obliga a tener un método
setEntity() (2) con el recibirá el
objeto que va a ser borrado. Si hay algún error de validación se añade al
objeto de tipo
Messages enviado a
validate() (3). Si
después de ejecutar todas las validaciones OpenXava detecta al menos 1
error de validación no realizará el borrado del objeto y enviará la lista
de mensajes al usuario.
En este caso si se comprueba si hay albaranes que usen este tipo de
albarán antes de poder borrarlo.
Tal y como ocurre con
@EntityValidator podemos usar varios
@RemoveValidator
por entidad (con la anotación
@RemoveValidators para versiones
anteriores a 6.1).
@RemoveValidator se ejecuta cuando borramos entidades desde
OpenXava (usando
MapFacade o las acciones estándar de OX), pero
no cuando usamos directamente JPA. Si queremos crear una restricción al
borrar que sea reconocida por JPA, podemos usar un método de retrollamada
de JPA, como
@PreRemove.
Métodos
de retrollamada de JPA
Con
@PrePersist podemos indicar que se ejecute
cierta lógica justo antes de crear el objeto como persistente.
Como sigue:
@PrePersist
private void antesDeCrear() {
setDescripcion(getDescripcion() + " CREADO");
}
En este caso cada vez que se graba por primera vez un
TipoAlbaran
se añade un sufijo a su descripción.
Como se ve es exactamente igual que cualquier otro método solo que este se
ejecuta automáticamente antes de crear.
Con
@PreUpdate podemos indicar que se ejecute
cierta lógica justo después de modificar un objeto y justo antes de
actualizar su contenido en la base de dato, esto es justo antes de hacer
el UPDATE.
Como sigue:
@PreUpdate
private void antesDeModificar() {
setDescripcion(getDescripcion() + " MODIFICADO");
}
En este caso cada vez que se modifica un
TipoAlbaran se añade un
sufijo a su descripción.
Como se ve es exactamente igual que cualquier otro método solo que este se
ejecuta automáticamente antes de modificar.
Podemos usar todas las anotaciones JPA de retrollamada:
@PrePersist,
@PostPersist,
@PreRemove,
@PostRemove,
@PreUpdate,
@PostUpdate y
@PostLoad.
Métodos
de retrollamada de OX (nuevo en V4.0.1)
Con
@PreCreate puede marcar métodos que serán ejecutados antes
de persistir algún objeto. De esta manera podrá utilizar el manejador de
persistencia o crear busquedas que no son permitidas dentro de las
retrollamadas de JPA.
Por ejemplo, si queremos crear un cliente automaticamente si en la factura
no se ha seleccionado ninguno.
public onPreCreate {
// Crea automaticamente un cliente
if (getCliente() == null) {
Cliente clte = new Cliente();
clte.setNombre(getNombre());
clte.setDireccion(getDireccion());
clte = XPersistence.getManager().merge(clte);
setCliente(clte);
}
}
En este ejemplo, la operación del manejador de persistencia, no afectará
el comportamiento de este y las demás retrollamadas. Además de
@PreCreate
están disponible
@PostCreate y
@PreDelete. Los métodos
que son decorados con estas anotaciones forman parte de la misma
transacción donde se ejecutaran las retrollamadas de JPA. Cuando estas
retrollamadas son combinadas con las de JPA el orden de ejecución es de
acuerdo a lo siguiente:
Para crear una entidad:
@PreCreate, @PrePersist(JPA),
@PostPersist(JPA) y
@PostCreate.
Para borrar una entidad:
@PreDelete, @PreRemove(JPA) y
@PostRemove(JPA).
Los métodos anotados con estas anotaciones deben no retornar ningún valor
ni tener ningún parámetro. A diferencia de las retrollamadas de JPA, las
de OX sólo funcionan en las entidades mismas y no son tomadas en cuentas
en los clases indicadas en
@Listeners.
Herencia
OpenXava soporta la herencia de
herencia de JPA y Java.
Por ejemplo podemos definer una superclase mapeada (
@MappedSuperclass) de esta manera:
package org.openxava.test.model;
import javax.persistence.*;
import org.hibernate.annotations.*;
import org.openxava.annotations.*;
/**
* Clase base para definir entidades con un oid UUID. <p>
*
* @author Javier Paniza
*/
@MappedSuperclass
public class Identificable {
@Id @GeneratedValue(generator="system-uuid") @Hidden
@GenericGenerator(name="system-uuid", strategy = "uuid")
private String oid;
public String getOid() {
return oid;
}
public void setOid(String oid) {
this.oid = oid;
}
}
Podemos definir otra
@MappedSuperclass que extienda de esta, por
ejemplo:
package org.openxava.test.model;
import javax.persistence.*;
import org.openxava.annotations.*;
/**
* Clase base para entidades con una propiedad 'nombre'. <p>
*
* @author Javier Paniza
*/
@MappedSuperclass
public class ConNombre extends Identifiable {
@Column(length=50) @Required
private String nombre;
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
}
Ahora podemos usar
Identificable y
ConNombre para
definir nuestra entidades, como sigue:
package org.openxava.test.model;
import javax.persistence.*;
/**
*
* @author Javier Paniza
*/
@Entity
@DiscriminatorColumn(name="TYPE")
@DiscriminatorValue("HUM")
@Table(name="PERSONA")
@AttributeOverrides(
@AttributeOverride(name="name", column=@Column(name="PNOMBRE"))
)
public class Humano extends ConNombre {
@Enumerated(EnumType.STRING)
private Sexo sexo;
public enum Sexo { MASCULINO, FEMENINO };
public Sexo getSexo() {
return sexo;
}
public void setSexo(Sexo sexo) {
this.sexo = sexo;
}
}
Y ahora, la auténtica herencia de entidades, una entidad que extiende de
otra entidad:
package org.openxava.test.model;
import javax.persistence.*;
/**
*
* @author Javier Paniza
*/
@Entity
@DiscriminatorValue("PRO")
public class Programador extends Humano {
@Column(length=20)
private String lenguajePrincipal;
public String getLenguajePrincipal() {
return lenguajePrincipal;
}
public void setLenguajePrincipal(String lenguajePrincipal) {
this.lenguajePrincipal = lenguajePrincipal;
}
}
Podemo crear un
módulo
OpenXava para
Humano y
Programador (no para
Identificable
ni
ConNombre directamente). En el módulo de
Programador
el usuario puede acceder solo a programadores, por otra parte usando el
módulo de
Humano el usuario puede acceder a objetos de tipo
Humano
y
Programador. Además cuando el usuario trata de visualizar el
detalle de un
Programador desde el módulo de
Humano se
mostrará la vista de
Programador. Polimorfismo puro.
A partir de v4.5 OpenXava soporta todas las características de la herencia
de JPA, incluyendo una única tabla por jerarquica, tabla para superclase y
tabla para subclase (joined) y tabla por clase como estrategias de mapeo,
antes de v4.5 sólo
@AttributeOverrides y la estrategia de una
única tabla por jerarquía se soportaban.
Clave múltiple
La forma preferida para definir la clave de una entidad es una clave única
autogenerada (anotada con
@Id y
@GeneratedValue), pero a veces, por ejemplo
cuando vamos contra bases de datos legadas, necesitamos tener una entidad
mapeada a una tabla que usa varias columnas como clave. Este caso se pude
resolver con JPA (y por tanto con OpenXava) de dos formas, usando
@IdClass o usando
@EmbeddedId
Clase id
En este caso usamos
@IdClass en nuestra entidad para indicar una
clase clave, y marcamos las propiedades clave como
@Id en nuestra entidad:
package org.openxava.test.model;
import javax.persistence.*;
import org.openxava.annotations.*;
import org.openxava.jpa.*;
/**
*
* @author Javier Paniza
*/
@Entity
@IdClass(AlmacenKey.class)
public class Almacen {
@Id
// Column también se especifica en AlmacenKey por un bug en Hibernate, ver
// http://opensource.atlassian.com/projects/hibernate/browse/ANN-361
@Column(length=3, name="ZONA")
private int codigoZona;
@Id @Column(length=3)
private int codigo;
@Column(length=40) @Required
private String nombre;
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public int getCodigo() {
return codigo;
}
public void setCodigo(int codigo) {
this.codigo = codigo;
}
public int getCodigoZona() {
return codigoZona;
}
public void setCodigoZona(int codigoZona) {
this.codigoZona = codigoZona;
}
}
También necesitamos declarar una clase id, una clase serializable normal y
corriente con todas las propiedades clave de la entidad:
package org.openxava.test.model;
import java.io.*;
import javax.persistence.*;
/**
*
* @author Javier Paniza
*/
public class AlmacenKey implements Serializable {
@Column(name="ZONE")
private int codigoZona;
private int codigo;
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
return obj.toString().equals(this.toString());
}
@Override
public int hashCode() {
return toString().hashCode();
}
@Override
public String toString() {
return "AlmacenKey::" + codigoZona + ":" + codigo;
}
public int getCodigo() {
return codigo;
}
public void setCodigo(int codigo) {
this.codigo = codigo;
}
public int getCodigoZona() {
return codigoZona;
}
public void setCodigoZona(int codigoZona) {
this.codigoZona = codigoZona;
}
}
Id
inscrustado
En este case tenemos una referencia a un objeto incrustado (
@Embeddable) marcada como
@EmbeddedId:
package org.openxava.test.model;
import javax.persistence.*;
import org.openxava.annotations.*;
/**
*
* @author Javier Paniza
*/
@Entity
public class Almacen {
@EmbeddedId
private AlmacenKey clave;
@Column(length=40) @Required
private String nombre;
public AlmacenKey getClave() {
return clave;
}
public void setClave(AlmacenKey clave) {
this.clave = clave;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
}
Y nuestra clave es una clase incrustable que contiene las propiedades
clave:
package org.openxava.test.model;
import javax.persistence.*;
/**
*
* @author Javier Paniza
*/
@Embeddable
public class AlmacenKey implements java.io.Serializable {
@Column(length=3, name="ZONA")
private int codigoZona;
@Column(length=3)
private int codigo;
public int getCodigo() {
return codigo;
}
public void setCodigo(int codigo) {
this.codigo = codigo;
}
public int getCodigoZona() {
return codigoZona;
}
public void setCodigoZona(int codigoZona) {
this.codigoZona = codigoZona;
}
}
Bean Validation
OpenXava tiene soporte completo del estándar Java para validación:
Bean Validation.
(1.1 JSR-349 desde v5.3 y 1.0 JSR-303 desde v4.1) Podemos definir nuestras
propias restricciones en nuestras entidades como se explica en la
especificación
Bean Validation, y OpenXava las reconocerá,
mostrando los mensajes de error correspondientes al usuario. Consulta la
última documentación de Hibernate Validator para
aprender como escribir un validador JSR-349, ya que la versión actual de
Hibernate Validator implementa JSR-349.
Además, a partir de v5.3 las anotaciones de OpenXava
@Required,
@PropertyValidator y
@EntityValidator están definidas como
restricciones de Bean Validation, esto significa que cuando grabamos una
entidad usando directamente JPA estas validaciones se aplicarán.
Por otra parte,
@RemoveValidator,
@PropertyValidator(onlyOnCreate=true),
EntityValidator(onlyOnCreate=true) y la característica de
validador
por defecto de OpenXava no son reconocidas ni por Bean Validation ni
por JPA, sino solo por OpenXava.
@AssertTrue
A partir de v4.9 OpenXava permite inyectar propiedades y propiedades
calificadas (propiedades de referencias) del bean validado, en el mensaje
identificado mediante el elemento message de
AssertTrue. Por ejemplo:
En este caso tenemos a
@AssertTrue anotando el campo de la
Entidad:
import javax.persistence.*;
import org.openxava.annotations.*;
import org.openxava.model.*
import javax.validation.constraints.*;
@Entity
public class Conductor extends Identifiable{
@Required
@Column(length = 40)
private String nombre;
@AssertTrue(message = "{no_puede_conducir}")
private boolean puedeConducir;
//getters y setters...
}
{no_puede_conducir} es el identificador de mensaje que se
encuentra declarado en el archivo i18n así:
no_puede_conducir=Conductor {nombre} no puede ser registrado: debe aprobar el examen de conducir
Si intentamos crear una entidad con
nombre=MIGUEL GRAU y
puedeConducir=false
se mostrará el mensaje de error:
Conductor MIGUEL GRAU no puede ser registrado: debe aprobar el examen
de conducir
En este caso tenemos a
@AssertTrue anotando un método de la
Entidad:
import javax.persistence.*;
import org.openxava.annotations.*;
import org.openxava.model.*;
import javax.validation.constraints.*;
@Entity
public class Vehiculo extends Identifiable{
@Required
@Column(length = 15)
private String tipo;
@Required
@Column(length = 7)
private String placa;
private boolean puedeCircular;
@ManyToOne
private Conductor conductor;
@AssertTrue(message="{no_puede_circular}")
private boolean isAptoParaCircular(){
return driver == null || roadworthy;
}
//getters y setters...
}
{no_puede_circular} es el identificador de mensaje que se
encuentra declarado en el archivo i18n así:
no_puede_circular={tipo} de placa {placa} no es apto para circular. No se puede asignar al conductor {conductor.nombre}
Si tenemos la entidad con: tipo=AUTO, placa=A1-0001 y puedeCircular=false;
e intentamos asignar conductor (nombre=MIGUEL GRAU), el método de
validación fallará y se mostrará el mensaje de error:
AUTO de placa A1-0001 no es apto para circular. No se puede asignar al
conductor MIGUEL GRAU
Hibernate
Validator (nuevo en v3.0.1)
OpenXava tiene soporte completo de
Hibernate Validator con soporte de Bean Validation
(nuevo
en v4.1). Hibernate Validator 3.x (con la vieja API) se soportó
hasta v5.2.x. Podemos definir nuestras propias restricciones en nuestras
entidades como se explica en la
documentación
de Hibernate Validator, y OpenXava las reconocerá, mostrando los
mensajes de error correspondientes al usuario.
Además, las anotaciones de OpenXava
@Required,
@PropertyValidator y
@EntityValidator están definidas como
restricciones de Hibernate Validator 3.x hasta v5.2.x y como restricciones
de Bean Validation a partir de v5.3, esto significa que cuando grabamos
una entidad usando directamente JPA estas validaciones se aplicarán.