Curso Jakarta EE 9 (37). JPA con Hibernate (20): Criteria API (2). Funciones y WHERE. Modificaciones.

logo Jakarta EE

Completamos el estudio de los fundamentos de Criteria API con la descripción de las funciones y del bloque WHERE, incluyendo los operadores existentes y la posibilidad de efectuar subconsultas. También habrá una pequeña sección dedicada a las actualizaciones y eliminaciones.

>>>> ÍNDICE <<<<

Funciones

El uso de las funciones de JPQL con la API es más cómodo porque vemos en todo momento sus argumentos, sus tipos y el resultado, un Expression tipado. En esta tabla se muestran las que vimos en el capítulo 35. Todas están disponibles en CriteriaBuilder salvo que se indique otra cosa.

JPQLUtilidadAPI
ABSValor absoluto.abs
CONCATUne todas las cadenas.concat
CURRENT_DATEFecha actual.currentDate
CURRENT_TIMETiempo actual.currentTime
CURRENT_TIMESTAMPFecha y hora actualcurrentTimestamp
INDEXBusca la posición de una entidad en la colección de una relación múltiple.ListJoin#index
LENGTHCuenta los caracteres de una cadenalength
LOCATEEncuentra la posición dentro de una cadena en la que comienza otralocate
LOWER\UPPERConversión a minúsculas\mayúsculaslower upper
MODEl resto de la división de dos enteros.mod
SIZEEl número de elementos de una colección.size
SQRTDouble con la raíz cuadrada.sqrt
SUBSTRINGExtrae de una cadena el fragmento situado entre una posición de inicio (obligatoria, empieza en 1) y otra final (opcional).substring
TRIMElimina las apariciones de un caracter al principio y al final de la cadena (configurable).trim
TYPEDevuelve el nombre de la entidad (vimos que era muy útil a la hora de trabajar con jerarquías).Path#type

He aquí un ejemplo con la función que une cadenas.

SELECT CONCAT (e.concept, ' : ', e.amount)
        cq.select(
                cb.concat(
                        cb.concat(
                                expenseRoot.get(Expense_.concept),
                                " : "),
                        expenseRoot.get(Expense_.amount).as(String.class)
                )
        )

Debido a que concat recibe dos argumentos, hay que aplicarla dos veces para unir tres cadenas.

Por ser el resultado de las funciones de tipo Expression, podemos emplearlas dentro de condiciones de selección en las cláusulas WHERE y HAVING, tal y como veremos en algunos de los ejemplos del capítulo.

Funciones de agregación

Las clásicas e imprescindibles funciones de agregación también están en CriteriaBuilder. Por la naturaleza de la API programática, algunas presentan varias versiones. Son el conteo (count, countDistinct), la media aritmética (avg), la suma (sum, sumAsLong, sumAsDouble), el mínimo (min, least) y el máximo (max, greatest).

Escribamos el conteo que implementa el DAO genérico para todos los DAO.

@Override
public Long count() {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Long> cq = cb.createQuery(Long.class);
    Root<T> root = cq.from(entityClass);

    cq.select(cb.count(root));
    return em.createQuery(cq)
            .getSingleResult();
}

Veamos un ejemplo más complicado. De nuevo, recordemos el capítulo 35 en el que vimos que lo más interesante de las funciones de agregación es la posibilidad de aplicar el cálculo de forma independiente a distintos grupos del conjunto total de resultados gracias a la cláusula GROUP BY. Este es el listado de categorías que tienen gastos, agrupadas por su nombre, con el conteo y la suma de gastos para cada una. O dicho de otro modo, un resumen estadístico sencillo para cada grupo de gastos de una categoría. Para hacer el ejemplo más completo, descartaremos los grupos (no los gastos) cuyo gasto medio sea superior a cierta cantidad. Esta selección del grupo se declara con la cláusula HAVING.

SELECT  c.category.id, c.category.name, COUNT(e), SUM(e.amount) 
FROM Expense e
GROUP BY  e.category.name HAVING AVG(e.amount) <= :maxAverage
ORDER BY e.category.name

Así pues, necesitamos una proyección múltiple en la que aparecerán funciones de agregación. Obsérvese que no he declarado la reunión interna de los gastos con sus categorías porque podemos navegar por la relación. Lo destacable viene a continuación: para las cláusulas GROUP BY y HAVING, llamaremos a los métodos de CriteriaQuery que las representan con un Expression. En el caso del primero, el Path con la propiedad por la que realizar la agrupación (podemos usar un listado de Path cuando sea necesario). En el segundo, una expresión de comparación que incluye la media aritmética calculada por AVG.

    @Override
    public List<CategorySummaryDTO> getSummaryAverageAbove(BigDecimal maxAverage) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<CategorySummaryDTO> cq = cb.createQuery(CategorySummaryDTO.class);
        Root<Expense> expenseRoot = cq.from(Expense.class);

        Path<Category> categoryPath = expenseRoot.get(Expense_.category);
        Path<String> categoryNamePath = categoryPath.get(Category_.name);

        cq.multiselect(
                categoryPath.get(Category_.id),
                categoryNamePath,
                cb.sum(expenseRoot.get(Expense_.amount)),
                cb.count(expenseRoot))
          .groupBy(categoryNamePath)
          .having(cb.greaterThanOrEqualTo(
                  cb.avg(expenseRoot.get(Expense_.amount)).as(BigDecimal.class),
                  maxAverage)))
          .orderBy(cb.asc(categoryNamePath));

        return em.createQuery(cq)
                 .getResultList();
    }

Un detalle digno de mención: en la línea 17 se ha convertido el resultado de avg a BigDecimal para poder compararlo con el parámetro. Si no lo hacemos, el tipado de la consulta nos dará un error en tiempo de compilación porque se espera un Double. Una vez más, la combinación de la API con el metamodelo estático nos ayuda a prevenir errores.

Otras funciones

El juego de funciones disponibles es demasiado pequeño. En JPQL mitigamos el problema con el uso de FUNCTION que nos permite utilizar las del SQL propio de la base de datos. De este modo, no tenemos que escribir la consulta en SQL si el único motivo para hacerlo es usar alguna función nativa. Recordemos que FUNCTION requiere que la función a ejecutar debe estar registrada en Hibernate.

Este es un ejemplo ya visto en el curso. WEEKOFYEAR es una función de MySQL ya registrada en Hibernate que devuelve el número de la semana de una fecha.

SELECT e.concept, e.date, FUNCTION('WEEKOFYEAR', e.date) 
FROM Expense e 
ORDER BY e.date DESC

En la API, contamos con CriteriaBuilder#function. Recibe el nombre de la función, el tipo devuelto en formato Class y sus argumentos, si los hubiera, en forma de Expression.

 @Override
 public List<ExpenseWeekDTO> getSummaryWithWeek() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<ExpenseWeekDTO> cq = cb.createQuery(ExpenseWeekDTO.class);
        Root<Expense> expenseRoot = cq.from(Expense.class);

        cq.multiselect(
                expenseRoot.get(Expense_.concept),
                expenseRoot.get(Expense_.amount),
                expenseRoot.get(Expense_.date),
                cb.function("WEEKOFYEAR", Integer.class, expenseRoot.get(Expense_.date)))
          .orderBy(cb.desc(expenseRoot.get(Expense_.date)));

        return em.createQuery(cq)
                 .getResultList();
    }

¿Y las funciones nativas registradas por nosotros? Se usan del mismo modo. En el proyecto de ejemplo tenemos WEEKOFYEAR registrada como WEEK por la clase CustomFunctionsMetadataBuilderContributor. En consecuencia, la siguiente sentencia es correcta.

cb.function("WEEK", Integer.class, expenseRoot.get(Expense_.date))

WHERE

La cláusula se declara suministrando a where un conjunto de expresiones, normalmente predicados (Predicate) que construimos, cómo no, con CriteriaBuilder.

