Durante los últimos meses he estado aprendiendo sobre GraphQL y cómo utilizarlo como alternativa al desarrollo de API RESTful. Hasta ahora me había centrado en GraphQL y Golang así como GraphQL y Node.js. Puede que haya exprimido todo lo que he podido esos dos lenguajes de desarrollo, así que he decidido cambiar de marcha y probar suerte con algo de Java.
En este tutorial, vamos a ver cómo poner rápidamente en marcha GraphQL utilizando Java y el popular framework Spring Boot.
Antes de entrar en materia, quiero dar crédito a quien lo merece. Prithviraj Pawar escribió un gran artículo titulado, Cómo poner en marcha su servidor GraphQL Java en un abrir y cerrar de ojos, que me dio muchas ideas aunque había cosas que desear, como una base de datos en lugar de datos simulados. Os animo a leer su artículo después de leer a través de la mía.
Requisitos
Hay algunos requisitos previos que deben cumplirse antes de saltar a la GraphQL y el lado de desarrollo de las cosas.
Vamos a asumir que ya tienes Couchbase instalado, configurado y funcionando. Cualquier cosa después de Couchbase Server 5.0 servirá porque sólo vamos a utilizar las características básicas.
También vamos a suponer que has descargado un proyecto base de Spring Boot. La página Sitio web de Spring Boot tiene un gran generador que te permite definir las dependencias. Si quieres mantener la coherencia con mi guía, tendrás que crear un proyecto Gradle en lugar de Maven.
Definición de las dependencias del proyecto Gradle
Cuando tengas una plantilla de Spring Boot a mano, probablemente estará muy desnuda. Necesitamos incluir las dependencias que planeamos usar en nuestra configuración de Gradle.
Abra el archivo build.gradle e incluya lo siguiente:
1 2 3 4 5 6 |
dependencias { compilar(org.springframework.boot:spring-boot-starter-web) compilar(com.couchbase.client:java-client) compilar(com.graphql-java:graphql-java:9.0) testCompile(org.springframework.boot:spring-boot-starter-test') } |
Usaremos Spring Boot para servir nuestro endpoint GraphQL, el SDK Java de Couchbase para interactuar con nuestra base de datos, y el plugin GraphQL para procesar nuestras consultas.
Bootstrapping de la API Java con RESTful Endpoints
Con las dependencias en su lugar, podemos empezar a desarrollar nuestra API. A pesar de que estamos utilizando GraphQL como un reemplazo a todas las cosas comunes API RESTful, esto no significa que REST está completamente fuera de la ecuación. Todavía necesitamos un punto final para enviar nuestras consultas GraphQL a.
Dependiendo de cómo haya llamado a su proyecto, paquete, etc., abra el archivo que contiene su principal
e incluyen lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
paquete com.couchbase.graphql; importar com.couchbase.cliente.java.Cubo; importar com.couchbase.cliente.java.Grupo; importar com.couchbase.cliente.java.CouchbaseCluster; importar com.couchbase.cliente.java.documento.json.JsonObject; importar graphql.esquema.Captador de datos; importar org.springframework.judías.fábrica.anotación.Autowired; importar org.springframework.judías.fábrica.anotación.Valor; importar org.springframework.arranque.SpringApplication; importar org.springframework.arranque.autoconfigure.SpringBootApplication; importar org.springframework.contexto.anotación.Judía; importar org.springframework.http.HttpStatus; importar org.springframework.http.RespuestaEntidad; importar org.springframework.web.vincular.anotación.RequestBody; importar org.springframework.web.vincular.anotación.RequestMapping; importar org.springframework.web.vincular.anotación.RequestMethod; importar org.springframework.web.vincular.anotación.RestController; importar graphql.Entrada de ejecución; importar graphql.Resultado de la ejecución; importar graphql.GraphQL; importar graphql.esquema.GraphQLSchema; importar graphql.esquema.StaticDataFetcher; importar graphql.esquema.idl.RuntimeWiring; importar graphql.esquema.idl.Generador de esquemas; importar graphql.esquema.idl.SchemaParser; importar graphql.esquema.idl.TypeDefinitionRegistry; importar javax.anotación.PostConstruir; importar estático graphql.esquema.idl.RuntimeWiring.newRuntimeWiring; importar java.io.Archivo; importar java.nio.archivo.Archivos; importar java.nio.archivo.Caminos; importar java.util.HashMap; importar java.util.Mapa; @SpringBootApplication @RestController público clase GraphqlApplication { público estático void principal(Cadena[] args) { SpringApplication.ejecute(GraphqlApplication.clase, args); } @RequestMapping(valor="/", método= RequestMethod.GET) público Objeto rootEndpoint() { Mapa<Cadena, Objeto> respuesta = nuevo HashMap<Cadena, Objeto>(); respuesta.poner("mensaje", "Hola Mundo"); devolver respuesta; } @RequestMapping(valor="/graphql", método= RequestMethod.POST) público Objeto graphql(@RequestBody Cadena solicitar) { JsonObject jsonRequest = JsonObject.fromJson(solicitar); devolver jsonRequest; } } |
Recuerde que su paquete y nombre de clase pueden no coincidir con los míos. Rellene los huecos donde corresponda.
Usted notará que tenemos dos puntos finales, donde uno es completamente opcional. He creado el rootEndpoint
para comprobar que mi API RESTful funcionaba. El sitio graphql
es donde vamos a pasar nuestro tiempo. Espera una petición POST así como un cuerpo de petición. El cuerpo de la solicitud con el tiempo será una consulta GraphQL real, pero por ahora podemos dejarlo como está.
Diseño de un documento de esquema GraphQL
Hasta ahora sólo hemos hecho un trabajo básico de preparación de Spring Boot y nada realmente relacionado con GraphQL. Para que GraphQL sea posible, necesitamos conocer un esquema definido que explique las consultas que están disponibles así como los tipos de datos con los que estamos trabajando. Hay numerosas maneras de hacer esto, pero vamos a centrarnos en un documento de esquema.
Dentro del recursos de su proyecto, incluya un archivo esquema.graphql que contiene lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
tipo Consulta{ pokemons: [Pokemon] juegos: [Juego] juego(id: Cadena): Juego } tipo Juego { id: Cadena nombre: Cadena } tipo Pokemon { id: Cadena juego: Juego nombre: Cadena altura: Int peso: Int } |
Por si no es obvio, vamos a trabajar con datos de Pokemon. En el archivo anterior hemos definido las consultas que están disponibles y los dos tipos de datos que están disponibles.
Hay una gran variedad de juegos Pokemon en el mercado y hay diferentes Pokemon en cada uno de los juegos, de ahí la relación que hemos definido. Si eres un verdadero fan de Pokemon sabrás que hay un poco más que esto, pero para este ejemplo es suficiente.
Desarrollo de la lógica de obtención para la base de datos NoSQL Couchbase
Con el esquema definido, tenemos que idear la lógica de base de datos que se ejecutará cuando se ejecute una consulta y esa lógica de base de datos debe devolver resultados que coincidan con cada uno de los dos tipos de datos.
Para hacer nuestra vida un poco más fácil cuando se trata de mantenimiento del proyecto, vamos a crear un archivo separado para la gestión de la base de datos. Dentro de tu paquete, crea un archivo Base de datos.java que contiene lo siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
paquete com.couchbase.graphql; importar com.couchbase.cliente.java.Cubo; importar com.couchbase.cliente.java.documento.JsonDocument; importar com.couchbase.cliente.java.documento.json.JsonObject; importar com.couchbase.cliente.java.consulta.N1qlQuery; importar com.couchbase.cliente.java.consulta.N1qlQueryResultado; importar com.couchbase.cliente.java.consulta.N1qlQueryRow; importar graphql.esquema.Captador de datos; importar java.util.ArrayList; importar java.util.HashMap; importar java.util.Lista; importar java.util.Mapa; público clase Base de datos { público estático Captador de datos getPokemonData(Cubo cubo) { } público estático Captador de datos getGameData(Cubo cubo) { } público estático Captador de datos getGamesData(Cubo cubo) { } privado estático Lista<Mapa<Cadena, Objeto>> extractResultOrThrow(N1qlQueryResultado resultado) { } } |
En su mayor parte, el conjunto anterior de funciones debe parecer familiar porque los hemos definido de alguna manera para que coincida con lo que teníamos en nuestro archivo de esquema GraphQL.
Antes de empezar a rellenar cada una de las funciones, hay que tener en cuenta algunas cosas:
- El cubo se definirá en nuestro archivo de proyecto principal poco después de definir nuestra lógica.
- GraphQL espera un determinado tipo de respuesta, de ahí la etiqueta
extractResultOrThrow
que hemos creado.
Dicho esto, empecemos por crear la función envoltorio. El extractResultOrThrow
se utilizará sólo en las consultas N1QL y tiene el siguiente aspecto:
1 2 3 4 5 6 7 |
privado estático Lista<Mapa<Cadena, Objeto>> extractResultOrThrow(N1qlQueryResultado resultado) { Lista<Mapa<Cadena, Objeto>> contenido = nuevo ArrayList<Mapa<Cadena, Objeto>>(); para (N1qlQueryRow fila : resultado) { contenido.añada(fila.valor().toMap()); } devolver contenido; } |
Básicamente, estamos recorriendo un conjunto de resultados de N1QL y creando un archivo Lista
de Mapa
de él.
Suponiendo que tenemos los datos del juego en nuestra base de datos, podemos consultarlos en la función getGamesData
función:
1 2 3 4 5 6 7 8 9 10 |
público estático Captador de datos getGamesData(Cubo cubo) { devolver medio ambiente -> { Sistema.fuera.println("BUSCANDO DATOS DE JUEGOS..."); Cadena declaración = "SELECT ejemplo.*" + "DEL ejemplo " + "WHERE type = 'game'"; N1qlQueryResultado queryResult = cubo.consulta(N1qlQuery.simple(declaración)); devolver extractResultOrThrow(queryResult); }; } |
Con N1QL podemos buscar documentos que coincidan con los criterios tipo
de juego
y procesar los resultados con nuestra función envoltorio. Si realmente quieres ser estricto, en lugar de utilizar un asterisco comodín, podrías definir las propiedades que coincidan con tu esquema.
La siguiente función, getPokemonData
nos permitirá consultar nuestros datos Pokemon de forma similar a como lo hicimos con los datos de los juegos.
1 2 3 4 5 6 7 8 9 10 |
público estático Captador de datos getPokemonData(Cubo cubo) { devolver medio ambiente -> { Sistema.fuera.println("OBTENIENDO DATOS POKEMON..."); Cadena declaración = "SELECT ejemplo.*" + "DEL ejemplo " + "WHERE type = 'pokemon'"; N1qlQueryResultado queryResult = cubo.consulta(N1qlQuery.simple(declaración)); devolver extractResultOrThrow(queryResult); }; } |
Observará que no hemos hecho nada nuevo más allá de cambiar el tipo
propiedad. Aquí es donde las cosas pueden ponerse interesantes. Tenemos una relación de datos para Pokemon con respecto a los datos del juego. Podríamos recuperarla en nuestra consulta actual o dividirla para que sea más modular y potencialmente más ligera. Vamos a optar por la opción modular y ligera.
Esto nos lleva a nuestro getGameData
función:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
público estático Captador de datos getGameData(Cubo cubo) { devolver medio ambiente -> { HashMap<Cadena, Objeto> padre = medio ambiente.getSource(); JsonDocument documento; si(padre != null) { Sistema.fuera.println("OBTENIENDO DATOS DEL JUEGO PARA" + (Cadena)padre.consiga("juego") + "..."); documento = cubo.consiga((Cadena) padre.consiga("juego")); } si no { Sistema.fuera.println("OBTENIENDO DATOS DEL JUEGO PARA" + (Cadena)medio ambiente.getArgument("id") + "..."); documento = cubo.consiga((Cadena)medio ambiente.getArgument("id")); } devolver documento.contenido().toMap(); }; } |
En la función anterior ocurren dos cosas, ninguna de las cuales utiliza N1QL.
En nuestro esquema hemos definido una consulta basada en argumentos para los datos del juego, así como una relación de datos en los datos de Pokemon. Vamos a acomodar ambos escenarios en la misma función.
Utilización de la environment.getSource()
podemos ver si hay algún dato padre que acompañe a la petición. Un ejemplo de estos datos padre podría ser cuando consultamos datos de Pokemon. El juego
se refiere al campo getGameData
mientras que los demás datos, como el nombre, el peso u otros, se refieren a los datos principales. En la base de datos, la información del juego se almacena como un id, por lo que después de extraerlo de los datos de los padres, podemos hacer una búsqueda de datos del juego.
El otro escenario es si pasamos un argumento que representa un id de juego.
Ambos son válidos y ambos hacen cosas diferentes. Al final, sólo queremos un único resultado en lugar de una matriz de resultados.
Cableado de la aplicación para un procesamiento GraphQL satisfactorio
Estamos en la recta final. Tenemos nuestra API y nuestra lógica de base de datos en su lugar. El último paso es conectar el esquema GraphQL a la lógica de la base de datos y ver los resultados en todo su esplendor.
¿Recuerdas que en el paso anterior mencioné la configuración de la información de nuestro cubo? Vamos a ocuparnos de eso primero. Abra el proyecto aplicación.propiedades que se encuentra en el archivo recursos directorio. Incluya lo siguiente:
1 2 3 4 |
nombre de host=127.0.0.1 cubo=ejemplo nombre de usuario=ejemplo contraseña=123456 |
La información de mi base de datos es sólo un ejemplo. Asegúrese de reemplazarla con la información que está utilizando para su instancia de Couchbase.
De vuelta en el archivo principal del proyecto, podemos asignar los valores y conectarnos a nuestra instancia:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
paquete com.couchbase.graphql; importar com.couchbase.cliente.java.Cubo; importar com.couchbase.cliente.java.Grupo; importar com.couchbase.cliente.java.CouchbaseCluster; importar com.couchbase.cliente.java.documento.json.JsonObject; importar graphql.esquema.Captador de datos; importar org.springframework.judías.fábrica.anotación.Autowired; importar org.springframework.judías.fábrica.anotación.Valor; importar org.springframework.arranque.SpringApplication; importar org.springframework.arranque.autoconfigure.SpringBootApplication; importar org.springframework.contexto.anotación.Judía; importar org.springframework.http.HttpStatus; importar org.springframework.http.RespuestaEntidad; importar org.springframework.web.vincular.anotación.RequestBody; importar org.springframework.web.vincular.anotación.RequestMapping; importar org.springframework.web.vincular.anotación.RequestMethod; importar org.springframework.web.vincular.anotación.RestController; importar graphql.Entrada de ejecución; importar graphql.Resultado de la ejecución; importar graphql.GraphQL; importar graphql.esquema.GraphQLSchema; importar graphql.esquema.StaticDataFetcher; importar graphql.esquema.idl.RuntimeWiring; importar graphql.esquema.idl.Generador de esquemas; importar graphql.esquema.idl.SchemaParser; importar graphql.esquema.idl.TypeDefinitionRegistry; importar javax.anotación.PostConstruir; importar estático graphql.esquema.idl.RuntimeWiring.newRuntimeWiring; importar java.io.Archivo; importar java.nio.archivo.Archivos; importar java.nio.archivo.Caminos; importar java.util.HashMap; importar java.util.Mapa; @SpringBootApplication @RestController público clase GraphqlApplication { @Valor("${hostname}") privado Cadena nombre de host; @Valor("${bucket}") privado Cadena cubo; @Valor("${username}") privado Cadena nombre de usuario; @Valor("${contraseña}") privado Cadena contraseña; público @Judía Grupo grupo() { Grupo grupo = CouchbaseCluster.crear(este.nombre de host); grupo.autentifique(este.nombre de usuario, este.contraseña); devolver grupo; } público @Judía Cubo cubo() { devolver grupo().openBucket(este.cubo); } privado GraphQL construya; público estático void principal(Cadena[] args) { SpringApplication.ejecute(GraphqlApplication.clase, args); } @RequestMapping(valor="/", método= RequestMethod.GET) público Objeto rootEndpoint() { } @RequestMapping(valor="/graphql", método= RequestMethod.POST) público Objeto graphql(@RequestBody Cadena solicitar) { } } |
Observa que hemos creado dos Judía
y usamos la información de nuestro archivo de propiedades. Así de fácil estamos conectados a Couchbase.
El siguiente paso es inicializar nuestro esquema GraphQL. Hay numerosas maneras de hacer esto, pero me pareció más fácil usando la anotación de Spring Boot para cuando se inicia la aplicación:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@PostConstruir() público void init() { Cargador de clases cargador de clases = getClass().getClassLoader(); Archivo schemaFile = nuevo Archivo(cargador de clases.getResource("esquema.graphql").getFile()); SchemaParser schemaParser = nuevo SchemaParser(); TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.analizar(schemaFile); RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() .tipo("Consulta",tipoCableado -> tipoCableado .dataFetcher("pokemons",Base de datos.getPokemonData(cubo())) ) .tipo("Consulta",tipoCableado -> tipoCableado .dataFetcher("juegos",Base de datos.getGamesData(cubo())) ) .tipo("Consulta",tipoCableado -> tipoCableado .dataFetcher("juego",Base de datos.getGameData(cubo())) ) .tipo("Pokemon",tipoCableado -> tipoCableado .dataFetcher("juego",Base de datos.getGameData(cubo())) ).construya(); Generador de esquemas schemaGenerator = nuevo Generador de esquemas(); GraphQLSchema graphQLSchema = schemaGenerator.makeEsquemaEjecutable(typeDefinitionRegistry, runtimeWiring); este.construya = GraphQL.newGraphQL(graphQLSchema).construya(); } |
En init
será llamada cuando se inicie la aplicación. Cargará nuestro archivo de esquema desde el directorio recursos y definir el cableado. Por ejemplo, sabemos que pokemons
, juegos
y game(id: cadena)
son todos tipos de consultas. Cada una se mapea a la función apropiada de la base de datos y se pasa el cubo abierto. Entonces establecemos un mapeo de juego
a la Pokemon
tipo. Estamos definiendo más o menos cómo se vinculan los campos y las consultas a la interacción con la base de datos.
Una vez construido el esquema podemos vincularlo a nuestra variable de clase. Con el esquema construido, podemos finalizar nuestro punto final GraphQL RESTful:
1 2 3 4 5 6 7 |
@RequestMapping(valor="/graphql", método= RequestMethod.POST) público Objeto graphql(@RequestBody Cadena solicitar) { JsonObject jsonRequest = JsonObject.fromJson(solicitar); Entrada de ejecución executionInput = Entrada de ejecución.newExecutionInput().consulta(jsonRequest.getString("consulta")).construya(); Resultado de la ejecución resultado de la ejecución = este.construya.ejecutar(executionInput); devolver resultado de la ejecución.toEspecificación(); } |
En nuestro endpoint tomamos la cadena de consulta que se pasó con la petición y la ejecutamos con nuestro esquema construido. El resultado será lo que el cliente haya solicitado en la consulta.
¿Qué aspecto tiene una consulta? Tomemos el siguiente ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{ pokemons { nombre peso juego { nombre } } juegos { nombre } juego(id: "juego-2") { nombre } } |
La consulta anterior probablemente no sea algo que nadie necesite ejecutar nunca, pero podrías hacerlo si quisieras. En el ejemplo anterior estamos consultando todos los datos de Pokemon y el juego del que forman parte. También estamos consultando todos los juegos, así como un juego específico. Las tres partes de la consulta no están relacionadas, pero existen en la misma consulta.
Como dato curioso, en el pokemons
si elegimos no consultar los datos del juego, la función de la base de datos nunca será llamada. GraphQL sólo ejecutará las funciones cuando sean necesarias.
Conclusión
Acabas de ver cómo construir una aplicación GraphQL usando Spring Boot, Java y Couchbase Server como base de datos NoSQL. Como has visto en mi consulta de ejemplo, hemos sido capaces de consultar muchas piezas diferentes de datos no relacionados en una sola solicitud, algo que habría requerido varias en una API RESTful.
Para obtener más información sobre el uso del SDK de Java con Couchbase, consulte la página Portal para desarrolladores de Couchbase.