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)
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
.
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
, oDivinidad
- 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 adicionalSubscription
, 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; } }
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}}" }
(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>
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: