5.6.2.1. Criteria API#

Criteria API es un mecanismo alternativo a JPQL para escribir consultas, que es más verborreico, pero en compensación facilita la construcción dinámica de las consultas en tiempo de ejecución, ya que no se basa en una cadena, sino en la aplicación de métodos a objetos. Se inspiró en Hibernate Criteria.

Lo primero antes de construir la consulta es crear un objeto CriteriaBuilder a partir del objeto EntityManager:

CriteriaBuilder cb = em.getCriteriaBuilder();

A partir de ahora debemos definir qué es lo que se querrá obtener (recordemos que estamos haciendo un SELECT) y sobre qué tabla/entidad se quiere hacer la consulta. Por ejemplo:

5.6.2.1.1. Consulta básica#

Empecemos por una consulta SQL muy básica:

0// SELECT e.* FROM Estudiante e
1CriteriaQuery<Estudiante> criteria = cb.createQuery(Estudiante.class); // Obtendremos estudiantes.
2Root<Estudiante> estudiante = criteria.from(Estudiante.class); // Consultamos la entidad Estudiante
3criteria.select(estudiante); // SELECT e.* FROM Estudiante e;
4
5TypedQuery<Estudiante> query = em.createQuery(criteria);

Como queremos obtener objetos Estudiante (primera línea) y consultamos la entidad Estudiante (segunda línea) es obvio que la consulta es:

SELECT e.* FROM Estudiante e

Si quisiéramos obtener un campo, entonces deberíamos alterar las líneas 1 y 3:

// SELECT e.nombre FROM Estudiante e;
CriteriaQuery<String> criteria = cb.createQuery(String.class); // Obtendremos cadenas
Root<Estudiante> estudiante = criteria.from(Estudiante.class); // Consultamos la entidad Estudiante
criteria.select(estudiante.get("nombre"));

TypedQuery<String> query = em.createQuery(criteria);

Y si varios[1]:

// SELECT e.nombre, e.nacimiento FROM Estudiante e;
CriteriaQuery<Object[]> criteria = cb.createQuery(Object[].class);
Root<Estudiante> estudiante = criteria.from(Estudiante.class); // Consultamos la entidad Estudiante
criteria.select(cb.array(estudiante.get("nombre"), estudiante.get("nacimiento")));

TypedQuery<Object[]> query = em.createQuery(criteria);

En este último caso, puede usarse también Tuple como en JPQL:

CriteriaQuery<Tuple> criteria = cb.createQuery(Tuple.class); // o cb.createTupleQuery();
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
criteria.select(cb.tuple(
   estudiante.get("nombre").alias("nombre"),
   estudiante.get("nacimiento").alias("nacimiento")
));

TypedQuery<Tuple> query = em.createQuery(query);

Pero en ambos casos, deberemos especificar los tipos (String, Centro) al obtenerlos.

Truco

Posiblemente, la solución más elegante es usar Record:

record NombreDate(String nombre, LocalDate nacimiento) {};

CriteriaQuery<NombreDate> criteria = cb.createQuery(NombreDate.class); // o cb.createTupleQuery();
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
var nombre = estudiante.get("nombre").as(String.class).alias("nombre");
var nacimiento = estudiante.get("nacimiento").as(LocalDate.class).alias("nacimiento");
criteria.select(cb.construct(NombreDate.class, nombre, nacimiento));

TypedQuery<NombreDate> query = em.createQuery(query);

el cual nos permite ahorrarnos el tipo al recuperar los datos:

query.getResultList().forEach(e -> {
   // e.nombre() es String; y e.nacimiento(), LocalDate.
   System.out.printf("%s: %s.\n", e.nombre(), e.nacimiento().format(df));
});

Justamente a continuación introduciremos el Metamodel, que nos ahorrará indicar los tipos incluso al hacer las definiciones.

Prudencia

No es una buena práctica referir los atributos de las clases con una cadena (estudiante.get("nombre")), ya que los errores de digitalización o de tipos no pueden detectarse en tiempo de compilación. Por ese motivo, es bastante recomendable generar el Metamodel, que nos permite escribir:

