GraphQL: ¡todos para uno y uno para todos! 2/2

Tras una primera parte más teórica sobre GraphQL, es el momento de ver un ejemplo completo en el que pondremos en práctica todo lo explicado.

Hemos escogido la implementación de Java para GraphQL y un stack tecnológico muy extendido cuya base será Spring Boot, Maven 3.X y la versión 1.8 de Java. ¡Empezamos!

Stack tecnológico

A parte del framework base de Spring Boot, estas son las librerías relacionadas con GraphQL que se han utilizado en la implementación del ejemplo:

Graphql para Java

Graphql para Java es la implementación oficial en la que están basados todos los frameworks del ecosistema Java.

Graphql-apigen

Filosofía “Schema first”. Es el framework que, a partir de un esquema definido en GraphQL, nos generará un armazón para incluir nuestra programación de la obtención de los datos de sus diferentes orígenes.

Personalmente opino que la declaración de esquemas programáticamente nos obliga casi a aprendernos un API completo, es excesivamente “verboso”, complejo y apenas reutilizable. Para este tipo de declaración existen otras implementaciones en otros lenguajes más idóneos como NodeJS o Python.

Sus características:

  • Genera todos los POJOS (Input, Types…).
  • Permite estructuración en paquetes de un modo sencillo.
  • Genera armazón de clases para la inclusión nuestra programación, es extensible, usa interfaces para todo. Incluye concepto de “UnResolved” y “Resolved” sobre los fields para su recuperación.
  • Posee un plugin de Maven de generación de los fuentes.
  • Genera clases @fluent.
  • Integración básica con Guice y Spring.

Graphql-Spring-Boot

Graphql-Spring-Boot es el oficial para Spring Boot que proponen, no obstante, existe una aproximación muy buena también que sería viable Spring Boot starter Graphql.

  • Implementa la exposición mediante endpoint Http del Graphql Server completa según la especificación nos indica.
  • Permite varias opciones de configuración:
    • Prefijos de los roles de los objetos… para su definición programática.
    • Configuración del CORS.
  • Usa Spring GraphQL Common, que es la librería oficial que nos “facilita” crear nuestros esquemas programáticamente mediante un conjunto de anotaciones. Esta librería alivia la definición de un esquema en Java, pero aún así, como indico más arriba, se convierte en algo complejo en comparación con la declaración mediante el lenguaje GraphQL.
  • Incluye herramienta oficial GraphiQL, la cual nos ayudará a la construcción de consultas y mutaciones. Es un cliente implementado mediante React.

Implementación de nuestro grafo

La estructuración del código podemos verla a continuación:

Identificamos las siguientes partes:

  • Service y repository: es el típico acceso a nuestra MongoDB con Spring Data.
  • Model: modelo de nuestros documentos de MongoDB.
  • Application: Arranque de la aplicación Spring Boot.
  • Config: configuración del proyecto e integración entre los frameworks de Graphql-apigen y de Graphql-Spring-Boot, en vez de declarar el esquema de modo programático lo que hacemos es crearlo directamente con la información que nos ofrece el framework de generación.
  • En el target/generated encontraremos las fuentes generadas por Graphql-apigen incluida la generación cada vértice del grafo.
  • Paquete graphql es la parte específica GraphQL: aquí es donde está la verdadera implementación de la obtención de la información y la realización de operaciones.

Resolviendo atributos

Como hemos explicado anteriormente, GraphQL toma cada field de forma independiente y en sus implementaciones incluye el concepto de “fetcher/resolver” para la obtención de la información.

En nuestro caso, en vez de fetchers, poseeremos resolvers. Las entidades podrán estar “resueltas” o “no resueltas”, en caso de que estén en este último estado las obtendremos de la fuente de datos que corresponda.

Alguien podría pensar que usando MongoDB, en el cual poseemos habitualmente la información embebida, estamos “trabajando” lo mismo que para obtener un field que todos los restantes de la entidad.

