1. Introducción
Building queries using JPA isn’t difficult; however, we sometimes forget simple things that make a huge difference. One of these things is JPA query parameters, and that’s what we’ll focus on in this tutorial.
La capacidad de construir consultas efectivas y seguras es fundamental para cualquier programador que trabaje con bases de datos mediante Java. Sin embargo, muchas veces pasamos por alto ciertos aspectos de JPA (Java Persistence API) que pueden facilitar muchísimo este proceso. En esta entrada de blog, exploraremos qué son los parámetros de consulta, cómo se utilizan en JPA, y los beneficios que ofrecen en términos de seguridad y rendimiento.
2. ¿Qué son los Parámetros de Consulta?
Para comenzar, expliquemos qué son los parámetros de consulta.
Los parámetros de consulta son una forma de crear y ejecutar consultas parametrizadas. Así, en lugar de escribir:
SELECT * FROM employees e WHERE e.emp_number = '123';
Escribimos:
SELECT * FROM employees e WHERE e.emp_number = ?;
Usando una sentencia preparada de JDBC, necesitamos establecer el parámetro antes de ejecutar la consulta:
pStatement.setString(1, "123");
3. ¿Por Qué Deberíamos Usar Parámetros de Consulta?
En lugar de usar parámetros de consulta, podríamos haber utilizado literales, aunque eso no es lo más recomendable, como veremos ahora.
Reescribamos la consulta anterior para obtener empleados por emp_number usando la API de JPA, pero en lugar de usar un parámetro, usaremos un literal:
String empNumber = "A123";
TypedQuery<Employee> query = em.createQuery(
"SELECT e FROM Employee e WHERE e.empNumber = '" + empNumber + "'", Employee.class);
Employee employee = query.getSingleResult();
Este enfoque tiene algunas desventajas:
- Incrustar parámetros introduce un riesgo de seguridad, haciéndonos vulnerables a ataques de inyección JPQL. En lugar del valor esperado, un atacante podría inyectar cualquier expresión JPQL inesperada y posiblemente peligrosa.
- Dependiendo de la implementación de JPA que usemos y de las heurísticas de nuestra aplicación, la caché de consultas puede agotarse. Una nueva consulta puede ser construida, compilada y almacenada en caché cada vez que la utilicemos con cada nuevo valor/parámetro. Como mínimo, esto no será eficiente, y también puede llevar a un OutOfMemoryError.
4. Parámetros de Consulta en JPA
Similar a los parámetros de sentencias preparadas de JDBC, JPA especifica dos maneras diferentes de escribir consultas parametrizadas utilizando:
- Parámetros posicionales
- Parámetros nombrados
Pueden usarse tanto parámetros posicionales como nombrados, pero no debemos mezclarlos en la misma consulta.
4.1. Parámetros Posicionales
Utilizar parámetros posicionales es una forma de evitar los problemas mencionados anteriormente.
Veamos cómo escribir una consulta con la ayuda de parámetros posicionales:
TypedQuery<Employee> query = em.createQuery(
"SELECT e FROM Employee e WHERE e.empNumber = ?1", Employee.class);
String empNumber = "A123";
Employee employee = query.setParameter(1, empNumber).getSingleResult();
Como hemos visto en el ejemplo anterior, declaramos estos parámetros dentro de la consulta escribiendo un signo de interrogación, seguido de un número entero positivo. Comenzamos con 1 y continuamos incrementándolo en cada uso.
Podemos usar el mismo parámetro más de una vez dentro de la misma consulta, lo que hace que estos parámetros sean más similares a los parámetros nombrados.
El número de los parámetros es una característica muy útil, ya que mejora la usabilidad, legibilidad y mantenibilidad.
Es importante mencionar que las consultas SQL nativas soportan la vinculación de parámetros posicionales también.
4.2. Parámetros Posicionales de Colección
Como se mencionó anteriormente, también podemos usar parámetros de tipo colección:
TypedQuery<Employee> query = entityManager.createQuery(
"SELECT e FROM Employee e WHERE e.empNumber IN (?1)" , Employee.class);
List<String> empNumbers = Arrays.asList("A123", "A124");
List<Employee> employees = query.setParameter(1, empNumbers).getResultList();
4.3. Parámetros Nombrados
Los parámetros nombrados son bastante similares a los parámetros posicionales; sin embargo, al usarlos, hacemos que los parámetros sean más explícitos y la consulta se vuelve más legible:
TypedQuery<Employee> query = em.createQuery(
"SELECT e FROM Employee e WHERE e.empNumber = :number" , Employee.class);
String empNumber = "A123";
Employee employee = query.setParameter("number", empNumber).getSingleResult();
La consulta anterior es la misma que la primera, pero hemos utilizado :number, un parámetro nombrado, en lugar de ?1.
Podemos ver que declaramos el parámetro con un dos puntos, seguido de un identificador de cadena (identificador JPQL), que es un marcador de posición para el valor real que estableceremos en tiempo de ejecución. Antes de ejecutar la consulta, debemos establecer el parámetro o parámetros utilizando el método setParameter.
Una cosa interesante que resaltar es que TypedQuery soporta encadenamiento de métodos, lo que se vuelve muy útil cuando se deben establecer múltiples parámetros.
Veamos una variación de la consulta anterior utilizando dos parámetros nombrados para ilustrar el encadenamiento de métodos:
TypedQuery<Employee> query = em.createQuery(
"SELECT e FROM Employee e WHERE e.name = :name AND e.age = :empAge" , Employee.class);
String empName = "John Doe";
int empAge = 55;
List<Employee> employees = query
.setParameter("name", empName)
.setParameter("empAge", empAge)
.getResultList();
Aquí estamos recuperando todos los empleados con un nombre y edad dados. Como vemos claramente, podemos construir consultas con múltiples parámetros y tantas ocurrencias de ellos como sea necesario.
Si por alguna razón necesitamos usar el mismo parámetro muchas veces dentro de la misma consulta, simplemente necesitamos establecerlo una vez usando el método “setParameter”. En tiempo de ejecución, los valores especificados reemplazarán cada ocurrencia del parámetro.
Finalmente, es importante mencionar que la especificación de la API de Java Persistence no estipula que las consultas nativas soporten parámetros nombrados. Aunque algunas implementaciones, como Hibernate, lo soportan, debemos tener en cuenta que si lo usamos, la consulta no será tan portable.
4.4. Parámetros Nombrados de Colección
Por claridad, también demostremos cómo funciona esto con parámetros de tipo colección:
TypedQuery<Employee> query = entityManager.createQuery(
"SELECT e FROM Employee e WHERE e.empNumber IN (:numbers)" , Employee.class);
List<String> empNumbers = Arrays.asList("A123", "A124");
List<Employee> employees = query.setParameter("numbers", empNumbers).getResultList();
Como podemos ver, funciona de manera similar a los parámetros posicionales.
5. Parámetros de Consulta en Criterios
Una consulta de JPA puede construirse utilizando la API de Criterios de JPA, que la documentación oficial de Hibernate explica en detalle.
En este tipo de consulta, representamos los parámetros usando objetos en lugar de nombres o índices.
Construyamos la misma consulta nuevamente, pero esta vez utilizando la API de Criterios para demostrar cómo manejar los parámetros de consulta al tratar con CriteriaQuery:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Employee> cQuery = cb.createQuery(Employee.class);
Root<Employee> c = cQuery.from(Employee.class);
ParameterExpression<String> paramEmpNumber = cb.parameter(String.class);
cQuery.select(c).where(cb.equal(c.get(Employee_.empNumber), paramEmpNumber));
TypedQuery<Employee> query = em.createQuery(cQuery);
String empNumber = "A123";
query.setParameter(paramEmpNumber, empNumber);
Employee employee = query.getResultList();
Para este tipo de consulta, la mecánica del parámetro es un poco diferente ya que utilizamos un objeto de parámetro, pero en esencia, no hay diferencia.
En el ejemplo anterior, podemos ver el uso de la clase Employee_. Generamos esta clase con el generador de metamodelo de Hibernate. Estos componentes son parte del metamodelo estático de JPA, que permite construir consultas de criterios de manera fuertemente tipada.
6. Conclusión
En este artículo, nos centramos en la mecánica de construir consultas utilizando parámetros de consulta de JPA o parámetros de entrada.
Aprendimos que tenemos dos tipos de parámetros de consulta, posicionales y nombrados, y depende de nosotros cuál se ajuste mejor a nuestros objetivos.
También vale la pena señalar que todos los parámetros de consulta deben ser de un solo valor, excepto para las expresiones in. Para las expresiones in, podemos usar parámetros de entrada de valor de colección, como arreglos o List, como se mostró en los ejemplos anteriores.
Aplicar buenos principios al usar estos parámetros no solo mejorará la seguridad de nuestras aplicaciones, sino que también optimizará el rendimiento de las consultas. Recuerda siempre utilizar parámetros en lugar de literales y asegúrate de elegir el enfoque que mejor se ajuste a tus necesidades y estilo de codificación.