GraphQL: una alternativa a REST (2/2)

joaquinsiabra

Joaquin Siabra

Posted on May 14, 2020

GraphQL: una alternativa a REST (2/2)

En el anterior artículo sobre GraphQL repasamos las principales características de este lenguaje de consultas como alternativa a REST. En esta segunda parte vamos a implementar un servidor GraphQL mediante Spring Boot.

GraphQL mediante Spring Boot

Vamos a empezar creando el proyecto Spring DemoGraphQL, mediante Spring Initializr o con la opción correspondiente de nuestro IDE.

Preparación del proyecto

Spring Boot da soporte a la librería graphql-java mediante el iniciador graphql-spring-boot-starter, que configura un servlet GraphQL en /graphql (que atiende consultas GET y POST) y usa una librería de gestión de esquemas GraphQL (por ejemplo, GraphQL Java Tools) para parsear los ficheros de esquema que existan en el classpath.

Por tanto, en nuestro pom incluiremos:

  • Spring Boot Web: para la gestión de los puntos de acceso (endpoints) de nuestra aplicación mediante protocolos web.
  • GraphQL-Java Spring boot starter para GraphQL: configura y sirve el punto de acceso /graphql en el contexto de Spring
  • GraphQL-Java Tools: gestiona la lectura y el tratamiento de los esquemas GraphQL.
  • Spring Boot DevTools: herramientas para desarrollo y depuración
  • GraphQL-Java Spring Boot Starter para GraphIQL: proporciona una web para interactuar con el servicio publicado en /graphql.

Añadimos además las referencias para JPA y la base de datos en memoria H2
El pom del proyecto quedará así:


<parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>2.0.2.RELEASE</version>
                <relativePath/> <!-- lookup parent from repository -->
                </parent>

                <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
                <java.version>8</java.version>
                </properties>

                <dependencies>

                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                </dependency>

                <dependency>
                        <groupId>com.graphql-java</groupId>
                        <artifactId>graphql-spring-boot-starter</artifactId>
                        <version>4.0.0</version>
                </dependency>

                <dependency>
                        <groupId>com.graphql-java</groupId>
                        <artifactId>graphql-java-tools</artifactId>
                        <version>4.3.0</version>
                </dependency>

                <dependency>
                        <groupId>com.graphql-java</groupId>
                        <artifactId>graphiql-spring-boot-starter</artifactId>
                        <version>3.10.0</version>
                </dependency>

                <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        </dependency>

                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-data-jpa</artifactId>
                </dependency>
                <dependency>
                        <groupId>com.h2database</groupId>
                        <artifactId>h2</artifactId>
                        <scope>runtime</scope>
                </dependency>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>

                </dependencies>
Enter fullscreen mode Exit fullscreen mode

Instalamos las dependencias del proyecto:

mvn install

y ejecutamos la clase principal, en nuestro caso com.example.DemoGraphQL, mediante el IDE o en línea de comandos con mvnw spring-boot:run. Como hemos indicado, por defecto la aplicación se publica en el contexto /graphql. Este contexto (junto con otras propiedades) lo podemos modificar en el fichero application.properties.

Por ejemplo, para publicar la aplicación en /miGraphql:

graphql:
  servlet:
    mapping: /miGraphql
    enabled: true
    corsEnabled: true
Enter fullscreen mode Exit fullscreen mode

Una vez arrancado el servidor, dispondremos en http://localhost:8080/h2-console/login.jsp de una consola de la base de datos en memoria H2.

La información de acceso es:

JDBC URL: jdbc:h2:mem:testdb
User Name: sa
Password: (vacía)

Alt Text

Definición del esquema GraphQL

Ya tenemos nuestro servidor GraphQL arrancado en http://localhost:8080/graphql.

Nos falta definir nuestro esquema e implementarlo en consecuencia. Dicho esquema se guarda en uno o varios ficheros .graphqls dentro del classpath. Vamos a crear una carpeta graphql en src/main/resources/ y, dentro de ella, los ficheros heroe.graphqls, ciudad.graphqls y divinidad.graphqls.

Alt Text

Aunque, por claridad, podemos separar el esquema en varios ficheros, hay que tener en mente que solo puede existir un tipo Consulta (Query) y un tipo Modificador (Mutation) principales para nuestro servidor. Así que usualmente definimos el tipo Query en uno de los ficheros, y los restantes ficheros de esquema lo extenderán.

Empecemos con el tipo Héroe y las consultas o los modificadores que implementaremos sobre este tipo, que se recogerán en el fichero heroe.graphqls:

//esquema

type Heroe {
  id: ID!
  nombre: String!
  apellido: String!
  Posesiones: [Ciudad]!
}

type Query {
  heroes: [Heroe]!
  Héroe(nombre: String): Heroe
  countHeroes: Long!
}

type Mutation {
    newHeroe(nombre: String!, apellido: String!) : Heroe!
}
Enter fullscreen mode Exit fullscreen mode

Hacemos lo correspondiente para Ciudad en el fichero ciudad.graphqls, extendiendo los tipos Query y Mutation:

//esquema

type Ciudad {
    id: ID!
    nombre: String!
    fundador: Heroe
    rey: Heroe
    divinidad: Divinidad
}

extend type Query {
    ciudades: [Ciudad]!
    ciudad(nombre: String): Ciudad
    ciudadesRey(nombre: String): [Ciudad]!
}

extend type Mutation {
    deleteCiudad(id: ID!) : Boolean
}
Enter fullscreen mode Exit fullscreen mode

Y para Divinidad:

//esquema

type Divinidad {
    id: ID!
    nombre: String!
    epiteto: String!
}

extend type Query {
    divinidades: [Divinidad]!
}

input DivinidadInput{
    nombre: String!
    epiteto: String!
}

extend type Mutation {
    newDivinidad(divinidad: DivinidadInput!) : Divinidad!
}
Enter fullscreen mode Exit fullscreen mode

En este caso hemos usado una estructura de entrada Input para simplificar los parámetros del modificador newDivinidad.

Todas estas operaciones se reúnen durante el tiempo de ejecución en el tipo Query, equivaliendo el resultado a:

type Query {
   heroes: [Heroe]!
   Héroe(nombre: String): Heroe
   countHeroes: Long!
   ciudades: [Ciudad]!
   ciudad(nombre: String): Ciudad
   ciudadesRey(nombre: String): [Ciudad]!
}
Enter fullscreen mode Exit fullscreen mode

Y de modo similar con el tipo Mutation:

type Mutation {
     newHeroe(nombre: String!, apellido: String!) : Heroe!
    deleteCiudad(id: ID!) : Boolean
    newDivinidad(divinidad: DivinidadInput!) : Divinidad!
}
Enter fullscreen mode Exit fullscreen mode

Con respecto a los tipos disponibles, graphql-java nos permite usar:

  • Escalares: String, Boolean, Int, Float, ID, Long, Short, Byte, Float, BigDecimal, BigInteger
  • Objetos como los que acabamos de definir: Héroe, Ciudad, o Divinidad
  • Uniones, Enumerados e incluso Interfaces. Por ejemplo:
//esquema

interface Persona {
  nombre: String!,
  apellido: String!
}
type Heroe implements Persona { }
type Rey implements Persona { }
type Query {
  buscaPorNombre(nombre: String!): [Persona]!
}
Enter fullscreen mode Exit fullscreen mode

Clases Java de las entidades

Definidos los tipos en nuestro esquema, la librería GraphQL Java Tools los convertirá en las correspondientes clases Java que hayamos definido. Vamos, pues, a ello.

En el paquete model creamos las clases Heroe, Ciudad y Divinidad como entidades persistentes.

Por ejemplo, la clase Heroe tendría esta forma (el código para las otras dos entidades puede consultarse en GitHub):


//Java

import java.util.Set;
import javax.persistence.*;

@Entity
public class Heroe {
                @Id
                @Column(name = "heroe_id", nullable = false)
                @GeneratedValue(strategy = GenerationType.AUTO)
                private Long id;

                @Column(name = "heroe_nombre", nullable = false)
                private String nombre;

                @Column(name = "heroe_apellido", nullable = false)
                private String apellido;

                @OneToMany(fetch = FetchType.EAGER,mappedBy="rey")
                private Set posesiones;

                public Heroe() {
                }

                public Heroe(Long id) {
                                    this.id = id;
                }

                public Heroe(String firstName, String lastName) {
                                    this.nombre = firstName;
                                    this.apellido = lastName;
                }

                public Long getId() {
                                    return id;
                }

                public void setId(Long id) {
                                    this.id = id;
                }

                public String getNombre() {
                                    return nombre;
                }

                public void setNombre(String nombre) {
                                    this.nombre = nombre;
                }

                public String getApellido() {
                                    return apellido;
                }

                public void setApellido(String apellido) {
                                    this.apellido = apellido;
                }