estudiante.get("nombre");

como:

estudiante.get(Estudiante_.nombre);

donde Estudiante_ es una clase generada por el compilador.

Nota

La elección de los atributos nombre y nacimiento no ha sido casual; si hubiéramos escogido otro atributo como centro, que hace referencia a la entidad Centro, JPA habría hecho internamente una composición con JOIN, lo cual no es nuestra intención por ahora; ya que más adelante lo trataremos al estudiar las relaciones.

5.6.2.1.2. Metamodel#

Para la habilitar la creación del metamodelo, debemos añadir a la sección <build> de pom.xml[2]:

<build>
   <plugins>
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.14.0</version>
           <configuration>
               <release>${maven.compiler.release}</release>
               <annotationProcessorPaths>
                   <path>
                       <groupId>org.hibernate.orm</groupId>
                       <artifactId>hibernate-processor</artifactId>
                       <version>${hibernate.version}</version>
                   </path>
               </annotationProcessorPaths>
           </configuration>
       </plugin>
   </plugins>
</build>

Con esto, deberíamos tener disponibles en el proyecto las metaclases correspondientes a nuestro modelo (Centro_ y Estudiante_ en el ejemplo).

Prudencia

Si usamos javadoc para la documentación, esta herramienta busca las clases del código fuente exclusivamente dentro de src/, mientras que las [meta]clases generadas automáticamente por hibernate-processor se incluyen en target/generated-sources/annotations/. Esta circunstancia debemos indicarla:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-javadoc-plugin</artifactId>
    <!-- La última versión puede consultarse en el repositorio de Maven -->
    <version>3.11.2</version>
    <configuration>
        <source>${maven.compiler.release}</source>
        <show>private</show>
        <!-- Debemos añadir annotations para que no den problemas las clases anotadas del Metamodel -->
        <sourcepath>${project.build.sourceDirectory};${project.build.directory}/generated-sources/annotations</sourcepath>
    </configuration>
</plugin>

Ahora podríamos reescribir el ejemplo anterior así:

record NombreDate(String nombre, LocalDate nacimiento) {};

CriteriaQuery<NombreDate> criteria = cb.createQuery(NombreDate.class); // o
cb.createTupleQuery();
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
var nombre = estudiante.get(Estudiante_.nombre).alias("nombre");
var centro = estudiante.get(Estudiante_.nacimiento).alias("nacimiento");
criteria.select(cb.construct(NombreDate.class, nombre, nacimiento));

TypedQuery<NombreDate> query = em.createQuery(criteria);

Nota

El Metamodel también genera Estudiante_.NOMBRE o Estudiante_.CENTRO, pero sólo son constantes de tipo String que se sustituyen por las cadenas "nombre" o "centro", allí donde están. Por tanto, sólo protegen de los fallos de digitalización, pero no permiten la comprobación estática de tipos o la refactorización de código.

5.6.2.1.3. Condiciones#

CriteriaBuilder tiene definidos métodos que implementan los operadores básicos de SQL. Por ejemplo, para escoger los estudiantes no matriculados:

CriteriaQuery<Estudiante> criteria = cb.createQuery(Estudiante.class);
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
criteria.select(estudiante);
criteria.where(cb.isNull(estudiante.get(Estudiante_.centro)));

TypedQuery<Estudiante> query = em.createQuery(criteria);

Para obtener los estudiantes de un centro determinado (suponiendo que éste se almacene en la variable centro) la condición habría sido:

criteria.where(cb.equal(estudiante.get(Estudiante_.centro), centro));

5.6.2.1.4. Ordenación#

Para ordenar resultados basta con aplicar el orden a criteria:

CriteriaQuery<Estudiante> criteria = cb.createQuery(Estudiante.class);
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
criteria.select(estudiante);
criteria.orderBy(cb.desc(estudiante.get(Estudiante_.nacimiento)));

TypedQuery<Estudiante> query = em.createQuery(criteria);

5.6.2.1.5. Agrupación#

También existe el equivalente a GROUP BY:

record CentroCuantos(Centro centro, Long cantidad) {};

