Salta el contingut

6. Spring Data MongoDB i API REST

Com sabem, el projecte Spring Data, inclòs a la plataforma Spring, proporciona un marc per simplificar l'accés i la persistència de dades en diferents repositoris d'informació. Dins d'aquest projecte hi ha Spring Data MongoDB, que proporciona integració amb bases de dades MongoDB, a través d'un model centrat en POJOs que interactuen amb col·leccions de documents i proporciona un repositori d'accés a dades.

En aquesta secció, i continuant amb la unitat anterior, abordarem el desenvolupament de components d'accés a dades a través de Spring Data, així com microserveis que ofereixen aquestes dades a través d'una API REST, tot això seguint el patró MVC que ja coneixem.

6.1. Definint el Model – Document

Una base de dades MongoDB està composta per col·leccions de Document. Tot i que aquests Documents poden tenir estructures diferents entre si o diferents tipus de dades, el model requereix una estructura estàtica. Així doncs, el primer que hem de fer és crear una classe que representi aquest Document Principal per a MongoDB, que serà el que es retornarà per les consultes que es facin.

En aquest context, hi ha dues anotacions principals que utilitzarem:

  • @Document → per indicar que una classe correspon a un objecte de domini (domain object) que es pot mapar a la base de dades per oferir persistència. Aquesta anotació per a MongoDB seria l'equivalent a @Entity en JPA. Si no s'indica res, el nom de la col·lecció que s'utilitzarà s'interpretarà com a corresponent al nom de la classe en minúscules. Així, si tenim la classe com.jaume.ad.Person, s'utilitzarà la col·lecció person. No obstant això, podem indicar la col·lecció amb la qual estem treballant, ja sigui a través dels atributs value o collection, amb la següent sintaxi:
    • @Document(value="collection")
    • @Document("collection")
    • @Document(collection="collection")
  • @Id S'aplica a un camp, i s'utilitza per indicar que el camp s'utilitzarà com a identificador. Com sabem, cada document a MongoDB requereix un identificador. Si no se'n proporciona cap, el controlador assignarà un ObjectID automàticament. És important tenir en compte que els tipus de dades que podem utilitzar com a identificadors poden ser tant Strings com BigInteger, ja que Spring s'encarregarà de convertir-los al tipus ObjectID.

Important

Text Only
1
Existeix una anotació `@DocumentReference` per relacionar Documents un dins d'altres, per exemple quan emmagatzemem en una classe objectes d'altres classes, com les relacions en bases de dades SQL.

A més d'aquestes, hi ha altres anotacions més específiques que podem utilitzar. Si ho desitgeu, podeu consultar-les a la documentació de referència de Spring Data MongoDB aquí.

6.2. Definint el Repositori

Com sabem, el repositori és la interfície encarregada de gestionar l'accés a les dades. En el cas de MongoDB, aquest derivarà de MongoRepository, que serà una interfície parametritzada per dos arguments:

  • MongoRepository<T, Id>, on:
    • T → El tipus de document, que correspondrà a la classe definida al model, i
    • Id→ El tipus de dada al qual pertanyerà l'identificador.

La interfície MongoRepository, com hem dit, serà específica per a MongoDB, i derivarà de les interfícies CrudRepository i PagingAndSortingRepository, de les quals heretarà tots els seus mètodes. D'aquesta manera, al repositori només haurem de declarar aquells mètodes que siguin més específics per a la nostra aplicació, ja que tots els mètodes per implementar operacions CRUD, així com findAll() i findById() seran heretats de MongoRepository.

Per definir les nostres pròpies consultes al repositori, utilitzarem l'anotació @Query, proporcionant la consulta en qüestió com a valor:

Java
@Query(value="{ parameterized_query}")   // respecte a la classe base del repositori
List<DocumentType> methodName(list_parameters);

Per subministrar paràmetres a la consulta, aquests es reben com a arguments del mètode, i es referencien pel seu ordre a la consulta: ?0 per al primer argument, ?1 per al segon, etc. Potser en versions més noves es poden utilitzar paràmetres de manera nominal, com :parameter_name.

6.3. Definint el servei

