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!