SELECT e
FROM Expense e
WHERE e.amount >= :amount
@Override
public List<Expense> findAllByMax() {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Expense> cq = cb.createQuery(Expense.class);
    Root<Expense> expenseRoot = cq.from(Expense.class);

    cq.select(expenseRoot);
    cq.where(cb.lessThanOrEqualTo(expenseRoot.get(Expense_.amount), new BigDecimal("50.00")));
    cq.orderBy(cb.desc(expenseRoot.get(Expense_.amount)));

    return em.createQuery(cq).getResultList();
}

El código se complica un poco si aparecen parámetros de entrada en la consulta. A diferencia de JPQL, donde es suficiente con utilizar un indicador con un índice («?1») o un nombre «:amount»), tenemos que crear un objeto ParameterExpression tipado que contenga ese indicador. Luego, le asignaremos su valor de la forma habitual.

ParameterExpression<BigDecimal> amountParam =
                cb.parameter(BigDecimal.class, "amount");
cq.where(cb.lessThanOrEqualTo(expenseRoot.get(Expense_.amount), amountParam));

return em.createQuery(cq)
                .setParameter("amount", maxAmount)
                .getResultList();

En el código anterior, puesto que el parámetro no se reutiliza, quizás el lector prefiera declararlo donde se use.

cq.where(cb.lessThanOrEqualTo(expenseRoot.get(Expense_.amount),
                cb.parameter(BigDecimal.class, "amount")));

No obstante, ¿es realmente necesario trabajar con parámetros? Es una pregunta pertinente, pues lo siguiente funciona.

 .where(cb.lessThanOrEqualTo(expenseRoot.get(Expense_.amount),
                  maxAmount))

De hecho, hice algo parecido en el método getSummaryAverageAbove.

cb.greaterThanOrEqualTo(
                  cb.avg(expenseRoot.get(Expense_.amount)).as(BigDecimal.class),
                  maxAverage)

La respuesta es sí, y el motivo ya lo expliqué. Sin parámetros, los valores se «pegan» tal cual en el SQL que Hibernate envía a la base de datos. En el caso de que sean cadenas de texto proporcionadas desde fuera de nuestro sistema, por ejemplo un formulario, esta práctica nos expone a ataques de inyección de SQL a menos que procesemos la cadena para evitarlo. La manera más fácil de prevenir este tipo de problemas de seguridad es recurrir al mecanismo de parametrización de Query.

Operadores

Ya hemos visto en varios ejemplos que en CriteriaBuilder ofrece métodos equivalentes a los operadores de JPQL, por lo que indagaremos en la interfaz para encontrar el que necesitamos. Algunos comparadores disponen de versiones abreviadas para simplificar el código. Por ejemplo, le es equivalente a lessThanOrEqualTo.

cq.where(cb.le(expenseRoot.get(Expense_.amount),
                cb.parameter(BigDecimal.class, "amount")));

La siguiente tabla recoge la correspondencia entre los operadores de JPQL y los métodos de la API. Cuando alguno presenta sobrecargas enlazo al primero que aparece en la documentación javadoc.

JPQLAPI
=equal
<>notEqual
>greaterThan, gt
>=greaterThanOrEqualTo, ge
<lessThan, lt
<=lessThanOrEqualToo, le
+sum
diff, usar neg para cambiar el signo.
*prod
/quot
ANDand
ORor
NOTnot
BETWEENbetween
IS NULLisNull
IS NOT NULLisNotNull
EXISTSexists, usar not(exists()) para negar
IS EMPTYisEmpty
IS NOT EMPTYisNotEmpty
MEMBERisMember
NOT MEMBER OFisNotMember
IS TRUEisTrue
IS FALSEisFalse
LIKElike
NOT LIKEnotLike
INin, usar not(in()) para negar
AND y OR

Lo que requiere de una explicación es cómo podemos «encadenar» expresiones con AND y OR. Veámoslo recuperando un ejemplo del capítulo anterior.

StringBuilder jpql = new StringBuilder("SELECT e FROM Expense e  WHERE 1 = 1 ");
if (dateFrom != null) {
    jpql.append(" AND e.date >= ?1 ");
}
if (dateTo != null) {
    jpql.append(" AND e.date <= ?2 ");
}
jpql.append("ORDER BY e.date DESC, e.amount DESC");

