Spring Data JPA - Guía para la anotación @Query

En esta guía, veremos ejemplos prácticos de cómo usar la anotación @Query de Spring Data JPA, cómo funciona la parametrización indexada y con nombre y cómo paginar y ordenar los resultados de las consultas.

Introducción

Si ha trabajado con Spring Data JPA durante algún tiempo, probablemente esté familiarizado con los métodos de consulta derivados:

1
2
3
public interface MyRepository extends JpaRepository<Client, Long> {
   List<Client> findByOrganizationName(String name);
}

Son una forma ingeniosa y rápida de descargar la carga de escribir consultas en Spring Data JPA simplemente definiendo nombres de métodos.

En este escenario hipotético, hemos definido un JpaRepository para una clase Entity, que tiene un atributo llamado organizationName. En lugar de implementar este método en un servicio que implementa MyRepository, Spring Data JPA genera una consulta automáticamente con el nombre del método. Generará una consulta que devuelve una lista de todos los registros de Entidad, con un nombre de organización coincidente.

Una vez que se llama al método, se realiza la siguiente solicitud de Hibernate:

1
2
3
4
5
6
7
select 
client0_.id as id1_0_, 
client0_.age as age2_0_, 
client0_.name as name3_0_, 
client0_.organinzation as organinz4_0_ 
from 
client client0_ where client0_.organinzation=?

Esta es una característica extremadamente flexible y poderosa de Spring Data JPA y le permite arrancar consultas sin escribir las consultas en sí, o incluso implementar cualquier lógica de manejo en el back-end.

Sin embargo, se vuelven muy difíciles de crear cuando se requieren consultas complejas:

1
2
3
public interface PropertyRepository extends JpaRepository<Property, Long> {
   List<Property> findPropertiesByTransactionTypeAndPropertyType(@Param("transaction_type") TransactionType transactionType, @Param("property_type") PropertyType propertyType);
}

Y esto es para solo dos parámetros. ¿Qué sucede cuando desea crear una consulta para 5 parámetros?

Además, ¿cuántas variaciones de método crearás?

Este es el punto en el que probablemente querrá escribir sus propias consultas. Esto es factible a través de la anotación @Query.

La anotación @Query se aplica a nivel de método en las interfaces JpaRepository y pertenecen a un solo método. El idioma utilizado dentro de la anotación depende de su back-end, o puede utilizar el JPQL neutral (para bases de datos relacionales).

En el caso de las variantes de JpaRepository, como MongoRepository, naturalmente, escribirá consultas Mongo, mientras que si está utilizando una base de datos relacional, escribirá consultas SQL.

Cuando se llama al método, la consulta desde dentro de la anotación @Query se activa y devuelve los resultados.

Nota: Esta guía cubrirá Spring Data JPA junto con una base de datos relacional y utilizará JPQL y SQL nativo que no son aplicables a bases de datos no relacionales.

If you'd like to read more about writing MongoDB-native queries, read our Spring Data MongoDB: Guía para la anotación @Query!

¿Qué es JPQL?

JPQL significa Lenguaje de consulta de persistencia de Java. Está definido en la especificación JPA y es un lenguaje de consulta orientado a objetos que se utiliza para realizar operaciones de base de datos en entidades persistentes.

JPA se activa como mediador y transpila consultas JPQL a consultas SQL para su ejecución.

Nota: Cabe señalar que, en comparación con el SQL nativo, JPQL no interactúa con las tablas, los registros y los campos de la base de datos, sino con las clases e instancias de Java.

Algunas de sus características incluyen:

  • Es un lenguaje de consulta independiente de la plataforma.
  • Es simple y robusto.
  • Se puede utilizar con cualquier tipo de base de datos de relaciones.
  • Se puede declarar estáticamente en metadatos o también se puede construir dinámicamente en el código.
  • Es insensible a mayúsculas y minúsculas.

Si su base de datos puede cambiar o varía de desarrollo a producción, siempre que ambas sean relacionales, JPQL funciona de maravilla y puede escribir consultas JPQL para crear una lógica genérica que se puede usar una y otra vez.

Si aún no está familiarizado con JPQL, lea nuestra Guía para comprender JPQL (¡próximamente!)

Si está utilizando una base de datos no relacional, como MongoDB, escribirá las consultas nativas de esa base de datos.

Again, if you'd like to read more about writing MongoDB-native queries, read our Spring Data MongoDB: Guía para la anotación @Query!

Estructura de consulta JPQL

La sintaxis de JPQL es muy similar a la de SQL. Dado que la mayoría de los desarrolladores ya están familiarizados con la sintaxis de SQL, se vuelve fácil de aprender y usar JPQL.

Las estructuras de las consultas SELECT, UPDATE y DELETE de JPQL son:

1
2
3
4
5
6
7
8
SELECT ... FROM ...
[WHERE ...]
[GROUP BY ... [HAVING ...]]
[ORDER BY ...]

DELETE FROM ... [WHERE ...]

UPDATE ... SET ... [WHERE ...]

Estaremos escribiendo estas consultas manualmente en la anotación @Query en un momento.

