Diferentes Tipos de Consultas en JPA

Introducción

En este artículo, nos enfocaremos en los diferentes tipos de consultas de Java Persistence API (JPA). También analizaremos las diferencias entre ellas y abordaremos los pros y los contras de cada tipo. La capacidad de interactuar con bases de datos de manera eficiente es fundamental para cualquier aplicación Java, y entender cómo funcionan estas consultas puede mejorar significativamente la calidad del código de tus proyectos.

1. Visión General

En este tutorial, discutiremos los diferentes tipos de JPA queries. Además, nos centraremos en comparar las diferencias entre ellas y expandiremos sobre los pros y los contras de cada una.

2. Configuración

Primero, definamos la clase UserEntity que utilizaremos para todos los ejemplos en este artículo:

@Table(name = "users")
@Entity
public class UserEntity {

    @Id
    private Long id;
    private String name;
    // Constructor estándar, getters y setters.
}

Hay tres tipos básicos de consultas JPA:

  • Query, escrita en sintaxis Java Persistence Query Language (JPQL).
  • NativeQuery, escrita en sintaxis SQL pura.
  • Criteria API Query, construida programáticamente a través de diferentes métodos.

Exploraremos cada una de ellas a continuación.

3. Query

3.1. Creación de una Query

Una Query es similar en sintaxis a SQL, y se utiliza generalmente para realizar operaciones CRUD. Aquí hay un ejemplo de cómo se podría obtener un usuario por su ID usando una Query:

public UserEntity getUserByIdWithPlainQuery(Long id) {
    Query jpqlQuery = getEntityManager().createQuery("SELECT u FROM UserEntity u WHERE u.id=:id");
    jpqlQuery.setParameter("id", id);
    return (UserEntity) jpqlQuery.getSingleResult();
}

Esta Query recupera el registro correspondiente de la tabla users y lo mapea al objeto UserEntity.

3.2. Subtipos de Query

3.2.1. TypedQuery

En el ejemplo anterior, debemos prestar atención a la declaración return. JPA no puede deducir qué tipo tendrá el resultado de la Query y, por lo tanto, debemos hacer un casting. Sin embargo, JPA proporciona un subtipo especial llamado TypedQuery. Este siempre es preferido si conocemos el tipo de resultado de nuestra Query de antemano. Además, hace que nuestro código sea mucho más confiable y fácil de probar.

public UserEntity getUserByIdWithTypedQuery(Long id) {
    TypedQuery typedQuery = getEntityManager().createQuery("SELECT u FROM UserEntity u WHERE u.id=:id", UserEntity.class);
    typedQuery.setParameter("id", id);
    return typedQuery.getSingleResult();
}

De esta manera, obtenemos un tipado más fuerte sin coste adicional, evitando posibles excepciones de casting en el futuro.

3.2.2. NamedQuery

Mientras que podemos definir dinámicamente una Query en métodos específicos, pueden eventualmente convertirse en una base de código difícil de mantener. ¿Qué pasaría si pudiéramos mantener consultas de uso general en un lugar centralizado y fácil de leer? JPA también tiene una solución para esto, conocida como NamedQuery.

Podemos definir NamedQueries en un archivo orm.xml o en un archivo de propiedades. También podemos definir un NamedQuery en la propia clase Entity, proporcionando una forma centralizada y rápida de leer y encontrar las consultas relacionadas con una Entity.

@Table(name = "users")
@Entity
@NamedQuery(name = "UserEntity.findByUserId", query = "SELECT u FROM UserEntity u WHERE u.id=:userId")
public class UserEntity {
    @Id
    private Long id;
    private String name;
    // Constructor estándar, getters y setters.
}

Importante: La anotación @NamedQuery debe agruparse dentro de una anotación @NamedQueries si estamos usando Java antes de la versión 8. Desde Java 8 en adelante, podemos simplemente repetir la anotación @NamedQuery en nuestra clase Entity.

Usar un NamedQuery es muy sencillo:

public UserEntity getUserByIdWithNamedQuery(Long id) {
    Query namedQuery = getEntityManager().createNamedQuery("UserEntity.findByUserId");
    namedQuery.setParameter("userId", id);
    return (UserEntity) namedQuery.getSingleResult();
}

4. NativeQuery

Uso de NativeQuery

Un NativeQuery es simplemente una consulta SQL. Estas consultas nos permiten aprovechar al máximo nuestro sistema de base de datos, ya que podemos usar características propietarias que no están disponibles en la sintaxis JPQL restringida.

Sin embargo, esto conlleva un costo. Perdemos la portabilidad de la base de datos de nuestra aplicación porque nuestro proveedor de JPA no puede abstraer detalles específicos de la implementación o del proveedor de la base de datos.