                public Set getPosesiones() {
                                    return posesiones;
                }

                public void setPosesiones(Set posesiones) {
                                    this.posesiones = posesiones;
                }

                @Override
                public boolean equals(Object o) {
                                    if (this == o)
                                                        return true;
                                    if (o == null || getClass() != o.getClass())
                                                        return false;

                                    Heroe heroe = (Heroe) o;

                                    return id.equals(heroe.id);
                }

                @Override
                public int hashCode() {
                                    return id.hashCode();
                }

                @Override
                public String toString() {
                                    return "Heroe{" + nombre + ' ' + apellido + '}';
                }
}
Enter fullscreen mode Exit fullscreen mode

Solucionadores del esquema

Los métodos getter y setter de las entidades son suficientes para asignar y obtener atributos de tipo escalar. Para los atributos de tipo Objeto (en nuestro caso, Ciudad en Heroe, o Heroe y Divinidad en Ciudad), debemos usar un solucionador o Resolver que determine su valor.

Son precisamente estos solucionadores los que van a permitir la navegación a través del grafo.

Los solucionadores implementarán el interfaz GraphQLResolver. Creamos el paquete resolver, y preparamos el solucionador para la clase Heroe:

//Java

package com.example.DemoGraphQL.resolver;

import java.util.Set;
import com.coxautodev.graphql.tools.GraphQLResolver;
import com.example.DemoGraphQL.model.Ciudad;
import com.example.DemoGraphQL.model.Heroe;

public class HeroeResolver implements GraphQLResolver {

                public Set getPosesiones(Heroe heroe) {
                                    return heroe.getPosesiones();
                }
}
Enter fullscreen mode Exit fullscreen mode

También tenemos que resolver la Divinidad y el Heroe fundador en la clase Ciudad:

//Java

package com.example.DemoGraphQL.resolver;

import java.util.Optional;
import com.coxautodev.graphql.tools.GraphQLResolver;
import com.example.DemoGraphQL.model.Ciudad;
import com.example.DemoGraphQL.model.Divinidad;
import com.example.DemoGraphQL.model.Heroe;
import com.example.DemoGraphQL.repository.HeroeRepository;

public class CiudadResolver implements GraphQLResolver {

                private HeroeRepository heroeRepository;

                public CiudadResolver(HeroeRepository heroeRepository) {
                                    this.heroeRepository = heroeRepository;
                }

                public Optional getFundador(Ciudad ciudad) {
                                    Heroe fundador = ciudad.getFundador();
                                    if (fundador!=null)
                                        return heroeRepository.findById(ciudad.getFundador().getId());
                                    return Optional.empty();
                }

                public Optional getDivinidad(Ciudad ciudad) {
                                    return Optional.ofNullable(ciudad.getDivinidad());
                }
}
Enter fullscreen mode Exit fullscreen mode

Solucionadores de Consultas y Modificadores

Acabamos de usar GraphQLResolver para obtener los atributos de tipo Objeto.
Además de este solucionador, GraphQL dispone de otros tres tipos:

  • GraphQLQueryResolver para definir las operaciones del tipo raíz Query.
  • GraphQLMutationResolver para definir las operaciones del tipo raíz Mutation.
  • GraphQLSubscriptionResolver para definir las operaciones del tipo raíz adicional Subscription, que permite la suscripción reactiva (no lo tendremos en cuenta de momento).

Para implementar los dos solucionadores que nos faltan, GraphQLQueryResolver y GraphQLMutationResolver, es necesario crear antes los repositorios del sistema de persistencia. Para ello recurrimos a Spring Data, que ya hemos incluido en la configuración de nuestro proyecto. En el paquete repository añadimos el interfaz HeroeRepository:

//Java

package com.example.DemoGraphQL.repository;

import com.example.DemoGraphQL.model.Heroe;
import java.util.List;
import org.springframework.data.repository.CrudRepository;

public interface HeroeRepository extends CrudRepository<Heroe, Long> {

                Heroe findByNombre(String nombre);

}
Enter fullscreen mode Exit fullscreen mode

Hemos añadido un método para obtener un Heroe por nombre. Spring Data generará automáticamente las implementaciones de los otros métodos CRUD: find, save, count y delete.

Ahora, el interfaz CiudadRepository:

//Java

package com.example.DemoGraphQL.repository;

import com.example.DemoGraphQL.model.Ciudad;
import java.util.List;
import org.springframework.data.repository.CrudRepository;