Este fragmento de código crea el WHERE de forma dinámica porque la selección que se puede realizar con los parámetros de entrada solo debe aplicarse cuando estos no sean nulos. Para concatenar expresiones con la API, hay que ir añadiéndolas a un predicado de conjunción (AND) o de disyunción (OR).

Predicate conjunction = cb.conjunction();
if (dateFrom != null) {
    conjunction.getExpressions().add(cb.greaterThanOrEqualTo(expenseRoot.get(Expense_.date),
                                                          cb.parameter(LocalDate.class, "dateFrom")));
}
if (dateTo != null) {
    conjunction.getExpressions().add(cb.lessThanOrEqualTo(expenseRoot.get(Expense_.date),
                                                          cb.parameter(LocalDate.class, "dateTo")));
}
if (!conjunction.getExpressions().isEmpty()) {
   cq.where(conjunction);
}

El método where tiene una sobrecarga que admite una lista de expresiones que serán relacionadas mediante AND. Pero cuidado, porque las múltiples llamadas a where descartan las expresiones que se hubieran establecido con anterioridad. Por tanto, lo más seguro es que solo haya una llamada a where.

Compliquemos un poco el caso de pruebas. Ahora queremos los presupuestos vigentes en algún momento dentro de un rango de fechas y con una cuantía de al menos cierta cantidad. Hay que tener en cuenta que la fecha de fin de un presupuesto es opcional, es decir, el presupuesto puede estar disponible mientras existan fondos. Por simplicidad, no calcularemos si se rebasaron los límites del presupuesto.

SELECT b 
FROM Budget b 
WHERE 
       b.amount >= :maxAmount 
       AND  b.dateFrom <=: maxDate 
       AND (b.dateTo IS NULL OR b.dateTo >= :minDate) 
ORDER BY b.name

Trasladar la consulta a Criteria API parece más difícil de lo que en realidad es; la clave está en ser cuidadosos al componer el WHERE. Vayamos paso a paso.

Primero creamos la disyunción para las dos condiciones sobre la propiedad dateTo.

Predicate disjunction = cb.disjunction();
disjunction.getExpressions().add(cb.isNull(budgetRoot.get(Budget_.dateTo)));
disjunction.getExpressions().add(cb.greaterThanOrEqualTo(budgetRoot.get(Budget_.dateTo),
                                                             cb.parameter(LocalDate.class, "minDate")));

Luego una conjunción con tres elementos: las selecciones sobre las propiedades amount y dateFrom y la disyunción.

Predicate conjunction = cb.conjunction();
conjunction.getExpressions().add(cb.greaterThanOrEqualTo(budgetRoot.get(Budget_.amount),
                                                          cb.parameter(BigDecimal.class, "minAmount")));
conjunction.getExpressions().add(cb.lessThanOrEqualTo(budgetRoot.get(Budget_.dateFrom),
                                                         cb.parameter(LocalDate.class, "maxDate")));
conjunction.getExpressions().add(disjunction);

Es posible crear un predicado de disyunción con el método CriteriaBuilder#or (y lo opuesto con CriteriaBuilder#and). Si además sabemos que where puede recibir varias expresiones que se relacionarán con AND, llegaremos a esta consulta.

cq.select(budgetRoot)
          .where(

                  cb.ge(budgetRoot.get(Budget_.amount),
                          cb.parameter(BigDecimal.class, "minAmount")),

                  cb.lessThanOrEqualTo(budgetRoot.get(Budget_.dateFrom),
                          cb.parameter(LocalDate.class, "maxDate")),

                  cb.or(
                          cb.isNull(budgetRoot.get(Budget_.dateTo)),
                          cb.greaterThanOrEqualTo(budgetRoot.get(Budget_.dateTo),
                                  cb.parameter(LocalDate.class, "minDate"))))

          .orderBy(cb.asc(budgetRoot.get(Budget_.name)));

He separado los argumentos de where para que se comprenda mejor. Con algo de práctica, el lector será capaz de escribir con este estilo sus consultas, suponiendo que lo considere más claro y conciso.