CriteriaQuery<CentroCuantos> criteria = cb.createQuery(CentroCuantos);
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
var centro_p = estudiante.get(Estudiante_.centro);
var centro = centro_p.alias("centro");
var cantidad_p = cb.count(estudiante);
var cantidad = cantidad_p.alias("cantidad");
criteria.select(cb.construct(CentroCuantos.class, centro, cantidad))
   .groupBy(centro_p);  // No puede usarse el alias, de ahí la variable intermedia centro_p

TypedQuery<CentroCuantos> query = em.createQuery(criteria);

Si ahora quisiéramos ordenar resultados por la cantidad de estudiantes que tiene cada centro deberíamos añadir:

criteria.orderBy(cb.asc(cantidad_p));

ya que no puede usarse el alias. O, si quisiéramos ordenar por el nombre del centro:

criteria.orderBy(cb.asc(centro_p.get(Centro_.nombre)));

5.6.2.1.6. Relaciones#

Como en JPQL también se puede relacionar fácilmente entidades.

INNER JOIN o, simplemente, JOIN

Es la relación más sencilla y relaciona dos entidades en las que cada clave foránea en una entidad hace referencia a un identificador de la otra. El resultado es que aparecen todos los registros relacionados entre sí. Por tanto:

SELECT e.*,c.* FROM Estudiante e JOIN Centro c ON e.centro = c.id;

muestra todos los estudiantes matriculados junto a los datos del centro en el que está matriculado. La relación es conmutativa y:

SELECT e.*,c.* FROM Centro c JOIN Estudiante e ON e.centro = c.id;

devuelve exactamente el mismo resultado. Para implementar esta relación en Criteria API, podemos hacer:

// SELECT c.* FROM Estudiante e INNER JOIN Centro c ON e.centro = c.id;
CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
Join<Estudiante, Centro> centro = estudiante.join(Estudiante_.centro, JoinType.INNER);
criteria.select(centro);

TypedQuery<Centro> query = em.createQuery(criteria);
query.getResultList().forEach(System.out::println);  // Imprime centros.

La segunda posibilidad, que devuelve el mismo resultado se escribe así:

// SELECT e.* FROM Centro c INNER JOIN Estudiante e ON e.centro = c.id;
CriteriaQuery<Estudiante> criteria = cb.createQuery(Estudiante.class);
Root<Centro> centro = criteria.from(Centro.class);
Join<Centro, Estudiante> estudiante = centro.join(Centro_.estudiantes, JoinType.INNER);
criteria.select(estudiante);

TypedQuery<Estudiante> query = em.createQuery(criteria);
query.getResultList().forEach(System.out::println);  // Imprime estudiantes.

pero obsérvese que requiere que hubiéramos definido la relación como bidireccional.

Prudencia

Hemos afirmado que las consultas son equivalentes, pero no es tal, ya que en el primer caso hemos obtenido centros y en el segundo hemos obtenido estudiantes. La consulta realmente equivalente habría exigido no obtener una entidad pura. Por ejemplo:

// SELECT e.*, c.* FROM Estudiante e JOIN Centro c ON e.centro = c.id;
record EstudianteCentro(Estudiante estudiante, Centro centro) {};

CriteriaQuery<EstudianteCentro> criteria = cb.createQuery(EstudianteCentro.class);
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
Join<Estudiante, Centro> centro = estudiante.join(Estudiante_.centro, JoinType.INNER);
var estudiante_a = estudiante.alias("estudiante");
var centro_a = centro.alias("centro");
criteria.select(EstudiateCentro.class, estudiante_a, centro_a);