public interface CiudadRepository extends CrudRepository<Ciudad, Long> {

                Ciudad findByNombre(String nombre);
}
Enter fullscreen mode Exit fullscreen mode

También aquí hemos añadido un método para obtener un Ciudad por nombre.

Por último, el interfaz DivinidadRepository:

//Java

package com.example.DemoGraphQL.repository;

import com.example.DemoGraphQL.model.Divinidad;
import org.springframework.data.repository.CrudRepository;

public interface DivinidadRepository extends CrudRepository<Divinidad, Long> {}
Enter fullscreen mode Exit fullscreen mode

Una vez preparados los repositorios JPA, implementamos el solucionador para nuestras consultas a partir de GraphQLQueryResolver. En el paquete resolver creamos la clase Query, y a partir de los repositorios establecemos la relación entre la consulta de GraphQL y el sistema de persistencia:

//Java

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import com.example.DemoGraphQL.model.Ciudad;
import com.example.DemoGraphQL.model.Divinidad;
import com.example.DemoGraphQL.model.Heroe;
import com.example.DemoGraphQL.repository.CiudadRepository;
import com.example.DemoGraphQL.repository.DivinidadRepository;
import com.example.DemoGraphQL.repository.HeroeRepository;

public class Query implements GraphQLQueryResolver {

        private CiudadRepository ciudadRepository;
        private DivinidadRepository divinidadRepository;
        private HeroeRepository heroeRepository;

        public Query(CiudadRepository ciudadRepository, DivinidadRepository divinidadRepository,
                                                    HeroeRepository heroeRepository) {

                this.ciudadRepository = ciudadRepository;
                this.divinidadRepository = divinidadRepository;
                this.heroeRepository = heroeRepository;
        }

        public List divinidades() {
                return (List) divinidadRepository.findAll();
        }

        public List heroes() {
                return (List) heroeRepository.findAll();
        }

        public Heroe heroe(String nombre) {
                return heroeRepository.findByNombre(nombre);
        }

        public long countHeroes() {
                return heroeRepository.count();
        }

        public List ciudades() {
                return (List) ciudadRepository.findAll();
        }

        public List ciudadesRey(String rey) {
            return ((List) ciudadRepository.findAll())
                .stream().filter(ciudad -> Objects.nonNull(ciudad.getFundador()))
                .filter(ciudad -> ciudad.getFundador().getNombre().equals(rey)).collect(Collectors.toList());
        }

        public Ciudad ciudad(String nombre) {
                return ciudadRepository.findByNombre(nombre);
        }
}
Enter fullscreen mode Exit fullscreen mode

El constructor recibe los repositorios que permiten implementar los métodos para las búsquedas que definimos antes en el esquema GraphQL: heroes(), heroe(nombre), countHeroes(), etc.

Nuestra implementación de la clase Mutation es similar:

//Java

package com.example.DemoGraphQL.resolver;

import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.example.DemoGraphQL.model.Divinidad;
import com.example.DemoGraphQL.model.Heroe;
import com.example.DemoGraphQL.repository.CiudadRepository;
import com.example.DemoGraphQL.repository.DivinidadRepository;
import com.example.DemoGraphQL.repository.HeroeRepository;

public class Mutation implements GraphQLMutationResolver {

                private CiudadRepository ciudadRepository;
                private DivinidadRepository divinidadRepository;
                private HeroeRepository heroeRepository;

                public Mutation(CiudadRepository ciudadRepository, DivinidadRepository divinidadRepository,
                                                        HeroeRepository heroeRepository) {

                                    this.ciudadRepository = ciudadRepository;
                                    this.divinidadRepository = divinidadRepository;
                                    this.heroeRepository = heroeRepository;
                }

                public Heroe newHeroe(String nombre, String apellido) {
                                    Heroe heroe = new Heroe();
                                    heroe.setNombre(nombre);
                                    heroe.setApellido(apellido);

                                    heroeRepository.save(heroe);

                                    return heroe;
                }


                public Divinidad newDivinidad(String nombre, String epiteto) {
                                    Divinidad divinidad = new Divinidad();
                                    divinidad.setNombre(nombre);
                                    divinidad.setEpiteto(epiteto);

                                    divinidadRepository.save(divinidad);

                                    return divinidad;
                }

                public Divinidad newDivinidad(Divinidad divinidad) {
                                    divinidadRepository.save(divinidad);
                                    return divinidad;
                }

                public boolean deleteCiudad(Long id) {
                                    ciudadRepository.deleteById(id);
                                    return true;
                }
}
Enter fullscreen mode Exit fullscreen mode