Comprensión de la anotación @Query {#comprensión de la anotación de consulta}

La anotación @Query solo se puede usar para anotar métodos de interfaz de repositorio. La llamada de los métodos anotados activará la ejecución de la declaración que se encuentra en él, y su uso es bastante sencillo.

La anotación @Query admite tanto SQL nativo como JPQL. Cuando se usa SQL nativo, el parámetro nativeQuery de la anotación debe establecerse en true:

1
2
@Query("NATIVE_QUERY...", nativeQuery=true)
List<Entity> findAllByName(String name);

Para seleccionar todos los clientes de una base de datos, podemos usar una consulta nativa o JPQL:

1
2
3
4
5
@Query("SELECT(*) FROM CLIENT", nativeQuery=true)
List<Client> findAll();

@Query("SELECT client FROM Client client")
List<Client> findAll();

Eso es todo. Su trabajo está hecho, similar a cómo los métodos de consulta derivados se encargan del trabajo por usted: este @Query se activa cuando llama al método findAll().

Sin embargo, si solo desea encontrar todos los registros, es más fácil usar un método de consulta derivado: para eso existen. Querrás escribir tus propias consultas cuando haya variables dinámicas que quieras pasar como parámetros a las propias consultas.

Parámetros del método de referencia {#parámetros del método de referencia}

Tanto las consultas nativas como las consultas JPQL en la anotación @Query pueden aceptar parámetros de método anotados, que se clasifican además en:

  • Parámetros basados ​​en posición
  • Parámetros con nombre

Al usar parámetros basados ​​en la posición, debe realizar un seguimiento del orden en que proporciona los parámetros en:

1
2
@Query("SELECT c FROM Client c WHERE c.name = ?1 AND c.age = ?2")
List<Client> findAll(String name, int age);

El primer parámetro pasado al método se asigna a ?1, el segundo se asigna a ?2, etc. Si accidentalmente los cambia, su consulta probablemente generará una excepción o producirá resultados incorrectos de forma silenciosa.

Por otro lado, parámetros con nombre son, bueno, nombrados y pueden ser referenciados por nombre, sin importar su posición:

1
2
@Query("SELECT c FROM Client c WHERE c.name = :name and c.age = :age")
List<Client> findByName(@Param("name") String name, @Param("age") int age);

El nombre dentro de la anotación @Param coincide con los parámetros nombrados en la anotación @Query, por lo que puede llamar a sus variables como quiera, pero por motivos de coherencia, es Se aconseja utilizar el mismo nombre.

Si no proporciona un ‘@Param’ coincidente para un parámetro con nombre en la consulta, se lanza una excepción en tiempo de compilación:

1
2
@Query("SELECT c FROM Client c WHERE c.name = :name and c.age = :age")
List<Client> findByName(@Param("name") String name, @Param("num1") int age);

Resultados en:

1
java.lang.IllegalStateException: Using named parameters for method public abstract ClientRepository.findByName(java.lang.String,int) but parameter 'Optional[num1]' not found in annotated query 'SELECT c FROM Client c WHERE c.name = :name and c.age = :age'!

Expresiones SpEL con la anotación @Query

SpEL (Spring Expression Language) es un lenguaje que admite consultas y la manipulación de un gráfico de objetos en tiempo de ejecución:

1
#{expression}

La expresión SpEL más útil que se puede usar en la anotación @Query es #{#entityname}.

Como su nombre lo indica, denota el nombre de la entidad al que hace referencia el repositorio en el que se encuentra. Evita indicar el nombre real de la entidad y se resuelve de la siguiente manera:

Si el tipo de dominio ha establecido la propiedad de nombre en la anotación @Entity, se usa. De lo contrario, se utiliza el nombre de clase simple del tipo de dominio.

En otras palabras:

1
2
3
4
5
6
7
// #{#entityName} resolves to "client_entity"
@Entity(name = "client_entity")
public class Client {}

// #{#entityName} resolves to "Client"
@Entity()
public class Client {}

Esto nos permite abstraer las consultas como:

1
2
3
4
public interface ClientRepository extends JpaRepository<Client, Long> {     
    @Query("select e from #{#entityName} e where e.name = ?1")     
    List<Client> findByName(String name);
}

Modificación de consultas {#modificación de consultas}

Las consultas DELETE y UPDATE se llaman consultas de modificación y deben llevar una anotación adicional: @Modifying. Esto activará la consulta en la anotación @Query para permitirle realizar modificaciones en las entidades, en lugar de solo recuperar datos.

Sin la anotación @Modifying adicional, estaríamos frente a una InvalidDataAccessApiUsageException, que nos informa que la anotación @Query no admite declaraciones DML (lenguaje de manipulación de datos):

1
2
3
4
5
6
7
@Modifying
@Query(DELETE c FROM Client c WHERE c.name = :name)
void deleteClientByName(@Param("name") String name);

@Modifying
@Query(UPDATE Client c WHERE c.id = :id)
void updateUserById(@Param("id") long id);

Clasificación y paginación

Tanto la clasificación como la paginación se pueden realizar de la misma manera que para las consultas derivadas:

Para crear consultas Paginables y Ordenables, proporcione el parámetro Paginable al método, que Spring Data JPA recoge automáticamente, si extiende la interfaz PagingAndSortingRepository al menos .

JpaRepository ya extiende PagingAndSortingRepository por lo que la funcionalidad está inherentemente allí:

1
2
3
4
5
@Repository
public interface ClientRepository extends JpaRepository<Client, Long> {
    @Query("select e from #{#entityName} e where e.organization = ?1")
    Page<Client> findByOrganization(String name, Pageable pageable);
}

El tipo de retorno del método se convierte en Page<T>, List<T> o Slice<T>, aunque Page<T> es el único que realiza un seguimiento de todas las entidades recuperadas y el resultados paginados. El Pageable que se supone que también pasaremos ahora está construido como PageRequest.of(int page, int size, Sort sort).

Proporcionamos la página que estamos buscando, el tamaño de las páginas y cómo se ordenan los datos en su interior:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@RestController
public class Controller {

    @Autowired
    private ClientRepository clientRepository;

    @GetMapping(value = "/", produces = MediaType.TEXT_PLAIN_VALUE)
    public ResponseEntity main() {
        clientRepository.save(new Client(1, 22, "David", "wikihtp"));
        clientRepository.save(new Client(2, 34, "John", "wikihtp"));
        clientRepository.save(new Client(3, 46, "Melissa", "wikihtp"));

        Pageable pageRequest = PageRequest.of(0, 10, Sort.by("age").descending());

        Page<Client> clientPage = clientRepository.findByOrganization("wikihtp", pageRequest);

        List<Client> clientList = clientPage.getContent();
        int pageNum = clientPage.getNumber();
        long numOfClients = clientPage.getTotalElements();
        long totalNumOfPages = clientPage.getTotalPages();

        return ResponseEntity.ok(
                String.format("Clients: %s, \nCurrent page: %s out of %s, \nTotal entities: %s", 
                clientList, 
                pageNum, 
                totalNumOfPages, 
                numOfClients));
    }

Si echamos un vistazo a los registros, veremos cómo se activan las consultas de Hibernate y cómo se transpilan:

1
2
3
4
5
6
7
2021-08-13 23:16:06.701 DEBUG 4252 --- [nio-8080-exec-1] org.hibernate.SQL                        : select client0_.id as id1_0_0_, client0_.age as age2_0_0_, client0_.name as name3_0_0_, client0_.organinzation as organinz4_0_0_ from client client0_ where client0_.id=?
2021-08-13 23:16:06.727 DEBUG 4252 --- [nio-8080-exec-1] org.hibernate.SQL                        : insert into client (age, name, organinzation, id) values (?, ?, ?, ?)
2021-08-13 23:16:06.732 DEBUG 4252 --- [nio-8080-exec-1] org.hibernate.SQL                        : select client0_.id as id1_0_0_, client0_.age as age2_0_0_, client0_.name as name3_0_0_, client0_.organinzation as organinz4_0_0_ from client client0_ where client0_.id=?
2021-08-13 23:16:06.733 DEBUG 4252 --- [nio-8080-exec-1] org.hibernate.SQL                        : insert into client (age, name, organinzation, id) values (?, ?, ?, ?)
2021-08-13 23:16:06.734 DEBUG 4252 --- [nio-8080-exec-1] org.hibernate.SQL                        : select client0_.id as id1_0_0_, client0_.age as age2_0_0_, client0_.name as name3_0_0_, client0_.organinzation as organinz4_0_0_ from client client0_ where client0_.id=?
2021-08-13 23:16:06.735 DEBUG 4252 --- [nio-8080-exec-1] org.hibernate.SQL                        : insert into client (age, name, organinzation, id) values (?, ?, ?, ?)
2021-08-13 23:16:06.754 DEBUG 4252 --- [nio-8080-exec-1] org.hibernate.SQL                        : select client0_.id as id1_0_, client0_.age as age2_0_, client0_.name as name3_0_, client0_.organinzation as organinz4_0_ from client client0_ where client0_.organinzation=? order by client0_.age desc limit ?

Alcanzar el punto final REST daría como resultado:

1
2
3
Clients: [Client{id=3, age=46, name='Melissa', organization='wikihtp'}, Client{id=2, age=34, name='John', organization='wikihtp'}, Client{id=1, age=22, name='David', organization='wikihtp'}], 
Current page: 0 out of 1, 
Total entities: 3

Nota: También puede ordenar los datos en la consulta misma, sin embargo, en algunos casos, es más fácil simplemente usar el objeto “Ordenar” con entrada dinámica.

Conclusión

Los métodos de consulta derivados son una excelente función de refuerzo de consultas de Spring Data JPA, pero la simplicidad tiene un costo de escalabilidad. Aunque son flexibles, no son ideales para escalar a consultas complejas.

Aquí es donde entra en juego la anotación @Query de Spring Data JPA.

En esta guía, echamos un vistazo a la anotación @Query y cómo utilizarla en aplicaciones basadas en Spring para escribir consultas nativas y JPQL personalizadas para sus repositorios.

Hemos explorado las opciones de parametrización, así como también cómo paginar y ordenar sus datos. .