De los dos ejemplos anteriores se pueden sacar tres conclusiones:

  1. La relación se establece entre una entidad y el atributo que la relaciona con la otra como en JPQL y a diferencia de lo que ocurre en SQL, en que se establece la relación entre dos tablas. Por ese motivo, el segundo ejemplo es imposible de llevar a cabo si no se hizo la relación bidireccional.

  2. A diferencia de lo que ocurre en la consulta SQL, el primero de los ejemplos no devuelve centros duplicados, aunque varios alumnos compartan un mismo centro. Esto se debe al propio comportamiento de JPA. Por ese motivo, el join explícito no se diferencia en nada de hacer un join implícito como este:

    // Aunque no se exprese el JOIN, éste se realiza para obtener objetos Centro:
    // SELECT c.* FROM Estudiante e JOIN Centro c ON e.centro = c.id;
    CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
    Root<Estudiante> estudiante = criteria.from(Estudiante.class);
    criteria.select(estudiante.get(Estudiante_.centro));
    
    TypedQuery<Centro> query = em.createQuery(criteria);
    query.getResultList().forEach(System.out::println);  // Imprime centros.
    

    ¿Por qué es implícito? Porque se piden objetos Centro completos y para ello JPA no tiene más remedio que recurrir a la relación. Hay aquí que hacer apostilla importante, si no se pidieran objetos Centro, sino objetos Estudiante, pero alguna de las condiciones de la consulta (clausula WHERE) involucrara a algún atributo de Centro, entonces JPA no tendría más remedio que internamente construir la consulta usando un JOIN. Ahora bien, eso no implica que en los estudiantes obtenidos el atributo Centro se haya obtenido también. Si la carga se hubiera definido como perezosa, a pesar del JOIN, no se habrían obtenidos los centros relacionados y cargar el centro de un estudiante concreto implicaría una consulta adicional.

  3. Para emular el comportamiento de SQL y obtener centros repetidos tenemos que evitar obtener entidades puras:

    // SELECT c.* FROM Estudiante e INNER JOIN Centro c ON e.centro = c.id;
    CriteriaQuery<Tuple> criteria = cb.createQuery(Tuple.class);
    Root<Estudiante> estudiante = criteria.from(Estudiante.class);
    Join<Estudiante, Centro> centro = estudiante.join(Estudiante_.centro, JoinType.INNER);
    var centro_a = centro.alias("centro");
    criteria.select(cb.tuple(centro_a));
    
    TypedQuery<Tuple> query = em.createQuery(criteria);
    query.getResultList().forEach(t -> {
       // Imprime centros repetidos.
       System.out.println("%s.\n", t.get("centro", Centro.class));
    });
    

    En este último caso, podría evitarse la repetición usando DISTINCT, tal como se hace en SQL:

    criteria.select(cb.tuple(centro_a))
       .distinct(true);
    

Carga inmediata

En los tres ejemplos en los que obtenemos centros (el join implícito, el join explícito y el join explícito que obtiene tuplas), el hecho de que pidamos centros provoca que estos se carguen directamente gracias a la consulta). Sin embargo, si nuestra consulta hubiera sido una en la que se usa carga perezosa como:

// SELECT c.* FROM Centro c;
CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
Root<Centro> centro = criteria.from(Centro.class);
criteria.select(centro);

TypedQuery<Centro> query = em.createQuery(criteria);
List<Centro> centros = query.getResultList();

Esos centros, como son entidad padre de estudiante, no han cargado de forma efectiva la lista de estudiantes matriculados, ya que de forma predeterminada la carga es perezosa. En consecuencia, usar .getEstudiantes() implica una consulta posterior para cada uno de los centros. Añadir la línea:

Join<Centro, Estudiante> estudiante = centro.join(Centro_.estudiantes, JoinType.INNER);

no provoca la carga inmediata, porque aunque forzamos a que se construya la consulta con un JOIN, no se obtienen expresamente estudiantes en la consulta; y el ORM construye los objetos Centro sin atender a que en la consulta se obtuvieron estudiantes. La consecuencia es que el JOIN es inútil y hemos hecho una consulta a la base de datos más costosa para nada.

Si nuestra intención es obligar a una carga inmediata gracias al JOIN interno, debemos añadir esta línea:

// SELECT c.* FROM Centro c INNER JOIN Estudiante e ON c.id = e.centro;
CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
Root<Centro> centro = criteria.from(Centro.class);
centro.fetch(Centro_.estudiantes, TypeJoin.INNER);
criteria.select(centro);

TypedQuery<Centro> query = em.createQuery(criteria);
List<Centro> centros = query.getResultList();

Ver también

Consulte para más información el epígrafe dedicado a optimización.

