Curso: 1. Primeros pasos | 2. Modelar con Java | 3. Pruebas automáticas | 4. Herencia | 5. Lógica de negocio básica | 6. Validación avanzada | 7. Refinar el comportamiento predefinido | 8. Comportamiento y lógica de negocio | 9. Referencias y colecciones | A. Arquitectura y filosofía | B. Java Persistence API | C. Anotaciones

Lección 5: Lógica de negocio básica

Has convertido tu modelo del dominio en una aplicación web plenamente funcional. Esta aplicación ya es bastante útil de por sí, aunque aún puedes hacerle muchas mejoras. Transformemos pues tu aplicación en algo más serio, y de paso, aprendamos algunas cosas interesantes sobre OpenXava.
Empezaremos por añadir algo de lógica de negocio a tus entidades para hacer de tu aplicación algo más que un simple gestor de base de datos.

Propiedades calculadas

Quizás la lógica de negocio más simple que puedes añadir a tu aplicación es una propiedad calculada. Las propiedades que has usado hasta ahora son persistentes, es decir, cada propiedad se almacena en una columna de una tabla de la base de datos. Una propiedad calculada es una propiedad que no almacena su valor en la base de datos, sino que se calcula cada vez que se accede a la propiedad. Observa la diferencia entre una propiedad persistente y una calculada:
// Propiedad persistente
private int cantidad; // Tiene un campo, por tanto es persistente
 
public int getCantidad() { // Un getter para devolver el valor del campo
    return cantidad;
}
 
public void setCantidad(int cantidad) { // Cambia el valor del campo
    this.cantidad = cantidad;
}
 
// Propiedad calculada
public int getImporte() { // No tiene campo, ni setter, solo un getter
    return cantidad * precio; // con un cálculo
}
Las propiedades calculadas son reconocidas automáticamente por OpenXava. Puedes usarlas en vistas, listas tabulares o cualquier otra parte de tu código.
Vamos a usar propiedades calculadas para añadir el elemento “económico” a nuestra aplicación Facturacion. Porque, tenemos líneas de detalle, productos, cantidades. Pero, ¿qué pasa con el dinero?

Propiedad calculada simple

El primer paso será añadir una propiedad de importe a Detalle. Lo que queremos es que cuando el usuario elija un producto y teclea la cantidad el importe de la línea sea recalculado y mostrado al usuario:
business-logic_es010.png
Añadir esta funcionalidad a tu actual código es prácticamente añadir una propiedad calculada a Detalle. Simplemente añade el código siguiente a la clase Detalle:
@Stereotype("DINERO")
@Depends("producto.numero, cantidad") // Cuando usuario cambie producto o cantidad
public BigDecimal getImporte() { // esta propiedad se recalculará y se redibujará
    return new BigDecimal(cantidad).multiply(producto.getPrecio());
}
Es tan solo poner el cálculo en getImporte() y usar @Depends para indicar a OpenXava que la propiedad importe depende de producto.numero y cantidad, así cada vez que el usuario cambia alguno de estos valores la propiedad se recalculará.
Ahora has de añadir esta nueva propiedad a la lista de propiedades mostradas en la colección detalles de DocumentoComercial:
@ElementCollection
@ListProperties("producto.numero, producto.descripcion, cantidad, importe") // importe añadida
private Collection<Detalle> detalles;
Nada más. Tan solo necesitas añadir el getter y modificar la lista de propiedades. Ahora puedes probar los módulos Factura y Pedido para ver la propiedad importe en acción.
Nota:
Verifica que los productos tengan precio registrado.

Usar @DefaultValueCalculator

La forma en que calculamos el importe de la línea de detalle no es la mejor. Tiene, al menos, dos inconvenientes. El primero es que el usuario puede querer tener la posibilidad de cambiar el precio unitario. Y segundo, si el precio de un producto cambia los importes de todas las facturas cambian también, y esto no es bueno.
Para evitar estos inconvenientes lo mejor es almacenar el precio de cada producto en cada línea de detalle. Añadamos pues una propiedad persistente precioPorUnidad a la clase Detalle, y calculemos su valor desde precio de Producto usando un @DefaultValueCalculator. De tal forma que consigamos el efecto que puedes ver en la siguiente figura:
business-logic_es020.png
El primer paso es obviamente añadir la propiedad precioPorUnidad. Añade el siguiente código a la clase Detalle:
@DefaultValueCalculator(
    value=CalculadorPrecioPorUnidad.class, // Esta clase calcula el valor inicial
    properties=@PropertyValue(
        name="numeroProducto", // La propiedad numeroProducto del calculador...
        from="producto.numero") // ... se llena con el valor de producto.numero de la entidad
)
@Stereotype("DINERO")
private BigDecimal precioPorUnidad; // Una propiedad persistente convencional...
 
