¡Compártelo!

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.
>> Recuerda que puedes consultar el código fuente en nuestro respositorio de Github.

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>

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

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)
Consola base de datos

Definición del esquema GraphQL

Carpeta 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.

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! }

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 }

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! }

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]! }

Y de modo similar con el tipo Mutation:

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

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]! }

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 + '}"; } }

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(); } }

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) { eturn Optional.ofNullable(ciudad.getDivinidad()); } }

Solucionadores de Consultas y Modificadores

Acabamos de usar GraphQLResolver<T> 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); }

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); }

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> {}

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); } }

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; } }
proyecto GraphQL

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í:

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, ... ... ... }] }] } } }

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álido en el ámbito del protocolo HTTP. Una consulta como la siguiente:

{ ciudades { nombre } }

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

 { "query": "{ciudades {nombre}}" }
GraphQL, consulta en Postman

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

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:

<!-- 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>
GraphiQL

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

Por ejemplo, para crear una nueva divinidad:

//consulta GraphQL mutation { newDivinidad( nombre: "Hermes", epiteto: "Argifonte") { nombre epiteto } }

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} } }

O para borrar la ciudad con id=8 //consulta GraphQL mutation { deleteCiudad(id:8) }

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.  
En un próximo artículo nos centraremos en funcionalidades adicionales de GraphQL, como los fragmentos, las directivas, la paginación o la gestión errores.
Puedes encontrar más información sobre GraphQL en:

Artículos relacionados

jdk 21

Jdk 21: mejoras en la última versión LTS de Java

Cada 6 meses Java lanza una nueva versión de nuestro lenguaje favorito. Da igual si la estábamos esperando con ganas o si nos pilla por sorpresa, es algo que celebrar dentro de la comunidad. Esta vez la versión 21 incluye diferentes características estables, otras en

openAPI

Explorando OpenApi: estructura, rutas y seguridad

En este artículo, nos adentraremos en la utilización de OpenApi para crear los diferentes endpoints de nuestra aplicación, con sus diferentes objetos de request y response que necesitemos. ¡Vamos allá! ¿Qué es una API? Las API (Interfaz de Programación de Aplicaciones) son piezas de código

Cómo hacer una Regresión Lineal Simple en Machine Learning

Cómo hacer una Regresión Lineal Simple en Machine Learning

En la actualidad, el aprendizaje automático se ha convertido en una de las bases para resolver problemas y como ayuda a la toma de decisiones. En este artículo nos centraremos en el análisis y aplicación práctica de uno de los modelos predictivos más sencillos, el