IN

Crearemos expresiones lógicas IN con las sobrecargas del método in del Path de la propiedad a la que se aplican. Estos son los gastos que pertenecen a ciertas categorías definidas por un listado de identificadores.

    @Override
    public List<Expense> findByCategories(List<Long> ids) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Expense> cq = cb.createQuery(Expense.class);
        Root<Expense> expenseRoot = cq.from(Expense.class);

        cq.select(expenseRoot)
          .where(
                  expenseRoot.get(Expense_.category).get(Category_.id)
                             .in(ids))
          .orderBy(
                  cb.desc(expenseRoot.get(Expense_.date)),
                  cb.asc(expenseRoot.get(Expense_.concept)));

        return em.createQuery(cq).getResultList();
    }

Con CriteriaBuilder puede escribirse así.

CriteriaBuilder.In<Long> in = cb.in(expenseRoot.get(Expense_.category).get(Category_.id));
ids.forEach(in::value);

Es poco práctico cuando tenemos una lista de argumentos dado que para cada valor contenido en el IN hay que invocar a value.

Con la versión de in que admite expresiones podemos usar las subconsultas que veremos a continuación.

Subconsultas

Las subconsultas se declaran con la interfaz Subquery, una especialización de AbstractQuery, al igual que CriteriaQuery. Esto hace que a todos los efectos sean consultas «corrientes». Pero también son expresiones (Expression), así que podemos usarlas donde estas se admitan, por ejemplo, como argumento de la función IN que acabamos de ver. En cualquier caso, solo aparecerán dentro del WHERE o en las cláusulas SET de UPDATE. HQL las permite en el SELECT, y SQL incluso en el FROM (el caso típico son las tablas derivadas).

Demostremos su uso con una consulta.

SELECT e 
FROM Expense e 
WHERE e.amount > (SELECT AVG(e.amount)  FROM Expense e ) 
ORDER BY e.amount

Queremos averiguar cuáles son los gastos cuya cuantía esté por encima de la media. Creamos la subconsulta, un objeto Subquery, que calcule el gasto medio que usaremos como condición en la consulta principal para definir la selección de gastos según su cuantía. Lo hacemos a partir del objeto CriteriaQuery de esa consulta principal.

    @Override
    public List<Expense> findAboveAverage() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Expense> cq = cb.createQuery(Expense.class);

        Subquery<Double> sqAvg = cq.subquery(Double.class);
        Root<Expense> sqRoot = sqAvg.from(Expense.class);
        sqAvg.select(cb.avg(sqRoot.get(Expense_.amount)));

        Root<Expense> expenseRoot = cq.from(Expense.class);
        cq.select(expenseRoot);
        cq.where(cb.greaterThanOrEqualTo(expenseRoot.get(Expense_.amount).as(Double.class), sqAvg));
        cq.orderBy(cb.desc(expenseRoot.get(Expense_.amount)));

        return em.createQuery(cq).getResultList();
    }

La subconsulta se puede vincular con las variables de la consulta que la contiene a través de los objetos raíces que las representan. Busquemos las categorías para las que «exista» un presupuesto mayor que cierta cantidad, lo cual permite introducir el operador EXISTS con la API. Si el lector ha seguido el curso en orden, notará que la siguiente consulta no es, ni más ni menos, que un SEMI-JOIN.

SELECT c
FROM Category c 
WHERE EXISTS ( 
    SELECT 1 
    FROM Budget b JOIN b.categories bc 
    WHERE bc.id = c.id AND b.amount >= :minAmount)
ORDER BY c.name

La versión programática.

@Override
 public List<Category> findAllCategoriesByBudgetMinAmount(BigDecimal minAmount) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Category> cq = cb.createQuery(Category.class);
    Root<Category> categoryRoot = cq.from(Category.class);

    Subquery<Integer> sqSub = cq.subquery(Integer.class);
    Root<Budget> sqRoot = sqSub.from(Budget.class);
    ListJoin<Budget, Category> join = sqRoot.join(Budget_.categories);

    sqSub.select(cb.literal(1))
         .where(
                 cb.equal(join.get(Category_.ID), categoryRoot.get(Category_.ID)),
                 cb.ge(sqRoot.get(Budget_.amount), cb.parameter(BigDecimal.class, "minAmount"))
         );

    cq.select(categoryRoot)
      .where(cb.exists(sqSub))
      .orderBy(cb.asc(categoryRoot.get(Category_.name)));

    return em.createQuery(cq)
             .setParameter("minAmount", minAmount)
             .getResultList();
}