Els serveis s'encarreguen de la capa de negoci de la nostra aplicació, i accedeixen a les dades a través del repositori, enviant els resultats al controlador. Aquests serveis, en general, es caracteritzen per:

  • Utilitzar les anotacions @Service, per indicar a Spring que s'està implementant un servei
  • D'una banda, es defineix la interfície del Servei i, d'altra banda, es realitza la implementació a través de la classe ServiceImpl.
  • S'utilitza l'anotació @Autowired en referències a repositoris per enllaçar o injectar el servei en qüestió amb aquest repositori.
  • Un cop obté les dades del repositori, les envia al controlador.

6.4. Definint el controlador

Finalment, ens queda la implementació del controlador, que ja coneixem de Spring. Recordem les característiques principals d'aquest:

  • Utilitzar l'anotació @RestController a nivell de classe per indicar que es tracta d'un controlador REST
  • Utilitzar l'anotació @RequestMapping a nivell de classe per especificar el camí base per als punts finals del servei,
  • Utilitzar l'anotació @Autowired a les propietats que fan referència al servei, per injectar-lo automàticament,
  • Utilitzar les anotacions @GetMapping, @PostMapping, @PutMapping, @DeleteMapping als mètodes que implementaran sol·licituds de tipus GET, POST, PUT o DELETE, especificant el seu Endpoint.
  • Utilitzar les anotacions @PathVariable o @RequestParam o @RequestBody als arguments dels mètodes anteriors per obtenir els valors del camí, sol·licitud o cos.

2. Swagger

Fins ara, estem provant la nostra API REST amb Postman, però ara oferim una eina que s'integra amb Spring i Tomcat per provar la nostra API de manera ràpida i senzilla.

Swagger (https://swagger.io) és molt senzill de començar:

  1. Afegir dependències.
  2. Afegir una classe de configuració, que escaneja els nostres controladors per trobar quins punts d'entrada estan definits.
  3. Crear automàticament una interfície d'usuari, que mostra quins punts d'entrada estan definits i eines per provar-los.

2.1. Dependència

Has d'afegir al teu pom.xml:

XML
1
2
3
4
5
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>

2.2. Classe de configuració

Necessitem crear una classe com aquesta:

Java
package com.jaumeii.moviesapi.swagger;

import java.util.Collections;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import springfox.documentation.service.Contact;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;

import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket apiDocket() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.jaumeii.moviesapi.controller"))
                .paths(PathSelectors.any())
                .build()
                .apiInfo(getApiInfo());
    }

    private ApiInfo getApiInfo() {
        return new ApiInfo(
                "Movies controller",
                "Eines per provar l'API de pel·lícules",
                "1.0",
                "http://www.ieseljust.com",
                new Contact("admin", "https://ieseljust.com", "admin@ieseljust.com"),
                "LICENSE",
                "LICENSE URL",
                Collections.emptyList()
                );
    }
}

Cal implementar dos mètodes:

  • Docket apiDocket() → que crea un Docket. Aquest Docket conté referències per treballar i provar la nostra API
  • ApiInfo getApiInfo() → que crea un ApiInfo, i com el seu nom indica, és informació bàsica sobre per a què s'ha creat aquesta API.

Atenció

Text Only
1
En l'exemple tenim els nostres controladors definits a `com.jaumeii.moviesapi.controller`, i és tot el que podríem fer.

2.3. Proves

Quan el nostre projecte Spring s'inicia, podem veure la informació del registre:

Text Only
2023-01-29 08:37:16.400  INFO 61951 --- [  restartedMain] pertySourcedRequestMappingHandlerMapping : Mapped URL path [/v2/api-docs] onto method [springfox.documentation.swagger2.web.Swagger2Controller#getDocumentation(String, HttpServletRequest)]

Això significa que tenim un nou punt d'entrada /v2/api-docs. Si provem aquest punt d'entrada, obtenim alguna cosa així:

JavaScript
    "swagger": "2.0",
    "info": {
        "description": "Eines per provar l'API de pel·lícules",
        "version": "1.0",
        "title": "Movies controller",
        "termsOfService": "http://www.ieseljust.com",
        "contact": {
            "name": "admin",
            "url": "https://ieseljust.com",
            "email": "admin@ieseljust.com"
        },
        "license": {
            "name": "LICENSE",
            "url": "LICENSE URL"
        }
    },
    "host": "localhost:8080",
    "basePath": "/",
    "tags": [
        {
            "name": "movie-controller",
            "description": "Movie Controller"
        }
    ],
    "paths": {
        "/api/v1/movie/byNumDirectors/{how_many}": {
            "get": {
                "tags": [
                    "movie-controller"
                ],
                "summary": "findByYear",
                "operationId": "findByYearUsingGET",
                "produces": [
                    "*/*"
    ...

És un document JSON que descriu la nostra API. Però per treballar de manera còmoda, podem sol·licitar /swagger-ui.html, i obtenim:

![swagger1](./img/Swagger-1.png){width=75%}

Aquesta pàgina d'interfície d'usuari conté la informació bàsica que hem establert al mètode getApiInfo(). A la part inferior podem veure més elements. Si obrim, obtenim:

![swagger2](./img/Swagger-2.png){width=75%}

Podem veure informació sobre els punts d'entrada i els models que retorna l'API. A més, podem entrar a cada punt d'entrada per obtenir informació sobre els paràmetres, i finalment executar-lo.

3. Apèndix. Solució a l'exercici sobre consultes mongo

  1. Obteniu totes les produccions que es van estrenar el 2015 o són del tipus series.

Consell

Text Only
1
Necessitem utilitzar l'operador `$or`, amb una matriu de condicions.
JavaScript
db.movies.find({$or:[{year:2015}, {type:"series"}]})
  1. Obteniu totes les pel·lícules NO estrenades entre els anys 2000 i 2002.

Consell

Text Only
1
Necessitem utilitzar l'operador `$not`, amb una condició per negar.
JavaScript
db.movies.find({year:{$not: {$gte: 2000, $lte: 2002}}}).pretty();
  1. Obteniu totes les pel·lícules per a les quals la clau "directors" no està definida.

Consell

Text Only
1
Necessitem utilitzar l'operador `$exists` o comprovar un camp `null`.
JavaScript
1
2
3
4
5
db.movies.find({directors:{$exists:false}})

or

db.movies.find({directors:null})
  1. Obteniu el títol de totes les pel·lícules que comencen amb la cadena star wars, independentment de majúscules.

Consell

Text Only
1
És interessant utilitzar expressions regulars. Recordeu utilitzar l'opció `i` per incloure una comprovació no sensible a majúscules.
JavaScript
1
2
3
4
5
db.movies.find({"title":/^star wars/i},{title:1})

or

db.movies.find({"title": {$regex:'^star wars', $options:'i'}},{title:1})
  1. Obteniu el títol de totes les pel·lícules que contenen el gènere comèdia (Comedy).

Consell

Text Only
1
Busquem a la matriu `genres` un gènere donat.
JavaScript
db.movies.find({genres:"Comedy"}, {genres:1})
  1. Mostreu el títol i els gèneres de les pel·lícules que contenen el gènere comèdia (Comedy) o aventura (Adventure).

Consell

Text Only
1
Necessitem provar que la pel·lícula compleix `$all` els gèneres donats.
JavaScript
1
2
3
db.movies.find(
    {genres:{ $all: ["Comedy", "Adventure"]}},
    {title:1, genres:1})
  1. Obteniu el títol i els gèneres de les pel·lícules que tenen tres gèneres.

Consell

Text Only
1
Busquem una matriu `genres` amb tres gèneres: operador `$size`.
JavaScript
db.movies.find({ genres : {$size:3} }, {title: 1, genres:1})
  1. Obteniu les pel·lícules amb una qualificació de Rotten Tomatoes superior a 4.

Consell

Text Only
1
Necessitem comprovar dins de documents incrustats, mitjançant notació de punts dins d'una cadena.
JavaScript
db.movies.find({"tomatoes.viewer.rating":{$gt:4}})
  1. Feu la mateixa consulta que abans, però limitant el nombre de documents a 10.

Consell

Text Only
1
Necessitem `limitar` els resultats.
JavaScript
db.movies.find({"tomatoes.viewer.rating":{$gt:4}}).limit(10)
  1. Ara mostreu el títol i la qualificació d'aquestes pel·lícules amb una qualificació superior a 4, ordenades per qualificació (de més alta a més baixa) i limitant els resultats a 10.

Consell

Text Only
1
I ara afegint un filtre d'ordenació.
JavaScript
1
2
3
4
5
db.movies.find(
        {"tomatoes.viewer.rating":{$gt:4}},
        {title:1, "tomatoes.viewer.rating":1})
        .sort({"tomatoes.viewer.rating":-1})
        .limit(10)