public BigDecimal getPrecioPorUnidad() { // ... con sus getter y setter
    return precioPorUnidad == null ? BigDecimal.ZERO : precioPorUnidad; // así nunca devuelve nulo
}
 
public void setPrecioPorUnidad(BigDecimal precioPorUnidad) {
    this.precioPorUnidad = precioPorUnidad;
}
CalculadorPrecioPorUnidad contiene la lógica para calcular el valor inicial. Simplemente lee el precio del producto. Observa el código de este calculador:
package org.openxava.facturacion.calculadores; // En el paquete calculadores
 
import org.openxava.calculators.*;
import org.openxava.facturacion.modelo.*;
 
import static org.openxava.jpa.XPersistence.*; //Para usar getManager()
 
public class CalculadorPrecioPorUnidad implements ICalculator {
 
    private int numeroProducto;
 
    @Override
    public Object calculate() throws Exception {
        Producto producto = getManager() // getManager() de XPersistence
            .find(Producto.class, numeroProducto); // Busca el producto
        return producto.getPrecio();    // Retorna su precio
    }
 
    public int getNumeroProducto() {
        return numeroProducto;
    }
 
    public void setNumeroProducto(int numeroProducto) {
        this.numeroProducto = numeroProducto;
    }
}
De esta forma cuando el usuario escoge un producto el campo de precio unitario se rellena con el precio del producto, pero, dado que es una propiedad persistente, el usuario puede cambiar este valor. Y si en el futuro el precio del producto cambiara este precio unitario de la línea de detalle no cambiaría.
Esto implica que has de adaptar la propiedad calculada importe:
@Stereotype("DINERO")
@Depends("precioPorUnidad, cantidad") // precioPorUnidad en vez de producto.numero
public BigDecimal getImporte() {
    return new BigDecimal(cantidad).multiply(getPrecioPorUnidad()); // getPrecioPorUnidad() en vez de producto.getPrecio()
}
Ahora getImporte() usa precioPorUnidad como fuente en lugar de producto.precio.
Finalmente, debemos editar la entidad DocumentoComercial y modificar la lista de propiedades de la colección para mostrar la nueva propiedad:
@ElementCollection
@ListProperties("producto.numero, producto.descripcion, cantidad, precioPorUnidad, importe") // precioPorUnidad añadida
private Collection<Detalle> detalles;
Prueba los módulos Pedido y Factura y podrás observar el nuevo comportamiento al añadir líneas de detalle.

Propiedades calculadas dependientes de una colección

También queremos añadir importes a Pedido y Factura. Tener IVA, importe base e importe total es indispensable. Para hacerlo solo necesitas añadir unas pocas propiedades calculadas. La siguiente figura muestra la interfaz de usuario para estas propiedades:
business-logic_es030.png
Empecemos con importeBase. El siguiente código muestra su implementación:
public BigDecimal getImporteBase() {
    BigDecimal resultado = new BigDecimal("0.00");
    for (Detalle detalle : getDetalles()) { // Iteramos por todas las líneas de detalle
        resultado = resultado.add(detalle.getImporte()); // Acumulamos el importe
    }
    return resultado;
}
La implementación es simple, se trata de sumar los importes de todas las líneas.
La siguiente propiedad a añadir es porcentajeIVA que se usará para calcular el IVA. El siguiente código muestra su implementación:
@Digits(integer=2, fraction=0) // Para indicar su tamaño
@Required
private BigDecimal porcentajeIVA;
 
public BigDecimal getPorcentajeIVA() {
    return porcentajeIVA == null ? BigDecimal.ZERO : porcentajeIVA; // Así nunca es nulo
}
 
public void setPorcentajeIVA(BigDecimal porcentajeIVA) {
    this.porcentajeIVA = porcentajeIVA;
}
Puedes ver como porcentajeIVA es una propiedad persistente convencional. En este caso usamos @Digits (una anotación del entorno de validación Hibernate Validator) como una alternativa a @Column para especificar el tamaño.
Continuaremos añadiendo la propiedad IVA. La puedes ver en el siguiente código:
public BigDecimal getIva() {
    return getImporteBase() // importeBase * porcentajeIVA / 100
        .multiply(getPorcentajeIVA())
        .divide(new BigDecimal(100));
}
Es un cálculo simple.
Solo nos queda importeTotal por añadir. Puedes ver su código:
public BigDecimal getImporteTotal() {
    return getImporteBase().add(getIva()); //importeBase + iva
}
Una vez más un cálculo simple.
Ahora que ya has escrito las propiedades para los importes de DocumentoComercial, tienes que modificar la vista por defecto para mostrar porcentajeIVA y la lista de propiedades de la colección detalles para mostrar las propiedades de total de DocumentoComercial. Veamos una primera aproximación:
@Entity
@View(members=
    "anyo, numero, fecha, porcentajeIVA;" + // Añadido porcentajeIVA
    "datos {" +
        "cliente;" +
        "detalles;" +
        "observaciones" +
    "}"
)
abstract public class DocumentoComercial extends Identificable {
 
