openxava / documentación / Lección 12: @Calculation y totales de colección

Curso: 1. Primeros pasos | 2. Modelo básico del dominio (1) | 3. Modelo básico del dominio (2) | 4. Refinar la interfaz de usuario | 5. Desarrollo ágil | 6. Herencia de superclases mapeadas | 7. Herencia de entidades | 8. Herencia de vistas | 9. Propiedades Java |10. Propiedades calculadas | 11. @DefaultValueCalculator en colecciones | 12. @Calculation y totales de colección |13. @DefaultValueCalculator desde archivo |14. Evolución del esquema manual |15. Cálculo de valor por defecto multiusuario |16. Sincronizar propiedades persistentes y calculadas |17. Lógica desde la base de datos |18. Validación avanzada | 19. Refinar el comportamiento predefinido | 20. Comportamiento y lógica de negocio | 21. Referencias y colecciones | A. Arquitectura y filosofía | B. Java Persistence API | C. Anotaciones | D. Pruebas automáticas

Tabla de contenidos

Lección 12: @Calculation y totales de colección
Propiedades persistentes con @Calculation
Propiedades de total de una colección
Resumen
Hemos visto como calcular los importes utilizando la anotación @DefaultValueCalculator en las colecciones. Veremos ahora como implementar totales usando la anotación @Calculation.
Si no te gustan los videos sigue las instrucciones a continuación.

Propiedades persistentes con @Calculation

A veces las propiedades calculadas no son la mejor opción. Imagínate que tienes una propiedad calculada en Factura, digamos descuento:
// NO LO AÑADAS A TU CÓDIGO, ES SÓLO PARA ILUSTRAR
public BigDecimal getDescuento() {
    return getImporte().multiply(new BigDecimal("0.10"));
}
Si necesitas procesar todas las facturas cuyo descuento sea mayor de 1000, has de escribir un código como el siguiente:
// NO LO AÑADAS A TU CÓDIGO, ES SÓLO PARA ILUSTRAR
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.getDescuento() // Pregunta a cada objeto
        .compareTo(new BigDecimal("1000")) > 0) {
            f.hacerAlgo();
    }
}
No puedes usar una condición en la consulta para discriminar por descuento, porque descuento no está en la base de datos, está sólo en el objeto Java, por lo que has de instanciar todos y cada uno de los objetos para poder preguntar por el descuento. En algunos casos esta forma es una buena opción, pero si tienes una cantidad inmensa de facturas y sólo unas pocas tiene el descuento mayor de 1000, entonce tu proceso va a ser muy ineficiente. ¿Qué alternativas tenemos?
Nuestra alternativa es usar la anotación @Calculation. @Calculation es una anotación OpenXava que permite asociar un cálculo simple a una propiedad persistente. Puedes definir descuento con @Calculation como se muestra en el siguiente código:
// NO LO AÑADAS A TU CÓDIGO, ES SÓLO PARA ILUSTRAR
@ReadOnly
@Calculation("importe * 0.10")
BigDecimal descuento;
Esto es una propiedad persistente convencional, es decir con una columna correspondiente en la base de datos, pero tiene un cálculo definido con @Calculation. En este caso el cálculo es importe * 0.10, de tal manera que cuando el usuario cambia importe en la interfaz de usuario descuento se recalcula instantaneamente. El valor recalculado se graba en la base de datos cuando el usuario pulsa en Grabar, como con cualquier otra propiedad persistente. También hemos anotado descuento con @ReadOnly, por lo que parece y se comporta como una propiedad calculada, aunque puedes omitir @ReadOnly y así el usuario podría modificar el valor calculado.
Lo más útil de las propiedades @Calculation es que se pueden usar en las condiciones, por lo que puedes reescribir el proceso de arriba como se muestra en el siguiente código:
// NO LO AÑADAS A TU CÓDIGO, ES SÓLO PARA ILUSTRAR
Query query = getManager().createQuery("from Factura f where f.descuento > :descuento"); // Condición permitida
query.setParameter("descuento", new BigDecimal(1000));
for (Object o: query.getResultList()) { // Itera sólo por los objectos seleccionados
    Factura f = (Factura) o;
    f.hacerAlgo();
}
De esta manera ponemos el peso de seleccionar los registros en el servidor de la base de datos y no en el servidor Java. Además, los descuentos no se recalculan cada vez, sino que ya está calculados y grabados.
Este hecho tiene también efecto en el modo lista, porque el usuario no puede filtrar ni ordenar por las propiedades calculadas, pero sí que lo puede hacer usando propiedades persistentes con @Calculation:
business-logic_es025.png
@Calculation es una buena opción cuando necesitas filtrar y ordenar, y un cálculo simple es suficiente. Una desventaja de las propiedades con @Calculation es que sus valores se recalculan sólo cuando el usuario interactúa con el registro y cambia algún valor de las propiedades usadas en el cálculo, por lo tanto cuando añades una nueva propiedad @Calculation a una entidad con datos existente has de actualizar los valores de la nueva columna en la tabla usando SQL. Por otra parte si necesitas un cálculo complejo, con bucles o consultando otras entidades, todavía sigues necesitando una propiedad calculada con tu lógica Java en el getter. En este último caso si además necesitas ordenar y filtrar en modo lista por la propiedad calculada una opción es tener ambas, la calculada y la persistente, y sincronizar sus valores usando los métodos de retrollamada de JPA (hablaremos sobre los métodos de retrollamada en próximas lecciones).