Por último, declaramos en la clase GraphQLApplication las instancias de los solucionadores y cargamos la base de datos H2 con datos de ejemplo.

El proyecto queda así:

Alt Text
Puedes encontrar el código fuente en GitHub.

Acceso al servidor GraphQL

¡Ya tenemos nuestro servidor GraphQL preparado!

Para comprobar que todo funciona correctamente, lo arrancamos y consultamos la URL http://localhost:8080/graphql/schema.json. Deberá aparecer el esquema que hemos definido antes (a continuación, un fragmento):

//respuesta JSON

{
        "data": {
                "__schema": {
                                    "queryType": {
                                        "name": "Query"
                                    },
                                    "mutationType": {
                                        "name": "Mutation"
                                    },
                                    "subscriptionType": null,
                                    "types": [{
                                            "kind": "OBJECT",
                                            "name": "Query",
                                            "description": "",
                                            "fields": [{
                                                        "name": "heroes",
                                                        "description": "",
                                                        "args": [],
                                                        "type": {
                                                                "kind": "NON_NULL",
                                                                "name": null,
                                                                "ofType": {
                                                                        "kind": "LIST",
                                                                        "name": null,
                                                                        "ofType": {
                                                                                "kind": "OBJECT",
                                                                                "name": "Heroe",
                                                                                "ofType": null
                                                                        }
                                                                }
                                                                            },
                                                                            "isDeprecated": false,
                                                                            "deprecationReason": null
                                                        },
                                                                
                                                                
                                                                

                    ]
                    }
            ]
            }
            }
}
Enter fullscreen mode Exit fullscreen mode

Nuestro servidor, pues, es accesible vía HTTP en el contexto /graphql. Es el momento de lanzar unas consultas y comprobar el resultado.

  • Curl/Postman Antes de nada, hay que tener en cuenta que la consulta SDL no es válida en el ámbito del protocolo HTTP. Una consulta como la siguiente:
{
 ciudades {
   nombre
 }
}
Enter fullscreen mode Exit fullscreen mode

deberá tener esta forma para enviarla como JSON en una operación HTTP POST:

{
    "query": "{ciudades {nombre}}"
}
Enter fullscreen mode Exit fullscreen mode

(La consulta, además de query, admite otros atributos, pero no los trataremos aquí).

En Postman:

Alt Text

También se puede enviar la consulta como argumento mediante HTTP GET. La consulta anterior tendría esta forma:

http://localhost:8080/graphql?query={ciudades{nombre}}

  • GraphiQL Para pruebas sencillas basta con las consultas anteriores, pero cuando tengamos que lanzar consultas un poco complejas recurriremos a GraphiQL, que es una aplicación de consulta incluida en nuestro propio servidor.

La incluimos al principio con esta referencia en el pom:

1<!-- https://mvnrepository.com/artifact/com.graphql-java/graphiql-spring-boot-starter -->
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphiql-spring-boot-starter</artifactId>
    <version>3.10.0</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

En http://localhost:8080/graphiql podemos ejecutar las consultas que deseemos:

Alt Text

Por ejemplo, para crear una nueva divinidad:

//consulta GraphQL

mutation {
  newDivinidad(
    nombre: "Hermes",
    epiteto: "Argifonte") {
    nombre epiteto
  }
}
Enter fullscreen mode Exit fullscreen mode

Para consultar los fundadores y las divinidades de todas las ciudades (nótese que “nombre” en cada caso se refiere al nombre de la ciudad, del fundador o de la divinidad, dependiendo del nivel en que se encuentre):

//consulta GraphQL

{ciudades {nombre fundador{nombre} divinidad {nombre}}}
Enter fullscreen mode Exit fullscreen mode

O para borrar la ciudad con id=8

//consulta GraphQL

mutation {
  deleteCiudad(id:8)
}
Enter fullscreen mode Exit fullscreen mode

Conclusión

GraphQL es un lenguaje de consulta muy flexible que abstrae los detalles de implementación de los datos (servicios web, sistema de persistencia) mediante esquemas que definen las entidades, las relaciones entre ellas y las operaciones disponibles con un sistema fuerte de tipos. A diferencia de REST, en el que cada operación implica un punto de acceso distinto, en GraphQL todas las operaciones son accesibles a través de un único punto de acceso.

GitHub

💖 💪 🙅 🚩
joaquinsiabra
Joaquin Siabra

Posted on May 14, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related