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

¡En este tutorial práctico detallado, aprenda todo lo que necesita saber sobre la anotación @Query con Spring Data MongoDB y Spring Boot!

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
4
@Repository
public interface BookRepository extends MongoRepository<Book, String> {
   List<Book> findByAuthor(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 MongoRepository para una clase Book, que tiene un atributo llamado author de tipo String.

{.icon aria-hidden=“true”}

Recordatorio: MongoRepository es solo un PagingAndSortingRepository especializado adecuado para Mongo, que a su vez es un CrudRepository especializado.

En lugar de implementar este método en un servicio que implementa BookRepository, 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 Libro, con un autor coincidente.

Una vez que se llama al método con alguna entrada, se realiza la siguiente solicitud:

1
2
3
4
5
find using query: 
{ "author" : "Max Tegmark"} 
fields: Document{{}} 
for class: 
class com.example.demo.Book in collection: books

{.icon aria-hidden=“true”}

Nota: Para ver este resultado, deberá establecer el nivel de depuración de MongoTemplate en DEBUG.

1
logging.level.org.springframework.data.mongodb.core.MongoTemplate=DEBUG

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 MongoRepository<Property, String> {
   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 de MongoRepository y pertenecen a un único método. El idioma utilizado dentro de la anotación depende de su back-end. Naturalmente, para los back-ends de Mongo, escribirá consultas nativas de Mongo, aunque @Query también es compatible con bases de datos relacionales y acepta consultas nativas para ellas, o el neutral JPQL (Lenguaje de consulta de persistencia de Java) eso se traduce automáticamente a las consultas nativas de la base de datos que está utilizando.

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

{.icon aria-hidden=“true”}

Nota: Esta guía cubrirá Spring Data JPA junto con una base de datos Mongo y utilizará consultas aplicables a MongoDB.

If you'd like to read more about writing relational/SQL queries, read our Spring Data JPA - Guía para la anotación @Query!

Modelo de dominio y repositorio

Definamos rápidamente un modelo de Libro que usaremos como un @Documento para nuestro repositorio. Para mostrar correctamente varias operaciones, como usar los operadores lt y gt de Mongo, tendremos un par de propiedades diferentes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Document(collection = "books")
public class Book {

    @Id
    private String id;
    private String name;
    private String author;
    private long pageNumber;
    private long publishedYear;

    // Getters, setters, constructor, toString()
}

MongoDB trata con ID de tipo String u ObjectId. Depende de usted elegir cuál usará, y ObjectId se puede convertir fácilmente a cadenas y viceversa, por lo que no hace mucha diferencia.

En cualquier caso, definamos un BookRepository simple para este modelo:

1
2
public interface BookRepository extends MongoRepository<Book, String> {
}

Actualmente está vacío, pero funciona bien para las operaciones CRUD, dado que MongoRepository es un descendiente de la interfaz CrudRepository. ¡Además, la paginación y la clasificación son compatibles desde el primer momento!

En las próximas secciones, echaremos un vistazo a la anotación @Query en sí misma, así como a la estructura de consulta de MongoDB, cómo hacer referencia a los parámetros del método, así como ordenar y paginar.

Comprender la anotación @Query

La anotación @Query es bastante simple y directa:

1
2
@Query("mongo query")
public List<Book> findBy(String param1, String param2);

Una vez que se llama al método findBy(), se devuelven los resultados. Tenga en cuenta que durante el tiempo de compilación, Spring Boot no sabe de antemano qué tipo devolverá la consulta. Por ejemplo, si devuelve varios resultados y solo tiene un único valor de retorno esperado, se lanzará una excepción durante tiempo de ejecución.

Depende de usted asegurarse de que la respuesta de la consulta coincida con el tipo de devolución del método.

Puede tener consultas fijas o dinámicas aquí. Por ejemplo, puede simplificar el nombre del método anterior y delegar los parámetros desordenados a la anotación @Query:

1
2
3
4
5
@Query("query with param1, param2, param3")
List<Book> findAllActive();

@Query("query with param1, param2, param3")
List<Book> findBy(param1, param2, param3);

En el primer ejemplo, tenemos un conjunto fijo de parámetros, como buscar siempre libros activos incluso si el cliente no lo especifica. Esta es una ventaja sobre los métodos de consulta derivados ya que el método nombre está limpio. Alternativamente, puede proporcionar parámetros a los métodos que luego se pueden inyectar en la anotación @Query:

1
2
3
4
5
6
7
8
@Query("{'active':true}")
List<Book> findAll();

@Query("{'author' : ?0, 'category' : ?1}")
List<Book> findPositionalParameters(String author, String category);

@Query("{'author' : :#{#author}, 'category' : :#{#category}}")
List<Book> findNamedParameters(@Param("author") String author, @Param("category") String category);

Para aquellos que no estén completamente familiarizados con la estructura de consulta de MongoDB, ¡echemos un vistazo a ellos antes de profundizar en la extracción de parámetros de método y usarlos en consultas!

Estructura de consulta de MongoDB

Sin embargo, MongoDB tiene una estructura de consulta bastante sencilla, diferente de las estructuras SQL. Si no ha usado MongoDB antes y está acostumbrado a las bases de datos relacionales, es una buena idea refrescar su memoria en estas estructuras.

Todas las consultas de Mongo tienen lugar entre corchetes:

1
{query}

La condición de igualdad estándar sigue un patrón simple:

1
2
3
4
5
{ 
<field1> : <value1>, 
<field2> : <value2>, 
... 
}

Por ejemplo, podemos consultar nuestros libros como:

1
2
3
4
5
{ 
author : 'Max Tegmark', 
pageNumber : 568, 
... 
}

Esta consulta busca todos los documentos de Libro en la colección, que se ajusten tanto al autor como al Número de página`. También puede incluir operadores en la mezcla aquí:

1
2
3
4
5
6
{
author : 
    {
        $in : ['Max Tegmark', 'Ray Kurzweil']
    }
}

Esta consulta comprueba si el autor es cualquiera de los valores proporcionados. Algunos de los operadores soportados son $gt, $lt, $in, $nin, $or, $and, $nor, ​​$not y $size , aunque hay bastantes [unas cuantas y vale la pena conocerlas]{rel=“nofollow noopener noreferrer” target="_blank"}. Por ejemplo, aquí hay una consulta que busca todos los documentos de cualquiera de dos autores, con un recuento de páginas entre 400 y 500, no publicados en 2018 y 2019:

1
2
3
4
5
{
author : { $in : ['Max Tegmark', 'Ray Kurzweil']},
pageNumber : { $gt : 400, $lt : 500},
publishedYear : {$nin : [2018, 2019]}
}

Esta es la mayor parte del conocimiento de consultas que necesitará para una gran cantidad de consultas, pero no deje de conocer MongoDB antes de trabajar seriamente con él. Además, es probable que desee trabajar también con agregaciones.

Si desea leer más sobre agregaciones con MongoDB, lea nuestra Spring Data MongoDB - Guía para @Aggregation Annotation

Referencia a parámetros de método posicional y con nombre {#referencia a parámetros de método posicional y con nombre}

Con el conocimiento funcional de MongoDB en nuestro haber, echemos un vistazo a cómo podemos hacer referencia a los parámetros del método. Puede hacer referencia a ellos a través de sus nombres, combinados con la anotación @Param y las expresiones SpEL, que son más detalladas pero más flexibles, o, a través de argumentos posicionales, que suele ser el enfoque preferido debido a la simplicidad:

1
2
3
4
5
@Query("{'author' : ?0, 'category' : ?1}")
List<Book> findPositionalParameters(String author, String category);

@Query("{'author' : :#{#author}, 'category' : :#{#category}}")
List<Book> findNamedParameters(@Param("author") String author, @Param("category") String category);

En el primer enfoque, el primer argumento posicional, ?0, corresponde al primer argumento del método, y se usará el valor del argumento en lugar de ?0. Esto significa que debe realizar un seguimiento de las posiciones y no mezclarlas, de lo contrario, MongoDB fallará silenciosamente y simplemente no devolverá los resultados, dada la flexibilidad del esquema, ya que también podría tener esa propiedad.

{.icon aria-hidden=“true”}

Consejo: Si ha activado DEBUG como su nivel de registro, podrá ver la consulta que se envió a Mongo en los registros. Puede copiar y pegar esa consulta en MongoDB Atlas para verificar si la consulta devuelve los resultados correctos allí y verificar si accidentalmente arruinó las posiciones. Lo más probable es que su consulta esté bien, pero acaba de mezclar las posiciones, por lo que el resultado está vacío.

En el segundo enfoque, usamos expresiones SpEL para hacer coincidir los parámetros provistos con los parámetros @Query. No es necesario que los defina en ningún orden en particular, ya que coincidirán por nombre, no por posición. Sin embargo, todavía tiene sentido mantener una posición uniforme para la legibilidad de la API.

Definamos un punto final simple en un controlador REST para probar este método:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@RestController
public class HomeController {
    @Autowired
    private BookRepository bookRepository;

    @GetMapping("/find")
    public ResponseEntity main() {
        return ResponseEntity.
                ok(bookRepository.findPositionalParameters("Ray Kurzweil", "Fiction"));
    }
}

Una vez configurado, enviemos una solicitud curl (o naveguemos a esta URL a través del navegador):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ curl localhost:8080/find

[ {
  "id" : "613fba633150f9788cd1858f",
  "name" : "Danielle: Chronicles of a Superheroine",
  "author" : "Ray Kurzweil",
  "pageNumber" : 472,
  "publishedYear" : 2019,
  "category" : "Fiction"

}                 

{.icon aria-hidden=“true”}

Nota: Para obtener una respuesta con letra bonita, recuerde cambiar INDENT_OUTPUT de Jackson a true en su application.properties.

1
spring.jackson.serialization.INDENT_OUTPUT=true

Resultados de paginación con Page y Pageable

La ordenación y la paginación son compatibles desde el primer momento, ya que MongoRepository amplía PagingAndSortingRepository. Como de costumbre, el proceso es devolver un tipo Página y proporcionar un Paginable al método en sí:

1
2
@Query("{'author' : ?0}")
Page<Book> findBy(String author, Pageable pageable);

Al llamar al método, debe proporcionar un objeto ‘Pageable’ válido, que se puede crear haciendo una solicitud de página:

1
2
3
4
5
6
7
8
@GetMapping("/find")
public ResponseEntity main() {
    //                  PageRequest.of(page, size)
    Pageable pageable = PageRequest.of(0, 2);

    return ResponseEntity.
            ok(bookRepository.findBy("Ray Kurzweil", pageable));
}

Aquí, estamos creando una PageRequest para la primera página (indexación basada en 0) con un tamaño de 2 documentos. Si hay 10 documentos adecuados en la base de datos, se devolverán 5 páginas, con un rango de 0..4. tienes que crear

Imprimamos todo el objeto Página que regresó, donde el contenido contiene los resultados de la consulta, y varias otras propiedades también están presentes relacionadas con la página. Aquí es donde puede ver cómo se organizan los resultados dentro de la página, es decir, la clasificación, el tamaño de página, el número de página, etc.:

 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$ curl localhost:8080/find

{
  "content" : [ {
    "id" : "613fb60a3150f9788cd18589",
    "name" : "The Singularity Is Near",
    "author" : "Ray Kurzweil",
    "pageNumber" : 652,
    "publishedYear" : 2005,
    "category" : "Popular Science"
  }, {
    "id" : "613fba633150f9788cd1858f",
    "name" : "Danielle: Chronicles of a Superheroine",
    "author" : "Ray Kurzweil",
    "pageNumber" : 472,
    "publishedYear" : 2019,
    "category" : "Fiction"
  } ],
  "pageable" : {
    "sort" : {
      "sorted" : false,
      "unsorted" : true,
      "empty" : true
    },
    "offset" : 0,
    "pageNumber" : 0,
    "pageSize" : 2,
    "unpaged" : false,
    "paged" : true
  },
  "last" : true,
  "totalPages" : 1,
  "totalElements" : 2,
  "size" : 2,
  "number" : 0,
  "sort" : {
    "sorted" : false,
    "unsorted" : true,
    "empty" : true
  },
  "numberOfElements" : 2,
  "first" : true,
  "empty" : false
}        

Si solo desea mostrar los resultados, puede acceder a Stream<Book> de datos y collect() en una lista:

1
2
3
4
5
6
7
8
9
@GetMapping("/find")
public ResponseEntity main() {
    //                  PageRequest.of(page, size)
    Pageable pageable = PageRequest.of(0, 2);
    return ResponseEntity.
            ok(bookRepository.findBy("Ray Kurzweil", pageable)
                    .get()
                    .collect(Collectors.toList()));
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 curl localhost:8080/find
[ {
  "id" : "613fb60a3150f9788cd18589",
  "name" : "The Singularity Is Near",
  "author" : "Ray Kurzweil",
  "pageNumber" : 652,
  "publishedYear" : "2005-08-31T22:00:00Z",
  "category" : "Popular Science"
}, {
  "id" : "613fba633150f9788cd1858f",
  "name" : "Danielle: Chronicles of a Superheroine",
  "author" : "Ray Kurzweil",
  "pageNumber" : 472,
  "publishedYear" : 2019,
  "category" : "Fiction"
} ]

Paginación con clasificación

Para ampliar esta funcionalidad con la clasificación, todo lo que tiene que hacer es proporcionar un objeto Sort a PageRequest, indicando por qué propiedad le gustaría ordenar y en qué orden:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@GetMapping("/find")
 public ResponseEntity main() {
    Pageable pageable = PageRequest.of(0, 3,
                                       Sort.by("name").ascending()
                                       .and(Sort.by("pageNumber").ascending()));

    return ResponseEntity.
            ok(bookRepository.findAll(pageable)
                    .get()
                    .collect(Collectors.toList()));
}

Aquí, hemos ordenado los resultados por nombre ascendente y número de página ascendente. ¡Al ordenar a través de múltiples propiedades, puede encadenar cualquier número de propiedades a través de and() y proporcionando otro Sort.by()!

El método findAll() es un método predeterminado presente en la interfaz MongoRepository y acepta instancias Sort y Pageable, y también se puede ejecutar sin ellas. Aquí, hemos aprovechado eso para consultar usando el nuevo Pageable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ curl localhost:8080/find

[ {
  "id" : "613fba633150f9788cd1858f",
  "name" : "Danielle: Chronicles of a Superheroine",
  "author" : "Ray Kurzweil",
  "pageNumber" : 472,
  "publishedYear" : 2019,
  "category" : "Fiction"
}, {
  "id" : "613fb6933150f9788cd1858e",
  "name" : "Our Mathematical Universe",
  "author" : "Max Tegmark",
  "pageNumber" : 432,
  "publishedYear" : 2014,
  "category" : "Popular Science"
}, {
  "id" : "613fb60a3150f9788cd18589",
  "name" : "The Singularity Is Near",
  "author" : "Ray Kurzweil",
  "pageNumber" : 652,
  "publishedYear" : 2005,
  "category" : "Popular Science"
} ]

¡La primera propiedad tiene prioridad aquí! Aunque el segundo libro tiene menos páginas que el primero, y hemos ordenado por número de página ascendente, ordenar por nombre da como resultado este orden. Si el orden por nombre fuera ambiguo, la segunda propiedad haría el corte.

Consultas con Operadores

Dicho todo esto, volvamos a crear la consulta desde el principio del artículo:

1
2
3
4
5
{
author : { $in : ['Max Tegmark', 'Ray Kurzweil']},
pageNumber : { $gt : 400, $lt : 500},
publishedYear : {$nin : [2018, 2019]}
}

¡Esto es tan fácil como copiar y pegar esta consulta en la anotación @Query! Sabiendo que tenemos tres libros, y que uno tiene 652 páginas, y que uno de ellos se publicó en 2019, deberíamos esperar que solo se devuelva un libro aquí: "Nuestro universo matemático" de Max Tegmark!

Probemos si eso es cierto:

1
2
3
4
5
6
@Query("{\n" +
        "author : { $in : ?0},\n" +
        "pageNumber : { $gt : ?1, $lt : ?2},\n" +
        "publishedYear : {$nin : ?3}\n" +
        "}")
List<Book> findBy(String[] authors, int pageNumLower, int pageNumUpper, int[] excludeYears);

O, para una implementación más limpia:

1
2
@Query("{'author' : { $in : ?0}, 'pageNumber' : { $gt : ?1, $lt : ?2},'publishedYear' : {$nin : ?3}}")
List<Book> findBy(String[] authors, int pageNumLower, int pageNumUpper, int[] excludeYears);

{.icon aria-hidden=“true”}

Nota: Al proporcionar matrices de datos, como autores y excluir años, no es necesario definir los parámetros como matrices en la consulta: [?0]. Esto crearía una matriz dentro de una matriz. La anotación @Query convertirá automáticamente su entrada en la consulta correcta.

Actualicemos el punto final y proporcionemos algunos datos:

1
2
3
4
5
6
7
8
9
@GetMapping("/find")
public ResponseEntity main() {
    return ResponseEntity.
            ok(bookRepository.findBy(
                    new String[]{"Ray Kurzweil", "Max Tegmark"}, // Authors
                    400,                                         // Lower pageNumber bound
                    500,                                         // Upper pageNumber bound
                    new int[]{2018, 2019}));                     // Exclusion years
    }

Y cuando le enviamos una solicitud:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ curl localhost:8080/find

[{
  "id" : "613fb6933150f9788cd1858e",
  "name" : "Our Mathematical Universe",
  "author" : "Max Tegmark",
  "pageNumber" : 432,
  "publishedYear" : 2014,
  "category" : "Popular Science"
}]

Como un reloj.

Conclusión

En esta guía, hemos echado un vistazo a la anotación @Query en el contexto de Spring Data MongoDB.

La anotación le permite definir sus propias consultas, nativas y JPQL, para varias bases de datos, relacionales y no relacionales. Hemos optado por usar consultas Mongo nativas para interactuar con una base de datos no relacional. Después de definir un modelo y un repositorio para él, exploramos la estructura de consulta utilizada por MongoDB y cómo funciona la anotación @Query en general. A esto le siguió la referencia a parámetros de métodos posicionales y con nombre, la paginación y clasificación de los resultados de las consultas, así como también cómo usar los operadores de MongoDB para construir consultas más complejas.