    @ElementCollection
    @ListProperties(
        "producto.numero, producto.descripcion, cantidad, precioPorUnidad, " +
        "importe[factura.importeBase, factura.iva, factura.importeTotal]" // Entidad padre Factura => [factura.importeBase, ...]
    )
    private Collection<Detalle> detalles;
 
    // El resto del código fuente
    ...
}
Prueba el módulo Factura y observarás las nuevas propiedades calculadas, pero, si pruebas el módulo Pedido estas propiedades no se mostrarán y obtendrás una fea excepción en el log de Eclipse. Esto es así porque las propiedades de total para Pedido no han sido definidas, solo las hemos definido para Factura. Observa como definir propiedades de total para Pedido y Factura en el siguiente código:
//  @ElementCollection
//  @ListProperties("producto.numero, producto.descripcion, cantidad, precioPorUnidad, " +
//     "importe[factura.importeBase, factura.iva, factura.importeTotal]" // Entidad padre Factura => [factura.importeBase, ...]
//  )
//  private Collection<Detalle> detalles; // La colección 'detalles' eliminada
//
//  public Collection<Detalle> getDetalles() { // Getter eliminado
//      return detalles;
//  }
//
//  public void setDetalles(Collection<Detalle> detalles) { // Setter eliminado
//      this.detalles = detalles;
//  }
 
abstract public Collection<Detalle> getDetalles(); // Añadido método abstracto
Primero eliminamos detalles de DocumentoComercial y declaramos un método abstracto que nos permitirá obtener los detalles de las subclases de DocumentoComercial.
Ahora veamos el código de Factura y Pedido:
public class Factura extends DocumentoComercial {
 
    @ElementCollection
    @ListProperties("producto.numero, producto.descripcion, cantidad, precioPorUnidad, " +
        "importe[factura.importeBase, factura.iva, factura.importeTotal]"
    )
    private Collection<Detalle> detalles;
 
    public Collection<Detalle> getDetalles() { // Este método implementa el método abstracto de 'DocumentoComecial'
        return detalles;
    }
 
    public void setDetalles(Collection<Detalle> detalles) {
        this.detalles = detalles;
    }
 
    // El resto del código fuente
    ...
}
public class Pedido extends DocumentoComercial {
 
    @ElementCollection
    @ListProperties("producto.numero, producto.descripcion, cantidad, precioPorUnidad, " +
        "importe[pedido.importeBase, pedido.iva, pedido.importeTotal]" // Entidad padre Pedido => [pedido.importeBase, ...]
    )
    private Collection<Detalle> detalles;
 
    public Collection<Detalle> getDetalles() { // Este método implementa el método abstracto de 'DocumentoComecial'
        return detalles;
    }
 
    public void setDetalles(Collection<Detalle> detalles) {
        this.detalles = detalles;
    }
 
    // El resto del código fuente
    ...
}
El código fuente añadido a Factura y Pedido generará dos tablas FACTURA_DETALLES y PEDIDO_DETALLES respectivamente.
Nota:
Elimina la tabla DOCUMENTOCOMERCIAL_DETALLES, aprendiste a hacerlo en la lección anterior.
Ahora puedes probar tu aplicación. Debería funcionar casi como en la figura del inicio de esta sección. “Casi” porque porcentajeIVA todavía no tiene un valor por defecto. Lo añadiremos en la siguiente sección.

Valor por defecto desde un archivo de propiedades

Es conveniente para el usuario tener el campo porcentajeIVA lleno por defecto con un valor adecuado. Puedes usar un calculador (@DefaultValueCalculator) que devuelva un valor fijo, en este caso cambiar el valor por defecto implica cambiar el código fuente. O puedes leer el valor por defecto de una base de datos (usando JPA desde tu calculador), en este caso cambiar el valor por defecto implica actualizar la base de datos.
Otra opción es tener estos valores de configuración en un archivo de propiedades, un archivo plano con pares clave=valor. En este caso cambiar el valor por defecto de porcentajeIVA es tan simple como editar un archivo plano con un editor de texto.
Implementemos la opción del archivo de propiedades. Crea un archivo llamado facturacion.properties en la carpeta Facturacion/properties con el siguiente contenido:
porcentajeIVADefecto=18
Aunque puedes usar la clase java.util.Properties de Java para leer este archivo preferimos usar una clase propia para leer estas propiedades. Vamos a llamar a esta clase PreferenciasFacturacion y la pondremos en un nuevo paquete llamado org.openxava.facturacion.util. Veamos el código:
package org.openxava.facturacion.util; // En el paquete 'util'
 
import java.io.*;
import java.math.*;
import java.util.*;
import org.apache.commons.logging.*;
import org.openxava.util.*;
 