Aquí hay un ejemplo de cómo utilizar una NativeQuery que produce los mismos resultados que nuestros ejemplos anteriores:

public UserEntity getUserByIdWithNativeQuery(Long id) {
    Query nativeQuery = getEntityManager().createNativeQuery("SELECT * FROM users WHERE id=:userId", UserEntity.class);
    nativeQuery.setParameter("userId", id);
    return (UserEntity) nativeQuery.getSingleResult();
}

Debemos considerar si una NativeQuery es la única opción. La mayoría de las veces, una buena Query JPQL puede satisfacer nuestras necesidades y, lo más importante, mantener un nivel de abstracción de la implementación real de la base de datos.

Usar NativeQuery no significa necesariamente quedarnos atados a un proveedor específico de bases de datos. Después de todo, si nuestras consultas no utilizan comandos SQL propietarios y utilizan solo una sintaxis SQL estándar, cambiar de proveedor no debería ser un problema.

5. Comparativa: Query, NamedQuery y NativeQuery

5.1. Query

Podemos crear una consulta utilizando entityManager.createQuery(queryString).

Pros:

  • Posibilidad de construir cadenas de consulta dinámicas.
  • Escribible en JPQL, por lo que son portables.

Contras:

  • Para una consulta dinámica, puede compilarse en una declaración SQL nativa varias veces dependiendo del cache de planes de consulta.
  • Las consultas pueden dispersarse en varias clases Java, lo que podría dificultar su mantenimiento.

5.2. NamedQuery

Una vez que se ha definido un NamedQuery, podemos referirnos a él utilizando el EntityManager:

entityManager.createNamedQuery(queryName);

Pros:

  • NamedQueries son compiladas y validadas cuando se carga la unidad de persistencia, es decir, se compilan una sola vez.
  • Centralizamos NamedQueries para hacerlas más fáciles de mantener.

Contras:

  • NamedQueries siempre son estáticas.
  • Aunque NamedQueries se pueden referenciar en los repositorios de Spring Data JPA, no se admite el ordenamiento dinámico.

5.3. NativeQuery

Podemos crear una NativeQuery utilizando EntityManager.

entityManager.createNativeQuery(sqlStmt);

Dependiendo del mapeo de resultados, también podemos pasar un segundo parámetro al método, como una clase Entity, como hemos visto anteriormente.

Pros:

  • A medida que nuestras consultas se vuelven complejas, a veces las declaraciones SQL generadas por JPA no son las más optimizadas. En este caso, podemos usar NativeQueries para hacer las consultas más eficientes.
  • NativeQueries nos permiten usar características específicas del proveedor de la base de datos.

Contras:

  • Las características específicas del proveedor pueden brindar comodidad y mejor rendimiento, pero pagamos por ese beneficio al perder la portabilidad de un sistema de base de datos a otro.

6. Criteria API Query

Uso de Criteria API

Las consultas de Criteria API son consultas construidas programáticamente y de forma segura en cuanto al tipo, que son algo similares a las consultas JPQL en sintaxis:

public UserEntity getUserByIdWithCriteriaQuery(Long id) {
    CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
    CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(UserEntity.class);
    Root userRoot = criteriaQuery.from(UserEntity.class);
    UserEntity queryResult = getEntityManager().createQuery(
            criteriaQuery.select(userRoot)
            .where(criteriaBuilder.equal(userRoot.get("id"), id))
    ).getSingleResult();
    return queryResult;
}

Al principio, puede parecer abrumador usar las consultas de Criteria API, pero pueden ser una excelente opción cuando necesitamos agregar elementos de consulta dinámicos o cuando se acoplan con el JPA Metamodel.

7. Conclusión

En este breve artículo, aprendimos qué son las consultas JPA, junto con su uso.

Las consultas JPA son una excelente manera de abstraer nuestra lógica de negocio de nuestra capa de acceso a datos, ya que podemos confiar en la sintaxis JPQL y dejar que nuestro proveedor de JPA elegido maneje la traducción de la consulta.

Como consejo práctico, cuando estés diseñando tu interacción con la base de datos en una aplicación Java, evalúa las necesidades específicas de tu proyecto. Utiliza NamedQueries para obtener claridad y mantenimiento en consultas comunes, pero no dudes en recurrir a NativeQueries cuando se requiera optimización y funcionalidad específica del proveedor. Evita el uso indiscriminado de NativeQueries, ya que puede dificultar la portabilidad de tu aplicación a diferentes bases de datos.

Experimenta con las distintas opciones y elige la que mejor se adapte a tus necesidades de desarrollo. ¡Feliz codificación!