LEFT JOIN

Una relación LEFT JOIN obtiene los registros relacionados entre sí (como INNER JOIN) más aquellos presentes en la entidad de la izquierda que no están relacionados con ninguno de la derecha. Debido a ello, los campos de la derecha aparecen a NULL en estos registros adicionales. Por ese motivo:

SELECT e.*,c.* FROM Estudiante e LEFT JOIN Centro c ON e.centro = c.id;

no equivale a:

SELECT e.*,c.* FROM Centro c LEFT JOIN Estudiante e ON e.centro = c.id;

ya que la primera consulta, además de los estudiantes matriculados, devuelve los no matriculados; pero no los centros sin estudiantes; mientras que la segunda consulta devuelve los estudiantes matriculados y los centros sin estudiantes; pero no los estudiantes no matriculados.

La primera relación se escribe en Criteria API así:

// SELECT e.*,c.* FROM Estudiante e LEFT JOIN Centro c ON e.centro = c.id;
record EstudianteCentro(Estudiante estudiante, Centro centro) {};

CriteriaQuery<EstudianteCentro> criteria = cb.createQuery(EstudianteCentro.class);
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
Join<Estudiante, Centro> centro = estudiante.join(Estudiante_.centro, JoinType.LEFT);
criteria.select(cb.construct(EstudianteCentro.class, estudiante.alias("estudiante"), centro.alias("centro")));

mientras que la segunda:

// SELECT e.*,c.* FROM Centro c LEFT JOIN Estudiante e ON e.centro = c.id;
record EstudianteCentro(Estudiante estudiante, Centro centro) {};

CriteriaQuery<EstudianteCentro> criteria = cb.createQuery(EstudianteCentro.class);
Root<Centro> centro = criteria.from(Centro.class);
Join<Centro, Estudiante> estudiante = centro.join(Centro_.estudiantes, JoinType.LEFT);
criteria.select(cb.construct(EstudianteCentro.class, estudiante.alias("estudiante"), centro.alias("centro")));

Pero nótese que esta segunda requiere una relación bidireccional.

Es muy común usar LEFT JOIN para obtener indirectamente los centros sin alumnos matriculados:

// SELECT c.* FROM Centro c LEFT JOIN Estudiante e ON c.id = e.centro WHERE e.id IS NULL;
CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
Root<Centro> centro = criteria.from(Centro.class);
Join<Centro, Estudiante> estudiante = centro.join(Centro_.estudiantes, JoinType.LEFT);
criteria.select(centro).where(cb.isNull(estudiante.get("id")));

Advertencia

JPA no define las relaciones RIGHT JOIN, ya que pueden considerarse redundantes al ser equivalentes a una relación LEFT JOIN con las entidades cambiadas de orden. Pero eso, como en el ejemplo de arriba, puede obligarnos a tener que definir relaciones bidireccionales para determinadas consultas.

Subconsultas

Criteria API también ofrece la posibilidad de hacer subconsultas. De hecho, el problema anterior podría haberse resuelto con esta otra consulta[3]:

SELECT c.* FROM Centro c WHERE c.id NOT IN (SELECT e.centro WHERE Estudiante e);

Esta solución, además, no obliga a hacer la relación bidireccional. Veamos cómo implementarla con Criteria API:

CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
Root<Centro> centro = criteria.from(Centro.class);

// Subconsulta
Subquery<Long> subquery = criteria.subquery(Long.class) // e.centro es Long
Root<Estudiante> subEstudiante = subquery.from(Estudiante.class);
subquery.select(subEstudiante.get(Estudiante_.centro));

// Consulta con la subconsulta.
criteria.select(centro)
   .where(cb.not(centro.in(subquery)));

Nota

Nótese que aquí podemos comparar directamente centros en vez de identificadores de centros que sería la traducción literal:

subquery.select(subEstudiante.get(Estudiante_.centro).get(Centro_.id));

// Consulta con la subconsulta.
criteria.select(centro)
   .where(cb.not(centro.get(Centro_.id).in(subquery)));

O bien, si usamos el operador EXISTS de SQL:

SELECT c.* FROM Centro c WHERE NOT EXISTS (SELECT 1 FROM Estudiante e WHERE e.centro = c.id);

que traducido a Criteria API queda:

CriteriaQuery<Centro> criteria = cb.createQuery(Centro.class);
Root<Centro> centro = criteria.from(Centro.class);

// Subconsulta
Subquery<Long> subquery = criteria.subquery(Long.class) // 1 es Long
Root<Estudiante> subEstudiante = subquery.from(Estudiante.class);
subquery.select(cb.literal(1L))
   // Como en el caso anterior, no es necesario comparar explícitamente IDs.
   .where(cb.equal(subEstudiante.get(Estudiante_.centro), centro));

// Consulta con la subconsulta
criteria.select(centro)
   .where(cb.not(cb.exists(subquery)));

Consejo

En términos de rendimiento, de las tres alternativas presentadas la más eficiente es esta última:

  • IN requiere generar toda las filas de la subconsulta y, además, genera resultados inesperados cuando algunos de los resultados de la subconsulta es nulo ya que en SQL estándar la operación 2 IN (1, NULL) devuelve UNKNOWN no FALSE. Justamente en este ejemplo, puede haber Estudiantes sin centro, por lo que tendríamos este problema y la alternativa ni siquiera es válida en este caso.

  • LEFT JOIN necesita generar todas las consultas y luego filtrar aquellas nulas. Esto puede generar un resultado intermedio grande que muchas veces lo hace menos eficiente que la alternativa con EXISTS.

  • EXISTS detiene la comprobación cuando encuentra la primera coincidencia.

Cadena de consultas

Observemos parte del código ya presentado anteriormente:

CriteriaQuery<Estudiante> criteria = cb.createQuery(Estudiante.class);
Root<Estudiante> estudiante = criteria.from(Estudiante.class);
Join<Estudiante, Centro> centro = estudiante.join(Estudiante_.centro, JoinType.INNER);
criteria.select(estudiante);

Partimos de Estudiante (estudiante) y relacionamos con Centro a través del atributo centro de Estudiante, de ahí que la relación se haya escrito utilizando el objeto estudiante.

O sea, para relacionar con Centro, juntamos estudiante (que es Estudiante) a través de su atributo llamado centro. Si deseamos que en la consulta participe una segunda relación, deberemos tener en cuenta con qué entidad se relaciona. Si la tercera entidad fuera la entidad Grupo relacionada con Estudiante entonces tendríamos que hacer:

Join<Estudiante, Centro> centro = estudiante.join(Estudiante_.centro, JoinType.INNER);
Join<Estudiante, Curso> curso = estudiante.join(Estudiante_.curso, JoinType.INNER);

En cambio, si la tercera entidad fuera la entidad ComunidadA relacionada con Centro, entonces la relación se establecería así:

Join<Estudiante, Centro> centro = estudiante.join(Estudiante_.centro, JoinType.INNER);
Join<Centro, ComunidadA> comunidad = centro.join(Centro_.comunidad, JoinType.INNER);

5.6.2.1.7. Actualización y borrado#

Al igual que JPQL, también se puede actualizar objetos. Por ejemplo, esto desmatricularía a todos los estudiantes cuyo nombre empieza por «J»:

// UPDATE Estudiante SET centro = NULL WHERE nombre LIKE 'J%';
CriteriaUpdate<Estudiante> update = cb.createCriteriaUpdate(Estudiante.class);
Root<Estudiante> estudiante = update.from(Estudiante.class);
update.set(estudiante.get(Estudiante_.centro), null);
update.where(cb.like(estudiante.get(Estudiante_.nombre), "J%"));

em.createQuery(update).executeUpdate();

También es posible borrar:

// DELETE FROM Estudiante WHERE nombre LIKE 'J%';
CriteriaDelete<Estudiante> delete = cb.createCriteriaDelete(Estudiante.class);
Root<Estudiante> estudiante = delete.from(Estudiante.class);
delete.where(cb.like(estudiante.get(Estudiante_.nombre), "J%"));

em.createQuery(delete).executeUpdate();

Notas al pie