public class PreferenciasFacturacion {
 
    private final static String ARCHIVO_PROPIEDADES="facturacion.properties";
    private static Log log = LogFactory.getLog(PreferenciasFacturacion.class);
 
    private static Properties propiedades; // Almacenamos las propiedades aquí
 
    private static Properties getPropiedades() {
        if (propiedades == null) { // Usamos inicialización vaga
            PropertiesReader reader = // PropertiesReader es una clase de OpenXava
                new PropertiesReader(
                    PreferenciasFacturacion.class, ARCHIVO_PROPIEDADES);
            try {
                propiedades = reader.get();
            }
            catch (IOException ex) {
                log.error(
                    XavaResources.getString( // Para leer un mensaje i18n
                        "properties_file_error",
                        ARCHIVO_PROPIEDADES),
                    ex);
                  propiedades = new Properties();
             }
        }
        return propiedades;
    }
 
    public static BigDecimal getPorcentajeIVADefecto() { // El único método público
        return new BigDecimal(getPropiedades().getProperty("porcentajeIVADefecto"));
    }
}
Como puedes ver PreferenciasFacturacion es una clase con un método estático, getPorcentajeIVADefecto(). La ventaja de usar esta clase en lugar de leer directamente del archivo de propiedades es que si cambias la forma en que se obtienen las preferencias, por ejemplo leyendo de una base de datos o de un directorio LDAP, solo has de cambiar esta clase en toda tu aplicación.
Puedes usar esta clase desde el calculador por defecto para la propiedad porcentajeIVA. Aquí tienes el código del calculador:
package org.openxava.facturacion.calculadores; // En el paquete 'calculadores'
 
import org.openxava.calculators.*; // Para usar 'ICalculator'
import org.openxava.facturacion.util.*; // Para usar 'PreferenciasFacturacion'
 
public class CalculadorPorcentajeIVA implements ICalculator {
 
    @Override
    public Object calculate() throws Exception {
        return PreferenciasFacturacion.getPorcentajeIVADefecto();
    }
}
 
Como ves, simplemente devuelve porcentajeIVADefecto de PreferenciasFacturacion. Ahora, ya puedes usar este calculador en la definición de la propiedad porcentajeIVA en DocumentoComercial. Mira el código:
@DefaultValueCalculator(CalculadorPorcentajeIVA.class)
private BigDecimal porcentajeIVA;
Con este código cuando el usuario pulsa para crear una nueva factura, el campo porcentajeIVA se rellenará con 18, o cualquier otro valor que hayas puesto en facturacion.properties.

Métodos de retollamadas JPA

Otra forma práctica de añadir lógica de negocio a tu modelo es mediante los métodos de retrollamada JPA. Un método de retrollamada se llama en un momento específico del ciclo de vida de la entidad como objeto persistente. Es decir, puedes especificar cierta lógica a ejecutar al grabar, leer, borrar o modificar una entidad.
En esta sección veremos algunas aplicaciones prácticas de los métodos de retrollamada JPA.

Cálculo de valor por defecto multiusuario

Hasta ahora estamos calculando el número para Factura y Pedido usando @DefaultValueCalculator. Éste calcula el valor por defecto en el momento que el usuario pulsa para crear una nueva Factura o Pedido. Por tanto, si varios usuarios pulsan en el botón “nuevo” al mismo tiempo todos ellos obtendrán el mismo número. Esto no es apto para aplicaciones multiusuario. La forma correcta de generar un número único es generándolo justo en el momento de grabar.
Vamos a implementar la generación del número usando métodos de retrollamada JPA. JPA permite marcar cualquier método de tu clase para ser ejecutado en cualquier momento de su ciclo de vida. Indicaremos que justo antes de grabar un DocumentoComercial calcule su número. De paso mejoraremos el cálculo para tener una numeración diferente para Pedido y Factura.
Edita la entidad DocumentoComercial y añade el método calcularNumero(). Veamos el código:
@PrePersist // // Ejecutado justo antes de grabar el objeto por primera vez
public void calcularNumero() {
    Query query = XPersistence.getManager().createQuery(
        "select max(f.numero) from " +
        getClass().getSimpleName() + // De esta forma es válido para Factura y Pedido
        " f where f.anyo = :anyo");
    query.setParameter("anyo", anyo);
    Integer ultimoNumero = (Integer) query.getSingleResult();
    this.numero = ultimoNumero == null ? 1 : ultimoNumero + 1;
}
El código anterior es el mismo que el de CalculadorSiguienteNumeroParaAnyo pero usando getClass().getSimpleName() en lugar de DocumentoComercial. El método getSimpleName() devuelve el nombre de la clase sin paquete, es decir, precisamente el nombre de la entidad. Será "Pedido" para Pedido y "Factura"" para Factura. Así podremos obtener una numeración diferente para Factura y Pedido.
La especificación JPA establece que no puedes usar el API JPA dentro de un método de retrollamada. Por tanto, el método de arriba no es legal desde un punto de vista estricto. Pero, Hibernate (la implementación de JPA que OpenXava usa por defecto) te permite usarla en @PrePersist. Y dado que usar JPA es la forma más fácil de hacer este cálculo, nosotros lo usamos.
Ahora borra la clase CalculadorSiguienteNumeroParaAnyo de tu proyecto, y modifica la propiedad numero de DocumentoComercial para que no la use. Mira el siguiente código:
@Column(length = 6)
//  @DefaultValueCalculator(value=CalculadorSiguienteNumeroParaAnyo.class, // Quita esto
//      properties=@PropertyValue(name="anyo")
//  )
@ReadOnly // El usuario no puede modificar el valor
private int numero;
Nota que, además de quitar @DefaultValueCalculator, hemos añadido la anotación @ReadOnly. Esto significa que el usuario no puede introducir ni modificar este número. Esta es la forma correcta de hacerlo ahora dado que el número es generado al grabar el objeto, por lo que el valor que tecleara el usuario sería sobrescrito siempre.
Prueba ahora el módulo de Factura o Pedido, verás como el número está vacío y no es editable, y cuando grabes el documento, el número se calcula y se actualiza en la interfaz de usuario.

