Hace unas semanas había mencionado que estaba aprendiendo sobre el cada vez más popular GraphQL y cómo puede ser un reemplazo a la API RESTful común. En mi ejemplo anteriorvimos cómo crear una aplicación GraphQL utilizando Node.js y una aplicación Couchbase Base de datos NoSQL. Sin embargo, soy tan fan del lenguaje de programación Go como de Node.js.
Vamos a ver cómo crear una aplicación con Golang que pueda crear y consultar datos utilizando Consultas GraphQL en lugar de varios puntos finales de API RESTful.
Si no estás familiarizado con GraphQL, funciona pasando consultas desde un front-end al back-end y obteniendo de vuelta sólo los datos que has solicitado, independientemente de su procedencia o complejidad, en lugar de consumir datos de cualquier número de puntos finales de API potencialmente no relacionados. De este modo, se reducen tanto las solicitudes como la carga de datos, lo que agiliza el proceso y lo hace más eficiente.
Primeros pasos con GraphQL y Golang
Al igual que con una API RESTful, la mayor parte de una aplicación GraphQL consistirá en la configuración. Sin embargo, menos de una aplicación GraphQL dependerá de la planificación, que es diferente de una API RESTful que requiere una planificación pesada con el fin de seguir siendo fácil de usar.
Necesitamos instalar una dependencia del paquete GraphQL para Golang antes de poder continuar. Desde la línea de comandos, ejecute lo siguiente:
1 |
ir consiga github.com/graphql-ir/graphql |
Con nuestra dependencia disponible, cree un nuevo proyecto dentro de su $GOPATH. Dentro de ese proyecto, cree un archivo llamado main.go e incluir lo siguiente que será nuestra base de 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 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 |
paquete principal importar ( "encoding/json" "fmt" "log" "net/http" "github.com/graphql-go/graphql" ) tipo Cuenta struct { ID cadena `json:"id,omitempty"` Nombre cadena `json:"nombre"` Apellido cadena `json:"apellido"` Tipo cadena `json:"tipo"` } tipo Blog struct { ID cadena `json:"id,omitempty"` Cuenta cadena `json:"cuenta"` Título cadena `json:"título` Contenido cadena `json:"contenido"` Tipo cadena `json:"tipo"` } func principal() { fmt.Imprimir("Iniciando aplicación...") accountType := graphql.NuevoObjeto(graphql.ObjectConfig{ Nombre: "Cuenta", Campos: graphql.Campos{}, }) blogTipo := graphql.NuevoObjeto(graphql.ObjectConfig{ Nombre: "Blog, Campos: graphql.Campos{}, }) rootQuery := graphql.NuevoObjeto(graphql.ObjectConfig{ Nombre: "Consulta", Campos: graphql.Campos{}, }) rootMutation := graphql.NuevoObjeto(graphql.ObjectConfig{ Nombre: "RootMutation", Campos: graphql.Campos{}, }) esquema, _ := graphql.NuevoEsquema(graphql.SchemaConfig{ Consulta: rootQuery, Mutación: rootMutation, }) http.HandleFunc("/graphql", func(w http.EscritorRespuesta, r *http.Solicitar) { resultado := graphql.Visite(graphql.Parámetros{ Esquema: esquema, RequestString: r.URL.Consulta().Visite("consulta"), }) json.Nuevo codificador(w).Codificar(resultado) }) http.ListenAndServe(":8080", nil) } |
Nuestro plan es crear una aplicación que pueda crear y consultar cuentas, así como crear y consultar entradas de blog para cualquier cuenta en particular. El modelo de datos de nuestra aplicación está definido por el módulo Cuenta
y la estructura Blog
estructuración. Este modelo de datos ayudará a alimentar nuestro modelo de datos de base de datos, así como nuestro modelo de datos GraphQL.
Cuando se trata de la configuración GraphQL, te darás cuenta de que hay algunos objetos creados. El accountType
y el blogTipo
representan el modelo de datos GraphQL que se creará alrededor de los Go structs. Aún no están configurados, pero lo estarán. El rootQuery
será el conjunto de consultas que podemos ejecutar y el rootMutation
será cualquier mutación de cambio de datos que se pueda ejecutar. Con GraphQL, no estás limitado a solo leer datos.
El esquema GraphQL nos permite definir qué son consultas y qué son mutaciones. Esto es necesario para cuando queramos consumir consultas. Aunque no estamos creando una API RESTful, seguimos necesitando un endpoint HTTP. Este único /graphql
se encargará de todas las consultas y mutaciones y se alimenta del esquema.
Antes de conectar la base de datos, podemos definir nuestro esquema GraphQL:
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 |
accountType := graphql.NuevoObjeto(graphql.ObjectConfig{ Nombre: "Cuenta", Campos: graphql.Campos{ "id": &graphql.Campo{ Tipo: graphql.Cadena, }, "nombre": &graphql.Campo{ Tipo: graphql.Cadena, }, "apellido": &graphql.Campo{ Tipo: graphql.Cadena, }, }, }) blogTipo := graphql.NuevoObjeto(graphql.ObjectConfig{ Nombre: "Blog, Campos: graphql.Campos{ "id": &graphql.Campo{ Tipo: graphql.Cadena, }, "cuenta": &graphql.Campo{ Tipo: graphql.Cadena, }, "título: &graphql.Campo{ Tipo: graphql.Cadena, }, "contenido": &graphql.Campo{ Tipo: graphql.Cadena, }, }, }) |
Fíjate que sólo estamos definiendo nombres de propiedades y su correspondiente tipo de datos. Te dije que sería bastante similar a como hemos definido las estructuras de datos en Go.
Ahora vamos a centrarnos en cargar nuestro esquema con datos de Couchbase Server.
Consulta de datos de una base de datos NoSQL con GraphQL
Por ahora no hemos hecho ninguna preparación de la base de datos. El objetivo de este tutorial no es configurar Couchbase. Necesitarás Couchbase Server disponible con soporte N1QL, un Bucket, algunos índices y una cuenta de control de acceso basada en roles para la aplicación.
Desde la línea de comandos, ejecute las siguientes líneas para obtener nuestros paquetes de base de datos:
1 2 |
ir consiga github.com/satori/ir.uuid ir consiga gopkg.en/couchbase/gocb.v1 |
Los comandos anteriores nos conseguirán un paquete para crear valores UUID para claves de documentos, así como el SDK de Couchbase Go.
Ignorando todo lo que ya le hemos hecho a nuestro main.go eche un vistazo al siguiente código y ajústelo a lo que tiene donde tenga sentido:
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 principal importar ( "encoding/json" "fmt" "log" "net/http" "github.com/graphql-go/graphql" uuid "github.com/satori/go.uuid" gocb "gopkg.in/couchbase/gocb.v1" ) var cubo *gocb.Cubo func principal() { fmt.Imprimir("Iniciando aplicación...") grupo, err := gocb.Conectar("couchbase://localhost") si err != nil { registro.Fatal(err) } grupo.Autentificar(gocb.PasswordAuthenticator{Nombre de usuario: "ejemplo", Contraseña: "123456"}) cubo, err = grupo.OpenBucket("ejemplo", "") si err != nil { registro.Fatal(err) } } |
Observa que hemos definido un Bucket que utilizaremos cuando interactuemos con la base de datos. Nos estamos conectando a un cluster de Couchbase en el directorio principal
autenticándose con una cuenta RBAC, y abriendo un Bucket, todo lo cual debería haber sido definido antes de comenzar este tutorial.
Supongamos que ya tienes datos en tu base de datos. Vamos a empezar por intentar consultar los datos. ¿Recuerdas el siguiente trozo de código que se añadió anteriormente?
1 2 3 4 |
rootQuery := graphql.NuevoObjeto(graphql.ObjectConfig{ Nombre: "Consulta", Campos: graphql.Campos{ }, }) |
Cada campo existirá como un campo en Campos
propiedad. Digamos que queremos obtener todas las cuentas de nuestra base de datos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
"cuentas": &graphql.Campo{ Tipo: graphql.NuevaLista(accountType), Resolver: func(p graphql.ResolverParámetros) (interfaz{}, error) { consulta := gocb.NuevoN1qlQuery("SELECT META(cuenta).id, cuenta.* FROM ejemplo COMO cuenta WHERE cuenta.tipo = 'cuenta'") filas, err := cubo.EjecutarN1qlQuery(consulta, nil) si err != nil { devolver nil, err } var cuentas []Cuenta var fila Cuenta para filas.Siguiente(&fila) { cuentas = añadir(cuentas, fila) } devolver cuentas, nil }, }, |
En el código anterior estamos creando un campo GraphQL llamado cuentas
que devolverá una lista de accountType
cuando se ejecuta. La dirección Resolver
es la que hace el trabajo pesado. Opcionalmente podemos pasar parámetros de consulta, pero para este campo en particular no lo estamos haciendo. Al intentar consultar las cuentas, estamos creando una consulta N1QL que devuelve todas las propiedades y resultados posibles. La consulta GraphQL determinará cuáles de esas propiedades y resultados llegan al cliente.
Tomemos la siguiente consulta frontal que puede ejecutarse:
1 2 3 4 5 6 |
{ cuentas { id, nombre } } |
Mientras que podríamos conseguir el id
, nombre
y apellido
estamos optando por obtener sólo el id
y nombre
de nuestros resultados. Para ejecutar realmente la consulta, emitiremos una sentencia cURL como ésta:
1 |
rizo -g 'http://localhost:8080/graphql?query={accounts{id,firstname}}' |
Recuerde, nuestra consulta GraphQL se envía ver los parámetros de consulta a nuestro punto final de la API.
Veamos otra posible consulta GraphQL. Digamos que queremos consultar una cuenta en particular, no todas las cuentas. Podríamos crear un campo como el siguiente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
"cuenta": &graphql.Campo{ Tipo: accountType, Args: graphql.FieldConfigArgument{ "id": &graphql.ArgumentConfig{ Tipo: graphql.NuevoNonNull(graphql.Cadena), }, }, Resolver: func(parámetros graphql.ResolverParámetros) (interfaz{}, error) { var cuenta Cuenta cuenta.ID = parámetros.Args["id"].(cadena) _, err := cubo.Visite(cuenta.ID, &cuenta) si err != nil { devolver nil, err } devolver cuenta, nil }, }, |
Si se consulta este campo, se obtendrá un único accountType
pero hay argumentos que se pueden pasar. Requerimos que un id
y exigimos que sea una cadena. En el Resolver
podemos obtener el id
y utilizarlo para obtener un documento NoSQL por la clave del documento. El resultado se devuelve y la consulta GraphQL determina qué propiedades se devuelven al cliente.
Tome la siguiente consulta:
1 2 3 4 5 6 |
{ cuenta(id:"2345345435") { nombre, apellido } } |
En la consulta anterior, pasamos un ID y elegimos devolver sólo el nombre
y apellido
de los resultados. Para ejecutar realmente esta consulta, podemos emitir una sentencia cURL como la siguiente:
1 |
rizo -g 'http://localhost:8080/graphql?query={account(id: "2345345435"){firstname,lastname}}' |
Supongamos ahora que queremos obtener todos los blogs de una cuenta de usuario determinada. Los pasos serán similares a los que vimos al consultar una cuenta concreta. Lo siguiente existiría como un campo en nuestro rootQuery
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
"blogs": &graphql.Campo{ Tipo: graphql.NuevaLista(blogTipo), Args: graphql.FieldConfigArgument{ "cuenta": &graphql.ArgumentConfig{ Tipo: graphql.NuevoNonNull(graphql.Cadena), }, }, Resolver: func(parámetros graphql.ResolverParámetros) (interfaz{}, error) { cuenta := parámetros.Args["cuenta"].(cadena) consulta := gocb.NuevoN1qlQuery("SELECT META(blog).id, blog.* FROM ejemplo COMO blog WHERE blog.type = 'blog' AND blog.account = $1") var n1qlParámetros []interfaz{} n1qlParámetros = añadir(n1qlParámetros, cuenta) filas, err := cubo.EjecutarN1qlQuery(consulta, n1qlParámetros) si err != nil { devolver nil, err } var blogs []Blog var fila Blog para filas.Siguiente(&fila) { blogs = añadir(blogs, fila) } devolver blogs, nil }, }, |
El resultado será una lista de blogTipo
y esperamos un cuenta
en formato de cadena. Dentro del campo Resolver
podemos obtener la función cuenta
y utilizarlo en una consulta N1QL parametrizada. La consulta devolverá todas las entradas del blog sólo para una cuenta en particular basada en el valor cuenta
valor.
La consulta para obtener todos los blogs podría tener el siguiente aspecto:
1 2 3 4 5 6 |
{ blogs(cuenta:"2345345435") { id, título } } |
Lo anterior se parece bastante a lo que vimos al consultar una cuenta concreta. Estamos esperando una variable y estamos optando por devolver únicamente la variable id
y el título
propiedades.
¿Quieres ver una consulta bastante interesante basada en lo que tenemos hasta ahora? Echa un vistazo a la siguiente consulta:
1 2 3 4 5 6 7 8 9 10 |
{ cuenta(id:"2345345435") { nombre, apellido } blogs(cuenta:"2345345435") { título, contenido } } |
En el ejemplo anterior estamos haciendo una única petición. Es una única consulta, pero estamos pidiendo los datos de la cuenta y los datos del blog de una cuenta en particular. Si lo hubiéramos hecho con una API RESTful habríamos realizado varias peticiones a nuestro servidor y habríamos hecho posibles mutaciones a nivel de aplicación.
El comando cURL tendría el siguiente aspecto:
1 |
rizo -g 'http://localhost:8080/graphql?query={account(id: "2345345435"){nombre,apellido}blogs(account: "2345345435"){título,contenido}}' |
Hasta ahora sólo hemos hecho consultas de lectura. ¿Y si quisiéramos hacer una mutación en la que creamos o alteramos datos?
Echemos un vistazo a las mutaciones de datos con GraphQL.
En lugar de trabajar en el rootQuery
vamos a trabajar en el rootMutation
. Para los campos, añada 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 |
"crearCuenta": &graphql.Campo{ Tipo: accountType, Args: graphql.FieldConfigArgument{ "nombre": &graphql.ArgumentConfig{ Tipo: graphql.NuevoNonNull(graphql.Cadena), }, "apellido": &graphql.ArgumentConfig{ Tipo: graphql.NuevoNonNull(graphql.Cadena), }, }, Resolver: func(parámetros graphql.ResolverParámetros) (interfaz{}, error) { var cuenta Cuenta cuenta.Nombre = parámetros.Args["nombre"].(cadena) cuenta.Apellido = parámetros.Args["apellido"].(cadena) cuenta.Tipo = "cuenta" id, _ := uuid.NuevoV4() _, err := cubo.Inserte(id.Cadena(), &cuenta, 0) si err != nil { devolver nil, err } cuenta.ID = id.Cadena() devolver cuenta, nil }, }, |
Observe que devolvemos un accountType
y que esperamos que se pasen dos parámetros con nuestra consulta. Sin embargo, la estrategia en realidad no es diferente.
En el Resolver
estamos obteniendo nuestros parámetros y usándolos para crear una nueva cuenta en la base de datos. Debido a que esto está vinculado a nuestro esquema como una mutación, estamos consultando de manera diferente.
En primer lugar, he aquí la consulta que querríamos ejecutar:
1 2 3 4 5 6 7 |
mutación+_ { crearCuenta(nombre:"Matt",apellido:"Arboledas") { id, nombre, apellido } } |
Fíjate en que estamos prefijando con la palabra mutación
. En cURL, tendría el siguiente aspecto:
1 |
rizo -g 'http://localhost:8080/graphql?query=mutation+_{createAccount(nombre: "Matt",apellido: "Groves"){id,nombre,apellido}}' |
Ahora podría seguir creando campos que hagan consultas o mutaciones, pero los pasos son los mismos. Dejaré a tu imaginación que siga contribuyendo a esta idea de aplicación en particular.
Conclusión
Acaba de ver cómo utilizar GraphQL para consultar datos en una aplicación Golang que utiliza un archivo Base de datos NoSQL. GraphQL es muy útil si quieres dejar que el usuario defina qué datos quiere en una sola solicitud en lugar de crear incansablemente múltiples puntos finales de API RESTful que el usuario debe seguir. Es importante tener en cuenta que GraphQL no es un sustituto cuando se trata de que tu aplicación consulte tu base de datos. Usted todavía tendrá que crear consultas N1QL adecuadas en Couchbase. GraphQL sólo opera a nivel de cliente.
Para obtener más información sobre el uso de Go con Couchbase, consulte la página Portal para desarrolladores de Couchbase.