Es decir, si nos piden el ID en una consulta, obtendremos el documento completo; por lo que no necesitaríamos hacer más consultas para los fields restantes… Es un ejemplo de cómo separar el modelo de persistencia del diseño de nuestro grafo de forma independiente exactamente igual que si de un API REST se tratase.

public abstract class BaseResolver<T>  {

	/**
	 * Resuelve la obtención de los datos con la implementación por defecto  
	 * 
	 * @param unresolvedList
	 * @return
	 */
	public List<T> resolve(List<T> unresolvedList) {
		List<T> resolvedList = new ArrayList<>();
		/* Miramos si no es null y nos viene con valor para obtenerlo si es necesario */
		if (!requireNonNull(unresolvedList).isEmpty()) {
			for (T element : unresolvedList) {
				if (element != null) {
					resolvedList.add(processElement(element));
				}
			}
		}
		return resolvedList;
	}

	/**
	 * Comprobaremos si está resuelta la entidad y si no es así obtenemos de la persistencia el "completo"
	 * 
	 * @param unresolverdEntity
	 * @return
	 */
	private T processElement(final T unresolved) {
		/* Obtenemos el elemto filtrado por lo que corresponda */
		if (unresolved.getClass().equals(unresolvedClass())) {
			T findElement = findById(unresolved);
			return findElement;
		}
		return unresolved;
	}

	/**
	 * Clase a implementar para la obtencion de los elementos relacionados
	 * 
	 * @param unresolved
	 * @return
	 */
	protected abstract T findById(T unresolved);
	
	/**
	 * Clase la cual indicará wue una entidad no está resuelta 
	 * 
	 * @return
	 */
	protected abstract Class<?> unresolvedClass();
	
}

Analizando el código, podemos observar cómo en el método “processElement” comprobamos si el elemento es necesario resolverlo. Si es así hacemos una llamada a obtenerlo por su ID.

La obtención por el ID es una estrategia adoptada por defecto, un ejemplo sería la obtención de la propiedad “model” de un “car” en cualquiera de las consultas que se intervenga.

@Component
public class ModelResolver extends BaseResolver<Model> implements Model.Resolver {

	@Autowired
	ModelService modelService;

	@Override
	protected Model findById(Model unresolved) {
		return modelService.findById(unresolved.getId());
	}

	@Override
	protected Class<?> unresolvedClass() {
		return Model.Unresolved.class;
	}

}

En el modelo se verá que, dentro de un CarMO, existe una referencia a ModelMO, la cual no se usa y se provoca la consulta independiente de este último como ejemplo claro, ya que siempre será marcado como “Unresolved” para que en la resolución del grafo se realice la llamada correspondiente.

Como vemos, una de las desventajas de GraphQL es la “cantidad” de accesos que se realizan a las fuentes de datos. Para ello propone la realización de cachés distribuidas, precisamente para evitar latencias y aplicando las políticas adecuadas en cada uno de los casos.

Consulta raíz

Como hemos aprendido siempre existirá una consulta raíz, a partir de la cual podremos consultar todo nuestro esquema según lo hayamos definido. En nuestro caso será:

El código que la identifica sería el siguiente:

@Component
public class QueryRootImpl implements QueryRoot {

	@Autowired
	CarService carService;

	@Autowired
	BrandService brandService;

	@Autowired
	ModelService modelService;

	@Override
	public List<Car> getCars() {
		return carService.findAll();
	}

	@Override
	public Car car(final CarArgs args) {
		return new Car.Unresolved(args.getId());
	}

	@Override
	public List<Brand> brands(BrandsArgs args) {
		return brandService.findAll();
	}

	@Override
	public List<Model> models(ModelsArgs args) {
		return modelService.findAll();
	}

	@Override
	public Model model(ModelArgs args) {
		return new Model.Unresolved(args.getId());
	}