Sincronizar propiedades persistentes y calculadas

La forma en que calculamos el IVA, el importe base y el importe total es natural y práctica. Usamos propiedades calculadas que calculan, usando Java puro, los valores cada vez que son llamadas.
Pero, las propiedades calculadas tienen algunos inconvenientes. Por ejemplo, si quieres hacer un proceso masivo o un informe de todas las facturas cuyo importe total esté entre ciertos rangos. En estos casos, si tienes una base de datos demasiado grande el proceso puede ser lentísimo, porque has de instanciar todas las facturas para calcular su importe total. Una solución para este problema es tener una propiedad persistente, por tanto una columna en la base de datos para el importe de la factura o pedido; así el rendimiento es bastante mayor.
En nuestro caso mantendremos nuestra actuales propiedades calculadas, pero vamos a añadir una nueva, llamada importe, que contendrá el mismo valor que importeTotal, pero importe será persistente con su correspondiente columna en la base de datos. Lo complicado aquí es mantener sincronizado el valor de la propiedad importe. Vamos a usar métodos de retrollamada JPA (y un truco más) en DocumentoComercial para conseguirlo.
El primer paso es añadir la propiedad importe a DocumentoComercial. Nada más fácil, puedes verlo en el siguiente código:
@Stereotype("DINERO")
private BigDecimal importe;
 
public BigDecimal getImporte() {
    return importe;
}
 
public void setImporte(BigDecimal importe) {
    this.importe = importe;
}
Cuando el usuario añade, modifica o elimina un detalle en la interface de usuario, el iva, importe base e importe total son recalculados con datos frescos instantáneamente, no obstante, para persistir estos cambios el usuario debe Guardar el DocumentoComercial. Para sincronizar importe con importeTotal la primera vez que registramos un documento comercial, nosotros ya sabemos que debemos usar @PrePersist, pero, resulta que JPA no permite marcar más de un método con la misma anotación, por lo tanto, vamos a reordenar nuestro código. Veamos:
// @PrePersist // Elimina esta anotación
public void calcularNumero() {
    Query query = XPersistence.getManager().createQuery(
        "select max(f.numero) from " +
        getClass().getSimpleName() + // De esta forma es válido para Factura y Pedido
        " f where f.anyo = :anyo");
    query.setParameter("anyo", anyo);
    Integer ultimoNumero = (Integer) query.getSingleResult();
    this.numero = ultimoNumero == null ? 1 : ultimoNumero + 1;
}
 
@PrePersist // Ejecutado justo antes de grabar el objeto por primera vez
private void preGrabar() throws Exception {
    calcularNumero();
    recalcularImporte();
}
 
public void recalcularImporte() {
    setImporte(getImporteTotal());
}
Básicamente, llamamos a recalcularImporte() cada vez que una entidad DocumentoComercial es registrada por primera vez en la base de datos. Pero, recalcularImporte() también debe ser ejecutado en la actualización de detalles. Una primera aproximación puede ser marcar recalcularImporte con @PreUpdate, pero este sería ejecutado solo cuando cambian las propiedades de DocumentoComercial, nunca cuando cambian detalles. Nosotros superaremos esto, ejecutando recalcularImporte() siempre que el usuario grabe un DocumentoComercial. Veamos el siguiente código:
@Version
private Integer version; // Añadida propiedad 'version', sin getter, ni setter
 
