GraphQL: una alternativa a REST (2/2)
Joaquin Siabra
Posted on May 14, 2020
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>
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, 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) {
return Optional.ofNullable(ciudad.getDivinidad());
}
}
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);
}
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álida 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:
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>
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.
GitHub
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
November 30, 2024
November 30, 2024