	@Override
	public Brand brand(BrandArgs args) {
		return new Brand.Unresolved(args.getId());
	}

}

Si observáis es plenamente identificativo con respecto a la definición realizada en el esquema. Esto es de lo que más me gusta de la librería elegida para la integración e implementación.

Podemos distinguir dos tipos de métodos:

  • Con parámetros: son aquellos que poseen argumentos. En nuestro caso no vamos más allá de un ID, pero podríamos incluir cualquier tipo de datos para usarlo como  filtro en la consulta (name para filtrar tipo like…).

A parte, vemos que para obtenerlo por su ID marcamos la entidad como no resuelta  (Unresolved) para que pase por el punto de obtención de la información.

  • Sin parámetros: son aquellos que obtienen directamente la información sin los argumentos ya que no los necesitan en su punto inicial, pero cuando se vayan resolviendo las relaciones y los nodos anidados se irán invocando cada uno de los resolvers (BrandResolver, ModelResolver, CarResolver…).

Mutaciones de Car

Al igual que existe una consulta raíz para las operaciones, en las mutaciones el concepto es análogo, es decir, un punto de entrada donde podremos ver todas las operaciones existentes en nuestro GraphQL Server.

En nuestro caso dos operaciones sobre coches createCar y deleteCar.

La correspondencia en el código en este caso sería la siguiente:

@Component
public class CarMutationImpl implements MutateCars {

	@Autowired
	CarService carService;

	@Override
	public Car createCar(CreateCarArgs args) {
		return carService.createCar(args.getCar());
	}

	@Override
	public String deleteCar(DeleteCarArgs args) {
		return carService.deleteCar(args.getId());
	}
}

Como podemos observar, tenemos un objeto para la entrada de los argumentos en cada una de las operaciones con los atributos definidos en nuestro esquema y realizaremos una llamada al servicio para hacer lo que corresponda.

Por ejemplo, en createCar vemos que nos retorna un coche una vez creado, dicho objeto entrará en el flujo de consulta normal.

Configuración y puesta en marcha del ejemplo

En este repositorio podréis encontrar todos los pasos para la configuración y puesta en marcha, pero deberéis tener configurado en vuestro entorno:

  • Jdk 1.8 y Maven 3.x.
  • MongoDB accesible para la configuración de la cadena de conexión.
  • Configurar application.yml:
    • Se deberá configurar la conexión al MongoDB que queramos usar.
    • Configurar si se quiere que en el arranque se nos cree un juego mínimo de datos (mongo_model.js).
  • Si tienes todo lo anterior será suficiente con ejecutar sh build_and_run.sh.

Conclusiones

Como se ha podido observar la implementación del esquema de GrahpQL, en Java se integra perfectamente con las herramientas que usamos en el día a día. Sin embargo, he de admitir que de primeras todas las lindezas que propone como novedosas transmiten complejidad y posible ineficiencia en la parte servidora si la implementación no es correcta.

La implementación con Java es más “verbosa”, más compleja y menos natural que con otros lenguajes de script como NodeJS (GraphQL-express-js) o Python (Graphene),ya que las convenciones de las librerías que lo implementan facilitan mucho el desarrollo. No obstante, en próximas entregas, mostraremos cómo se implementa el mismo ejemplo en estos lenguajes para que cada uno decida por sí mismo ;)

GraphQL ha llegado para quedarse como alternativa clara a las tecnologías que usamos habitualmente para la construcción de aplicaciones SPA, aplicaciones móviles… Aunque no sea la solución para todos los casos, no dudes en tenerlo en cuenta  para tu próxima arquitectura.

Arquitecto de profesión, pero con corazón de programador ;) Mi objetivo del día a día es aprender, disfrutar de lo que hago y enriquecerme con las personas que me rodean. Con más de 15 años en el mundo del desarrollo e intentando seguirle el ritmo a las nuevas tecnologías.

Ver toda la actividad de J. Manuel García Rozas

Escribe un comentario