El requisito de existencia es un Predicate creado con CriteriaBuilder#exists a partir de la subconsulta. En ella, dado que es suficiente con saber si existe un presupuesto que cumpla con las restricciones y no nos importa cuál sea, la costumbre es indicarlo devolviendo el literal 1. Pero lo interesante está en la condición de su WHERE: usamos la raíz de la categoría (categoryRoot) de la consulta «padre» para asociarla con el presupuesto de la subconsulta mediante un join.

Modificaciones

Para las actualizaciones y borrados masivos de entidades, nos olvidamos de CriteriaQuery. Usaremos objetos de las interfaces CriteriaUpdate y CriteriaDelete, creados, como siempre, con CriteriaBuilder.

Veamos la actualización. Esta es la consulta JPQL que usamos en su momento.

UPDATE Coupon c 
SET c.name = CONCAT(:prefix, c.name) 
WHERE c.expense IS NOT NULL  AND SUBSTRING(c.name, 1, :length) NOT LIKE :prefix

Debemos crear un objeto CriteriaUpdate con una raíz para la entidad que se va a actualizar. Haremos una llamada a un método set por cada atributo a modificar, indicándolo con su nombre, Path o el SingularAttribute del metamodelo. El nuevo valor se proporciona directamente o bien con un Expression, por lo que puede ser una subconsulta. El WHERE se define como ya hemos hecho tantas veces.

    @Override
    public int updateUsedPrefixBulk() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaUpdate<Coupon> cu = cb.createCriteriaUpdate(Coupon.class);
        Root<Coupon> couponRoot = cu.from(Coupon.class);

        cu.set(Coupon_.name, 
                      cb.concat(Coupon.USED_PREFIX, couponRoot.get(Coupon_.name)))
          .where(cb.and(
                  cb.isNotNull(couponRoot.get(Coupon_.expense)),
                  cb.notLike(
                          couponRoot.get(Coupon_.name),
                          cb.substring(couponRoot.get(Coupon_.name), 1, Coupon.USED_PREFIX.length()))));

        return em.createQuery(cu).executeUpdate();
    }

El borrado es análogo pero más simple porque no tenemos la cláusula SET. Así eliminamos todos los cupones que han caducado (su fecha de expiración es anterior al día de hoy) sin que hayan sido usados (no están asociados a un gasto).

DELETE FROM Coupon c 
WHERE c.expiration < CURRENT_DATE AND c.expense IS NULL
    @Override
    public int deleteExpired() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaDelete<Coupon> cd = cb.createCriteriaDelete(Coupon.class);
        Root<Coupon> couponRoot = cd.from(Coupon.class);

        cd.where(cb.and(
                cb.isNull(couponRoot.get(Coupon_.expense)),
                cb.lessThan(
                        couponRoot.get(Coupon_.expiration).as(Date.class),
                        cb.currentDate())));

        return em.createQuery(cd).executeUpdate();
    }

Nótese que se ha vuelto a recurrir al método as para realizar una conversión de tipos. En esta ocasión, pasamos de LocalDate a Date para poder comparar el atributo Coupon#expiration con el resultado de la función CURRENT_DATE.

En las dos operaciones (UPDATE y DELETE), los cambios no se ven reflejados en las entidades que ya estuvieran en el contexto de persistencia, circunstancia que también se produce si usamos JPQL. Lo comprobamos con algunos tests al final del capítulo 32.

Código de ejemplo

El código de ejemplo se encuentra en GitHub (todos los proyectos son independientes pero están en un único repositorio). Para más información sobre cómo utilizar GitHub, consultar este artículo.

>>>> ÍNDICE <<<<

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.