@PreUpdate // Añadido '@PreUPdate'
public void recalcularImporte() { // Ejecutado justo antes de actualizar el objeto
    setImporte(getImporteTotal());
}
La propiedad version asegura que la retrollamada @PreUpdate sea ejecutada siempre que el usuario Salve un DocumentoComercial, porque esta propiedad siempre será actualizada al guardar.
Puedes probar los módulos Factura y Pedido con este código y verás que cuando una línea de detalle se añade, remueve o modifica, la columna importe en la base de datos es correctamente actualizada después de grabar, lista para ser usada en un proceso masivo.
Nota:
Elimina la tabla DOCUMENTOCOMERCIAL para que se vuelva a generar incluyendo la columna "version".

Lógica desde la base de datos (@Formula)

Idealmente escribirás toda tu lógica de negocio en Java, dentro de tus entidades. Sin embargo, hay ocasiones que esto no es lo más conveniente. Imagina que tienes una propiedad calculada en DocumentoComercial, digamos beneficioEstimado, como la siguiente:
@Stereotype("DINERO")
public BigDecimal getBeneficioEstimado() {
    return getImporte().multiply(new BigDecimal("0.10"));
}
Si necesitas realizar un proceso con todas las facturas con un beneficioEstimado mayor de 1000, has de escribir algo parecido al siguiente código:
Query query = getManager().createQuery("from Factura"); // Sin condición en la consulta
for (Object o : query.getResultList()) { // Itera por todos los objetos
    Factura f = (Factura) o;
    if (f.getBeneficioestimado() // Pregunta a cada objeto
       .compareTo(new BigDecimal("1000")) > 0) {
        f.doSomething();
    }
}
No puedes usar una condición en la consulta para discriminar por beneficioEstimado, porque beneficioEstimado no está en la base de datos, solo está en el objeto Java, por tanto tienes que instanciar cada objeto para preguntar por su beneficioEstimado A veces esto es una buena opción, pero si tienes una cantidad inmensa de facturas, y solo unas cuantas tienen el beneficioEstimado mayor de 1000, entonces el proceso será muy ineficiente. ¿Qué alternativa tenemos?
Nuestra alternativa es usar la anotación @Formula. @Formula es una extensión de Hibernate al JPA estándar, que te permite mapear tu propiedad contra un estamento SQL. Puedes definir beneficioEstimado con @Formula como muestra el siguiente código:
@org.hibernate.annotations.Formula("IMPORTE * 0.10") // El cálculo usando SQL
@Stereotype("DINERO")
private BigDecimal beneficioEstimado; // Un campo, como con las propiedades persistentes
 
public BigDecimal getBeneficioEstimado() { // Sólo el getter es necesario
    return beneficioEstimado;
}
Esto indica que cuando un DocumentoComercial se lea de la base de datos, el campo beneficioEstimado se rellenará con el cálculo de @Formula, un cálculo que por cierto hace la base de datos. Lo más útil de las propiedades @Formula es que puedes usarlas en las condiciones, por tanto puedes reescribir el anterior proceso como muestra el siguiente código:
Query query = getManager().createQuery(
    "from Factura f where f.beneficioEstimado > :beneficioEstimado"); // Podemos usar una condición
query.setParameter("beneficioEstimado", new BigDecimal(1000));
for (Object o: query.getResultList()) { // Iteramos solo por los objetos seleccionados
    Factura f = (Factura) o;
    f.doSomething();
}
De esta forma pones el peso del cálculo de beneficioEstimado y la selección de los registros en el servidor de base de datos, y no el servidor Java.
Este hecho también tiene efecto en modo lista, porque el usuario no puede filtrar ni ordenar por propiedades calculadas, pero sí por propiedades con @Formula:
business-logic_es040.png
@Formula es una buena opción para mejorar el rendimiento en algunos casos. De todas formas, generalmente es mejor usar propiedades calculadas y escribir así tu lógica en Java. La ventaja de las propiedades calculadas sobre @Formula es que tu código no es dependiente de la base de datos. Además, con las propiedades calculadas puedes reejecutar el cálculo sin tener que leer el objeto de la base de datos, por tanto puedes usar @Depends.

Pruebas JUnit

Antes de ir a la siguiente lección, vamos a escribir el código JUnit para ésta. Recuerda, el código no está terminado si no tiene pruebas JUnit. Puedes escribir las pruebas antes, durante o después del código principal. Pero siempre has de escribirlas.
El código de prueba mostrado aquí no es solo para darte un buen ejemplo, sino también para enseñarte maneras de probar diferentes casos en tu aplicación OpenXava.

Modificar la prueba existente