Propiedades de total 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 sólo necesitas añadir unas pocas propiedades a la clase DocumentoComercial. La siguiente figura muestra la interfaz de usuario para estas propiedades:
business-logic_es030.png
Añade el siguiente código a la entidad DocumentoComercial:
@Digits(integer=2, fraction=0) // Para indicar su tamaño
BigDecimal porcentajeIVA;
   
@ReadOnly
@Stereotype("DINERO")
@Calculation("sum(detalles.importe) * porcentajeIVA / 100")
BigDecimal iva;

@ReadOnly
@Stereotype("DINERO")
@Calculation("sum(detalles.importe) + iva")    
BigDecimal importeTotal;    
Fíjate como hemos escogido propiedades persistentes con @Calculation + @ReadOnly en lugar de propiedades calculadas para iva e importeTotal, porque los cálculos son simples, y filtrar y ordenar por ellos es muy útil. También, puedes ver como en @Calculation puedes usar sum(detalles.importe) para referirte a la suma de columna importe de la colección detalles, de esta manera podemos prescindir de una propiedad importeBase. Por otra parte, porcentajeIVA es un propiedad persistente convencional. En este caso usamos @Digits (una anotación de Bean Validation, el estándar de validación de Java) como una alternativa a @Column para especificar su tamaño.
Ahora que ya has escrito las propiedades para los importes de DocumentoComercial, tienes que modificar la lista de propiedades de la colección detalles para mostrar las propiedades de total de DocumentoComercial. Veámoslo:
abstract public class DocumentoComercial extends Identificable {
 
    @ElementCollection
    @ListProperties(
        "producto.numero, producto.descripcion, cantidad, precioPorUnidad, " +
        "importe+[" + 
        	"documentoComercial.porcentajeIVA," +
        	"documentoComercial.iva," +
        	"documentoComercial.importeTotal" +
        "]" 
    )	
    private Collection<Detalle> detalles;
 
    ...
}
Las propiedades de total son propiedades normales de la entidad (DocumentoComercial en este caso) que en la interfaz de usuario se localizan debajo de una columna de una colección. Para eso, en @ListProperties se usan corchetes después de la propiedad para enumerarlas, algo así como importe[documentoComercial.importeTotal]. Además, si simplemente quieres la suma de la columna no necesitas una propiedad para ello, con un + después de la propiedad en @ListProperties es suficiente, como importe+. En nuestro caso combinamos ambas cosas, + y propiedades de total entre [ ].
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.

Resumen

En esta lección has aprendido cómo definir propiedades persistentes con cálculos específicos utilizando la anotación @Calculation y vimos la utilidad de la anotación @Digits para definir tipo y longitud de campos. También definimos propiedades de total utilizando propiedades persistentes con las anotaciones @ReadOnly +@Calculation, y vimos como utilizar sum para prescindir de una propiedad específica.

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