Crear una nueva prueba para cada nuevo caso parece una buena idea desde un punto de vista estructural, pero en la mayoría de los casos no es práctico, porque de esa forma tu código de prueba crecerá muy rápido, y con el tiempo, ejecutar todas las pruebas supondrá muchísimo tiempo.
El enfoque más pragmático es modificar el código de prueba existente para cubrir todos los nuevos casos que hemos desarrollado. Hagámoslo de esta forma.
En nuestro caso, todo el código de esta lección aplica a DocumentoComercial, por tanto vamos a modificar el método testCrear() de PruebaDocumentoComercial para ajustarlo a la nueva funcionalidad. Dejamos el método testCrear() tal como muestra el siguiente código:
public void testCrear() throws Exception {
    login("admin", "admin");
    calcularNumero(); // Añadido para calcular primero el siguiente número de documento
    verificarValoresDefecto();
    escogerCliente();
    anyadirDetalles();
    ponerOtrasPropiedades();
    grabar();
    verificarImporteYBeneficioEstimado(); // Prueba el método de retrollamada y @Formula
    verificarCreado();
    borrar();
}
Como ves, añadimos una nueva línea, después de login(...), para calcular el siguiente número de documento, y una llamada al nuevo método verificarImporteYBeneficioEstimado().
Ahora nos conviene más calcular el siguiente número de documento al principio para usarlo en el resto de la prueba. Para hacer esto, cambia el viejo método getNumero() por los dos métodos mostrados en el siguiente código:
private void calcularNumero() {
    Query query = getManager().createQuery(
        "select max(f.numero) from " +
        modelo + // Cambiamos DocumentoComercial por una variable
        " f where f.anyo = :anyo");
    query.setParameter("anyo", Dates.getYear(new Date()));
    Integer ultimoNumero = (Integer) query.getSingleResult();
    if (ultimoNumero == null) ultimoNumero = 0;
    numero = Integer.toString(ultimoNumero + 1);
}
 
private String getNumero() {
    return numero;
}
Anteriormente, teníamos solo getNumero() que calculaba y devolvía el número, ahora tenemos un método para calcular (calcularNumero()), y otro para devolver el resultado (getNumero()). Puedes notar que la lógica del cálculo tiene un pequeño cambio, en vez de usar “DocumentoComercial” como fuente de la consulta usamos "modelo", una variable. Esto es así porque ahora la numeración para facturas y pedidos está separada. Llenamos esta variable, un campo de la clase de prueba, en el constructor, tal como muestra el siguiente código:
private String modelo; // Nombre del modelo para la condición. Puede ser 'Factura' o 'Pedido'
 
public PruebaDocumentoComercial(String nombrePrueba, String nombreModulo) {
    super(nombrePrueba, "Facturacion", nombreModulo);
    this.modelo = nombreModulo; // El nombre del módulo coincide con el del modelo
}
En este caso el nombre de módulo, "Factura" o "Pedido", coincide con el nombre de modelo, Factura o Pedido, así que la forma más fácil de obtener el nombre de modelo es desde el nombre de módulo.
Veamos el código que prueba la nueva funcionalidad.

Verificar valores por defecto y propiedades calculadas

En esta lección hemos hecho algunas modificaciones en los valores por defecto. Primero, el valor por defecto para numero ya no se calcula mediante un @DefaultValueCalculator en su lugar usamos un método de retrollamada JPA. Segundo, tenemos una nueva propiedad, porcentajeIVA, cuyo valor inicial se calcula leyendo de un archivo de propiedades. Para probar estos casos hemos de modificar el método verificarValoresDefecto() como ves en el siguiente código:
private void verificarValoresDefecto() throws Exception {
    execute("CRUD.new");
    assertValue("anyo", getAnyoActual());
    // assertValue("numero", getNumero()); // Ahora el número no tiene valor inicial...
    assertValue("numero", ""); // ... al crear un documento nuevo
    assertValue("fecha", getFechaActual());
    assertValue("porcentajeIVA", "18"); // Valor de archivo de propiedades
}
Comprobamos el cálculo del valor por defecto de procentajeIVA y verificamos que numero no tiene valor inicial, porque ahora numero no se calcula hasta el momento de grabar el documento (sección Cálculo de valor por defecto multiusuario). Cuando el documento (factura o pedido) se grabe verificaremos que numero se calcula. Cuando la línea se añade podemos verificar el cálculo de importe de detalle (la propiedad calculada simple, sección Propiedad calculada simple), el valor por defecto para precioPorUnidad (@DefaultValueCalculator, sección Usar @DefaultValueCalculator) y las propiedades de importes del documento (propiedades calculadas que dependen de una colección, sección Propiedades calculadas dependientes de una colección). Probamos todo esto haciendo unas ligeras modificaciones en el ya existente método anyadirDetalles():
private void anyadirDetalles() throws Exception {
    assertCollectionRowCount("detalles", 0);
 
    // Añadir una línea de detalle
    setValueInCollection("detalles", 0, "producto.numero", "1");
    assertValueInCollection("detalles", 0,
        "producto.descripcion", "Peopleware: Productive Projects and Teams");
    assertValueInCollection("detalles", 0,
        "precioPorUnidad", "31,00"); // @DefaultValueCalculator, section 'Usar @DefaultValueCalculator'
    setValueInCollection("detalles", 0, "cantidad", "2");
    assertValueInCollection("detalles", 0,
        "importe", "62,00"); // Propiedada calculada, sección 'Propiedad calculada simple'
 
    // Verificando propiedades calculadas del documento
    assertTotalInCollection("detalles", 0, "importe", "62,00"); // Propiedades calculadas
    assertTotalInCollection("detalles", 1, "importe", "11,16");  // dependientes de una coleccion,
    assertTotalInCollection("detalles", 2,
        "importe", "73,16"); // sección 'Propiedades calculadas dependientes de una colección'
 
    // Añadir otro detalle
    setValueInCollection("detalles", 1, "producto.numero", "2");
    assertValueInCollection("detalles", 1, "producto.descripcion", "Arco iris de lágrimas");
    assertValueInCollection("detalles", 1,
        "precioPorUnidad", "15,00"); // @DefaultValueCalculator, sección 'Usar @DefaultValueCalculator'
    setValueInCollection("detalles", 1, "precioPorUnidad", "10,00"); // Modificando el valor pode defecto
    setValueInCollection("detalles", 1, "cantidad", "1");
    assertValueInCollection("detalles", 1, "importe", "10,00");
 
    assertCollectionRowCount("detalles", 2);
 
    // Verificando propiedades calculadas del documento
    assertTotalInCollection("detalles", 0, "importe", "72,00");
    assertTotalInCollection("detalles", 1, "importe", "12,96");
    assertTotalInCollection("detalles", 2, "importe", "84,96");
}
Como ves, con estas modificaciones sencillas probamos la mayoría de nuestro nuevo código. Nos quedan sólo las propiedades importe y beneficioEstimado. Las cuales probaremos en la siguiente sección.

Sincronización entre propiedad persistente y calculada / @Formula

En la sección Sincronizar propiedades persistentes y calculadas usamos métodos de retrollamada de JPA en DocumentoComercial para tener una propiedad persistente, importe, sincronizada con una calculada, importeTotal. La propiedad importe solo se muestra en modo lista.
En la sección Lógica desde la base de datos hemos creado una propiedad que usa @Formula, beneficioEstimado. Esta propiedad se muestra solo en modo lista.
Obviamente, la forma más simple de probarlo es yendo a modo lista y verificando que los valores para estas dos propiedades son los esperados. En testCrear() llamamos a verificarImporteYBeneficioEstimado(). Veamos su código:
private void verificarImporteYBeneficioEstimado() throws Exception {
    execute("Mode.list"); // Cambiar a modo lista
    setConditionValues(new String [] { // Filtra para ver en la lista solamente
        getAnyoActual(), getNumero() // el documento que acabamos de crear
    });
    execute("List.filter"); // Hace filtro
    assertValueInList(0, 0, getAnyoActual()); // Verifica que
    assertValueInList(0, 1, getNumero()); // el filtro ha funcionado
    assertValueInList(0, "importe", "84,96"); // Confirma el importe
    assertValueInList(0, "beneficioEstimado", "8,50"); // Confirma el beneficio estimado
    execute("Mode.detailAndFirst"); // Va a modo detalle
}
Dado que ahora vamos a modo lista y después volvemos a detalles, hemos de hacer una pequeña modificación en el método verificarCreado(), que es ejecutado justo después de verificarImporteYBeneficioEstimado(). Veamos la modificación:
private void verificarCreado() throws Exception {
    // setValue("anyo", getAnyoActual()); // Borramos estas líneas
    // setValue("numero", getNumero());  // para buscar el documento
    // execute("Search.search"); // porque ya lo hemos buscado desde el modo lista
    // El resto de la prueba ...
    ...
Quitamos estas líneas porque ahora no es necesario buscar el documento recién creado. Ahora en el método verificarImporteYBeneficioEstimado() vamos a modo lista y escogemos el documento, por tanto ya estamos editando el documento.
¡Enhorabuena! Ahora tus pruebas ya están sincronizadas con tu código. Es un buen momento para ejecutar todas las pruebas de tu aplicación.

Resumen

En esta lección has aprendido algunas formas comunes de añadir lógica de negocio a tus entidades. No hay duda sobre la utilidad de las propiedades calculadas, los métodos de retrollamada o @Formula. Sin embargo, todavía tenemos muchas otras formas de añadir lógica a tu aplicación OpenXava, que vamos a aprender a usar.
En futuros lecciones verás como añadir validación, modificar el funcionamiento estándar del módulo y añadir tu propia lógica de negocio, entre otras formas de añadir lógica personalizada a tu aplicación.

Descargar código fuente de esta lección

¿Problemas con la lección? Pregunta en el foro ¿Ha ido bien? Ve a la lección 6