GraphQL y Golang

La tecnolog铆a GraphQL en los 煤ltimos a帽os, despu茅s de que la compa帽铆a Facebook la transfiri贸 a la categor铆a de c贸digo abierto, se ha vuelto muy popular. El autor del material, cuya traducci贸n publicamos hoy, dice que trat贸 de trabajar con GraphQL en Node.js y, por su propia experiencia, estaba convencido de que esta tecnolog铆a, gracias a sus notables capacidades y simplicidad, no atrae tanta atenci贸n accidentalmente. Recientemente, mientras participaba en un nuevo proyecto, cambi贸 de Node.js a Golang. Luego decidi贸 probar la colaboraci贸n de Golang y GraphQL.



Informaci贸n preliminar


Puede aprender de la definici贸n oficial de GraphQL que este es un lenguaje de consulta para la API y un tiempo de ejecuci贸n para ejecutar tales consultas en los datos existentes. GraphQL proporciona una descripci贸n completa y comprensible de los datos en una determinada API, permite a los clientes solicitar exactamente la informaci贸n que necesitan, y nada m谩s, simplifica el desarrollo de la API con el tiempo y brinda a los desarrolladores herramientas poderosas.

No hay muchas bibliotecas GraphQL para Golang. En particular, prob茅 bibliotecas como Thunder , graphql , graphql-go y gqlgen . Debo se帽alar que lo mejor de todo lo que prob茅 fue la biblioteca gqlgen.

La biblioteca gqlgen todav铆a est谩 en beta, al momento de escribir este material era la versi贸n 0.7.2 . La biblioteca est谩 evolucionando r谩pidamente. Aqu铆 puede conocer los planes para su desarrollo. Ahora el patrocinador oficial de gqlgen es el proyecto 99designs , lo que significa que esta biblioteca, posiblemente, se desarrollar谩 a煤n m谩s r谩pido que antes. Los principales desarrolladores de esta biblioteca son vektah y neelance , mientras que neelance, adem谩s, funciona en la biblioteca graphql-go.

Hablemos de la biblioteca gqlgen con el supuesto de que ya tiene conocimientos b谩sicos de GraphQL.

Caracter铆sticas de Gqlgen


En la descripci贸n de gqlgen, puede encontrar lo que tenemos ante nosotros es una biblioteca para crear r谩pidamente servidores GraphQL estrictamente tipados en Golang. Esta frase me parece muy prometedora, ya que significa que cuando trabaje con esta biblioteca no me encontrar茅 con algo como la map[string]interface{} , ya que aqu铆 se utiliza un enfoque basado en una escritura estricta.

Adem谩s, esta biblioteca utiliza un enfoque basado en un esquema de datos. Esto significa que las API se describen utilizando el lenguaje de definici贸n de esquema GraphQL. Este lenguaje tiene sus propias potentes herramientas de generaci贸n de c贸digo que crean autom谩ticamente c贸digo GraphQL. En este caso, el programador solo puede implementar la l贸gica b谩sica de los m茅todos de interfaz correspondientes.

Este art铆culo est谩 dividido en dos partes. El primero est谩 dedicado a los m茅todos b谩sicos de trabajo y el segundo a los avanzados.

Los principales m茅todos de trabajo: configuraci贸n, solicitudes para recibir y cambiar datos, suscripciones


Nosotros, como aplicaci贸n experimental, utilizaremos un sitio donde los usuarios pueden publicar videos, agregar capturas de pantalla y revisiones, buscar videos y ver listas de registros asociados con otros registros. Comencemos a trabajar en este proyecto:

 mkdir -p $GOPATH/src/github.com/ridhamtarpara/go-graphql-demo/ 

Cree el siguiente archivo de esquema de datos ( schema.graphql ) en el directorio ra铆z del proyecto:

 type User {   id: ID!   name: String!   email: String! } type Video {   id: ID!   name: String!   description: String!   user: User!   url: String!   createdAt: Timestamp!   screenshots: [Screenshot]   related(limit: Int = 25, offset: Int = 0): [Video!]! } type Screenshot {   id: ID!   videoId: ID!   url: String! } input NewVideo {   name: String!   description: String!   userId: ID!   url: String! } type Mutation {   createVideo(input: NewVideo!): Video! } type Query {   Videos(limit: Int = 25, offset: Int = 0): [Video!]! } scalar Timestamp 

Aqu铆 se describen los modelos de datos b谩sicos, una mutaci贸n ( Mutation , descripci贸n de la solicitud de cambio de datos), que se utiliza para publicar nuevos archivos de video en el sitio, y una consulta ( Query ) para obtener una lista de todos los archivos de video. Lea m谩s sobre el esquema GraphQL aqu铆 . Adem谩s, aqu铆 declaramos uno de nuestros propios tipos de datos escalares. No estamos satisfechos con los 5 tipos de datos escalares est谩ndar ( Int , Float , String , Boolean e ID ) que se encuentran en GraphQL.

Si necesita usar sus propios tipos, puede declararlos en schema.graphql (en nuestro caso, este tipo es Timestamp ) y proporcionar sus definiciones en el c贸digo. Al usar la biblioteca gqlgen, debe proporcionar m茅todos para ordenar y desarmar para todos sus propios tipos escalares y configurar la asignaci贸n usando gqlgen.yml .

Cabe se帽alar que en la 煤ltima versi贸n de la biblioteca hubo un cambio importante. A saber, se elimin贸 la dependencia de los archivos binarios compilados. Por lo tanto, el archivo scripts/gqlgen.go debe agregarse al proyecto scripts/gqlgen.go siguiente contenido:

 // +build ignore package main import "github.com/99designs/gqlgen/cmd" func main() { cmd.Execute() } 

Despu茅s de eso, debe inicializar dep :

 dep init 

Ahora es el momento de aprovechar las capacidades de generaci贸n de c贸digo de la biblioteca. Le permiten crear todo el c贸digo aburrido repetitivo, que, sin embargo, no se puede llamar completamente desinteresado. Para iniciar el mecanismo de generaci贸n autom谩tica de c贸digo, ejecute el siguiente comando:

 go run scripts/gqlgen.go init 

Como resultado de su ejecuci贸n, se crear谩n los siguientes archivos:

  • gqlgen.yml : archivo de configuraci贸n para gestionar la generaci贸n de c贸digo.
  • generated.go : c贸digo generado.
  • models_gen.go : todos los modelos y tipos de datos del esquema proporcionado.
  • resolver.go : aqu铆 estar谩 el c贸digo que crea el programador.
  • server/server.go : punto de entrada con http.Handler para iniciar el servidor GraphQL.

Observe el modelo generado para el tipo de Video (archivo generated_video.go ):

 type Video struct { ID          string  `json:"id"` Name        string  `json:"name"` User        User  `json:"user"` URL         string  `json:"url"` CreatedAt   string  `json:"createdAt"` Screenshots []*Screenshot `json:"screenshots"` Related     []Video  `json:"related"` } 

Aqu铆 puede ver que la ID es una cadena, CreatedAt tambi茅n es una cadena. Otros modelos relacionados se configuran en consecuencia. Sin embargo, en aplicaciones reales esto no es necesario. Si est谩 utilizando cualquier tipo de datos SQL, entonces necesita, por ejemplo, que el campo ID sea, seg煤n la base de datos utilizada, un tipo int o int64 .

Por ejemplo, uso PostgreSQL en esta aplicaci贸n de demostraci贸n, por lo que, por supuesto, necesito que el campo ID sea 鈥嬧媎e tipo int y CreatedAt tipo time.Time . Esto lleva al hecho de que necesitamos definir nuestro propio modelo y decirle a gqlgen que necesitamos usar nuestro modelo en lugar de generar uno nuevo. Aqu铆 est谩 el contenido del archivo models.go :

 type Video struct { ID          int `json:"id"` Name        string `json:"name"` Description string    `json:"description"` User        User `json:"user"` URL         string `json:"url"` CreatedAt   time.Time `json:"createdAt"` Related     []Video } //    int  ID func MarshalID(id int) graphql.Marshaler { return graphql.WriterFunc(func(w io.Writer) {   io.WriteString(w, strconv.Quote(fmt.Sprintf("%d", id))) }) } //        func UnmarshalID(v interface{}) (int, error) { id, ok := v.(string) if !ok {   return 0, fmt.Errorf("ids must be strings") } i, e := strconv.Atoi(id) return int(i), e } func MarshalTimestamp(t time.Time) graphql.Marshaler { timestamp := t.Unix() * 1000 return graphql.WriterFunc(func(w io.Writer) {   io.WriteString(w, strconv.FormatInt(timestamp, 10)) }) } func UnmarshalTimestamp(v interface{}) (time.Time, error) { if tmpStr, ok := v.(int); ok {   return time.Unix(int64(tmpStr), 0), nil } return time.Time{}, errors.TimeStampError } 

Le decimos a la biblioteca que debe usar estos modelos (archivo gqlgen.yml ):

 schema: - schema.graphql exec: filename: generated.go model: filename: models_gen.go resolver: filename: resolver.go type: Resolver models: Video:   model: github.com/ridhamtarpara/go-graphql-demo/api.Video ID:   model: github.com/ridhamtarpara/go-graphql-demo/api.ID Timestamp:   model: github.com/ridhamtarpara/go-graphql-demo/api.Timestamp 

El punto de todo esto es que ahora tenemos nuestras propias definiciones para ID y Timestamp con m茅todos para calcular y desglosar y asignarlos en el archivo gqlgen.yml . Ahora que el usuario proporciona la cadena como ID , el m茅todo UnmarshalID() convierte esa cadena en un entero. Al enviar una respuesta, el m茅todo MarshalID() convierte el n煤mero en una cadena. Lo mismo sucede con Timestamp o con cualquier otro tipo escalar declarado por el programador.

Ahora es el momento de implementar la l贸gica de la aplicaci贸n. Abra el archivo resolver.go y agregue descripciones de mutaciones y consultas en 茅l. Ya existe un c贸digo repetitivo generado autom谩ticamente que debemos llenar de significado. Aqu铆 est谩 el c贸digo para este archivo:

 func (r *mutationResolver) CreateVideo(ctx context.Context, input NewVideo) (api.Video, error) { newVideo := api.Video{   URL:         input.URL,   Name:        input.Name,   CreatedAt:   time.Now().UTC(), } rows, err := dal.LogAndQuery(r.db, "INSERT INTO videos (name, url, user_id, created_at) VALUES($1, $2, $3, $4) RETURNING id",   input.Name, input.URL, input.UserID, newVideo.CreatedAt) defer rows.Close() if err != nil || !rows.Next() {   return api.Video{}, err } if err := rows.Scan(&newVideo.ID); err != nil {   errors.DebugPrintf(err)   if errors.IsForeignKeyError(err) {     return api.Video{}, errors.UserNotExist   }   return api.Video{}, errors.InternalServerError } return newVideo, nil } func (r *queryResolver) Videos(ctx context.Context, limit *int, offset *int) ([]api.Video, error) { var video api.Video var videos []api.Video rows, err := dal.LogAndQuery(r.db, "SELECT id, name, url, created_at, user_id FROM videos ORDER BY created_at desc limit $1 offset $2", limit, offset) defer rows.Close();   if err != nil {   errors.DebugPrintf(err)   return nil, errors.InternalServerError } for rows.Next() {   if err := rows.Scan(&video.ID, &video.Name, &video.URL, &video.CreatedAt, &video.UserID); err != nil {     errors.DebugPrintf(err)     return nil, errors.InternalServerError   }   videos = append(videos, video) } return videos, nil } 

Ahora probemos la mutaci贸n.

Mutaci贸n createVideo

Funciona! Pero, 驴por qu茅 no hay nada en la informaci贸n del usuario (objeto de user )? Cuando se trabaja con GraphQL, se aplican conceptos similares a la carga "perezosa" (perezosa) y "codiciosa" (ansiosa). Dado que este sistema es extensible, debe especificar qu茅 campos deben rellenarse "con avidez" y cu谩les son "perezosos".

Le suger铆 al equipo de la organizaci贸n donde trabajo la siguiente "regla de oro" que se aplica al trabajar con gqlgen: "No incluya en el modelo los campos que deben cargarse solo si el cliente los solicita".

En nuestro caso, necesito descargar datos sobre videoclips relacionados (e incluso informaci贸n del usuario) solo si el cliente solicita estos campos. Pero como incluimos estos campos en el modelo, gqlgen supone que proporcionamos estos datos al recibir informaci贸n sobre el video. Como resultado, ahora tenemos estructuras vac铆as.

A veces sucede que se necesita un cierto tipo de datos cada vez, por lo que no es pr谩ctico descargarlo usando una solicitud por separado. Para esto, para mejorar el rendimiento, puede usar algo como las uniones SQL. Una vez (esto, sin embargo, no se aplica al ejemplo considerado aqu铆), necesitaba cargar sus metadatos junto con el video. Estas entidades fueron almacenadas en diferentes lugares. Como resultado, si mi sistema recibi贸 una solicitud para descargar un video, ten铆a que hacer otra solicitud para obtener metadatos. Pero, dado que conoc铆a este requisito (es decir, sab铆a que el cliente y el video y sus metadatos siempre son necesarios en el lado del cliente), prefer铆 usar la t茅cnica de carga codiciosa para mejorar el rendimiento.

Reescribamos el modelo y generemos el c贸digo gqlgen nuevamente. Para no complicar la historia, solo escribimos m茅todos para el campo de user (archivo models.go ):

 type Video struct { ID          int `json:"id"` Name        string `json:"name"` Description string    `json:"description"` UserID      int `json:"-"` URL         string `json:"url"` CreatedAt   time.Time `json:"createdAt"` } 

UserID una UserID User y eliminamos la estructura de User . Ahora regenere el c贸digo:

 go run scripts/gqlgen.go -v 

Gracias a este comando, se crear谩n los siguientes m茅todos de interfaz para resolver estructuras indefinidas. Adem谩s, deber谩 determinar lo siguiente en el solucionador (archivo generated.go ):

 type VideoResolver interface { User(ctx context.Context, obj *api.Video) (api.User, error) Screenshots(ctx context.Context, obj *api.Video) ([]*api.Screenshot, error) Related(ctx context.Context, obj *api.Video, limit *int, offset *int) ([]api.Video, error) } 

Aqu铆 est谩 la definici贸n (archivo resolver.go ):

 func (r *videoResolver) User(ctx context.Context, obj *api.Video) (api.User, error) { rows, _ := dal.LogAndQuery(r.db,"SELECT id, name, email FROM users where id = $1", obj.UserID) defer rows.Close() if !rows.Next() {   return api.User{}, nil } var user api.User if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {   errors.DebugPrintf(err)   return api.User{}, errors.InternalServerError } return user, nil } 

Ahora, los resultados de la prueba de mutaci贸n se ver谩n como se muestra a continuaci贸n.


Mutaci贸n createVideo

Lo que acabamos de discutir son los fundamentos de GraphQL, habiendo dominado cu谩les, ya puedes escribir algo por tu cuenta. Sin embargo, antes de sumergirse en experimentos con GraphQL y Golang, ser谩 煤til hablar sobre las suscripciones, que est谩n directamente relacionadas con lo que estamos haciendo aqu铆.

鈻 Suscripciones


GraphQL proporciona la capacidad de suscribirse a los cambios de datos que ocurren en tiempo real. La biblioteca gqlgen permite, en tiempo real, usar sockets web, para trabajar con eventos de suscripci贸n.

La suscripci贸n debe describirse en el archivo schema.graphql . As铆 es como se ve la descripci贸n para suscribirse a un evento de publicaci贸n de video:

 type Subscription {   videoPublished: Video! } 

Ahora, ejecute la generaci贸n autom谩tica de c贸digo nuevamente:

 go run scripts/gqlgen.go -v 

Como ya se mencion贸, durante la creaci贸n autom谩tica de c贸digo en el archivo generated.go , se crea una interfaz que debe implementarse en el reconocedor. En nuestro caso, se ve as铆 (archivo resolver.go ):

 var videoPublishedChannel map[string]chan api.Video func init() { videoPublishedChannel = map[string]chan api.Video{} } type subscriptionResolver struct{ *Resolver } func (r *subscriptionResolver) VideoPublished(ctx context.Context) (<-chan api.Video, error) { id := randx.String(8) videoEvent := make(chan api.Video, 1) go func() {   <-ctx.Done() }() videoPublishedChannel[id] = videoEvent return videoEvent, nil } func (r *mutationResolver) CreateVideo(ctx context.Context, input NewVideo) (api.Video, error) { //   ... for _, observer := range videoPublishedChannel {   observer <- newVideo } return newVideo, nil } 

Ahora, al crear un nuevo video, debe activar un evento. En nuestro ejemplo, esto se hace en la l铆nea for _, observer := range videoPublishedChannel .

Ahora es el momento de verificar su suscripci贸n.


Verificar suscripci贸n

GraphQL, por supuesto, tiene ciertas capacidades valiosas, pero como dicen, no todo lo que brilla es oro. Es decir, estamos hablando del hecho de que alguien que usa GraphQL debe ocuparse de la autorizaci贸n, la complejidad de las solicitudes, el almacenamiento en cach茅, el problema de las solicitudes N + 1, la limitaci贸n de la velocidad de ejecuci贸n de consultas y algunas otras cosas. De lo contrario, un sistema desarrollado con GraphQL puede enfrentar una seria ca铆da en el rendimiento.

T茅cnicas avanzadas: autenticaci贸n, cargadores de datos, complejidad de consultas


Cada vez que leo manuales como este, tengo la sensaci贸n de que, despu茅s de dominarlos, aprendo todo lo que necesito saber sobre cierta tecnolog铆a y tengo la capacidad de resolver problemas de cualquier complejidad.

Pero cuando comienzo a trabajar en mis propios proyectos, generalmente me encuentro con situaciones imprevistas que parecen errores del servidor o solicitudes que se han estado ejecutando durante a帽os, o como algunas otras situaciones de punto muerto. Como resultado, para hacer esto, tengo que profundizar en lo que recientemente parec铆a perfectamente entendible. En este mismo manual, espero que esto se pueda evitar. Es por eso que en esta secci贸n veremos algunas t茅cnicas avanzadas para trabajar con GraphQL.

鈻 Autenticaci贸n


Cuando trabajamos con la API REST, tenemos un sistema de autenticaci贸n y herramientas de autorizaci贸n est谩ndar cuando trabajamos con un determinado punto final. Pero cuando se usa GraphQL, solo se usa un punto final, por lo tanto, las tareas de autenticaci贸n se pueden resolver usando directivas de esquema. Edite el archivo schema.graphql la siguiente manera:

 type Mutation {   createVideo(input: NewVideo!): Video! @isAuthenticated } directive @isAuthenticated on FIELD_DEFINITION 

Creamos la directiva isAuthenticated y la aplicamos a la suscripci贸n createVideo . Despu茅s de la pr贸xima sesi贸n de generaci贸n autom谩tica de c贸digo, debe definir una definici贸n para esta directiva. Ahora las directivas se implementan en forma de m茅todos de estructuras, y no en forma de interfaces, por lo que debemos describirlas. Edit茅 el c贸digo generado autom谩ticamente ubicado en el archivo server.go y cre茅 un m茅todo que devuelve la configuraci贸n GraphQL para el archivo server.go . Aqu铆 est谩 el archivo resolver.go :

 func NewRootResolvers(db *sql.DB) Config { c := Config{   Resolvers: &Resolver{     db: db,   }, } //   c.Directives.IsAuthenticated = func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {   ctxUserID := ctx.Value(UserIDCtxKey)   if ctxUserID != nil {     return next(ctx)   } else {     return nil, errors.UnauthorisedError   } } return c } 

Aqu铆 est谩 el archivo server.go :

 rootHandler:= dataloaders.DataloaderMiddleware(   db,   handler.GraphQL(     go_graphql_demo.NewExecutableSchema(go_graphql_demo.NewRootResolvers(db)   ) ) http.Handle("/query", auth.AuthMiddleware(rootHandler)) 

Leemos la ID usuario del contexto. 驴No te parece extra帽o? 驴C贸mo lleg贸 este significado al contexto y por qu茅 incluso apareci贸 en el contexto? El hecho es que gqlgen proporciona contextos de solicitud solo a nivel de implementaci贸n, por lo que no tenemos forma de leer ning煤n dato de solicitud HTTP, como encabezados o cookies, en reconocedores o directivas. Como resultado, debe agregar sus propios mecanismos intermedios al sistema, recibir estos datos y ponerlos en contexto.

Ahora necesitamos describir nuestro propio mecanismo de autenticaci贸n intermedio para obtener datos de autenticaci贸n de la solicitud y verificarlo.

No se define l贸gica aqu铆. En cambio, para los datos de autorizaci贸n, con fines de demostraci贸n, la ID usuario simplemente se pasa aqu铆. Este mecanismo se combina en server.go con un nuevo m茅todo de carga de configuraci贸n.

Ahora la descripci贸n de la directiva tiene sentido. No procesamos solicitudes de usuarios no autorizados en el c贸digo de middleware, ya que dichas solicitudes ser谩n procesadas por la directiva. As铆 es como se ve.


Trabajar con un usuario no autorizado.


Trabajar con un usuario autorizado

Al trabajar con directivas de esquema, incluso puede pasar argumentos:

 directive @hasRole(role: Role!) on FIELD_DEFINITION enum Role { ADMIN USER } 

鈻 Cargadores de datos


Me parece que todo esto parece bastante interesante. Descarga datos cuando los necesita. Los clientes tienen la capacidad de administrar datos; exactamente lo que se necesita se toma del almacenamiento. Pero todo tiene un precio.

驴Cu谩l es el precio a pagar por estas oportunidades? Echa un vistazo a los registros de descarga de todos los videos. Es decir, estamos hablando del hecho de que tenemos 8 videos y 5 usuarios.

 query{ Videos(limit: 10){   name   user{     name   } } } 


Detalles de descarga de video

 Query: Videos : SELECT id, name, description, url, created_at, user_id FROM videos ORDER BY created_at desc limit $1 offset $2 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 Resolver: User : SELECT id, name, email FROM users where id = $1 

驴Qu茅 est谩 pasando aqu铆? 驴Por qu茅 hay 9 solicitudes (1 solicitud est谩 asociada con la tabla de video y 8 - con la tabla de usuario)? Se ve horrible Mi coraz贸n casi se detuvo cuando pens茅 que nuestra API existente tendr铆a que ser reemplazada por esto ... Es cierto, los cargadores de datos pueden hacer frente por completo a este problema.

Esto se conoce como el problema N + 1. Estamos hablando del hecho de que hay una consulta para obtener todos los datos y para cada pieza de datos (N) habr谩 otra consulta en la base de datos.

Este es un problema muy serio cuando se trata de rendimiento y recursos: aunque estas solicitudes son paralelas, agotan los recursos del sistema.

Para resolver este problema, utilizaremos la biblioteca de carga de datos del autor de la biblioteca gqlgen. Esta biblioteca le permite generar c贸digo Go. Primero, genere un cargador de datos para la entidad User :

 go get github.com/vektah/dataloaden dataloaden github.com/ridhamtarpara/go-graphql-demo/api.User 

Tenemos a nuestra disposici贸n el archivo userloader_gen.go , que tiene m茅todos como Fetch , LoadAll y Prime .

Ahora, para obtener resultados generales, necesitamos definir el m茅todo Fetch (archivo dataloader.go ):

 func DataloaderMiddleware(db *sql.DB, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {   userloader := UserLoader{     wait : 1 * time.Millisecond,     maxBatch: 100,     fetch: func(ids []int) ([]*api.User, []error) {       var sqlQuery string       if len(ids) == 1 {         sqlQuery = "SELECT id, name, email from users WHERE id = ?"       } else {         sqlQuery = "SELECT id, name, email from users WHERE id IN (?)"       }       sqlQuery, arguments, err := sqlx.In(sqlQuery, ids)       if err != nil {         log.Println(err)       }       sqlQuery = sqlx.Rebind(sqlx.DOLLAR, sqlQuery)       rows, err := dal.LogAndQuery(db, sqlQuery, arguments...)       defer rows.Close();       if err != nil {         log.Println(err)       }       userById := map[int]*api.User{}       for rows.Next() {         user:= api.User{}         if err := rows.Scan(&user.ID, &user.Name, &user.Email); err != nil {           errors.DebugPrintf(err)           return nil, []error{errors.InternalServerError}         }         userById[user.ID] = &user       }       users := make([]*api.User, len(ids))       for i, id := range ids {         users[i] = userById[id]         i++       }       return users, nil     },   }   ctx := context.WithValue(r.Context(), CtxKey, &userloader)   r = r.WithContext(ctx)   next.ServeHTTP(w, r) }) } 

Aqu铆 esperamos 1 ms. antes de ejecutar la solicitud y recopilar las solicitudes en paquetes de hasta 100 solicitudes. Ahora, en lugar de ejecutar una solicitud para cada usuario individualmente, el cargador esperar谩 el tiempo especificado antes de acceder a la base de datos. A continuaci贸n, debe cambiar la l贸gica del reconocedor reconfigur谩ndola utilizando la solicitud para usar el cargador de datos (archivo resolver.go ):

 func (r *videoResolver) User(ctx context.Context, obj *api.Video) (api.User, error) { user, err := ctx.Value(dataloaders.CtxKey).(*dataloaders.UserLoader).Load(obj.UserID) return *user, err } 

As铆 es como se ven los registros despu茅s de eso en una situaci贸n similar a la descrita anteriormente:

 Query: Videos : SELECT id, name, description, url, created_at, user_id FROM videos ORDER BY created_at desc limit $1 offset $2 Dataloader: User : SELECT id, name, email from users WHERE id IN ($1, $2, $3, $4, $5) 

Aqu铆 solo se ejecutan dos consultas a la base de datos, como resultado, todos est谩n contentos. Es interesante observar que solo se env铆an 5 identificadores de usuario a la solicitud, aunque se solicitan datos para 8 videos. Esto sugiere que el cargador de datos elimina registros duplicados.


GraphQL API , . , API DOS-.

, .

Video , . GraphQL Video . . 鈥 .

, 鈥 :

 { Videos(limit: 10, offset: 0){   name   url   related(limit: 10, offset: 0){     name     url     related(limit: 10, offset: 0){       name       url       related(limit: 100, offset: 0){         name         url       }     }   } } } 

100, . (, , ) , .

gqlgen , . , ( handler.ComplexityLimit(300) ) GraphQL (300 ). , ( server.go ):

 rootHandler:= dataloaders.DataloaderMiddleware( db, handler.GraphQL(   go_graphql_demo.NewExecutableSchema(go_graphql_demo.NewRootResolvers(db)),   handler.ComplexityLimit(300) ), ) 

, , . 12. , , , ( , , , , ). resolver.go :

 func NewRootResolvers(db *sql.DB) Config { c := Config{   Resolvers: &Resolver{     db: db,   }, } //  countComplexity := func(childComplexity int, limit *int, offset *int) int {   return *limit * childComplexity } c.Complexity.Query.Videos = countComplexity c.Complexity.Video.Related = countComplexity //   c.Directives.IsAuthenticated = func(ctx context.Context, obj interface{}, next graphql.Resolver) (res interface{}, err error) {   ctxUserID := ctx.Value(UserIDCtxKey)   if ctxUserID != nil {     return next(ctx)   } else {     return nil, errors.UnauthorisedError   } } return c } 

, , .







, , related . , , , , .

Resumen


, , GitHub . . , , .

Estimados lectores! GraphQL , Go?

Source: https://habr.com/ru/post/444346/


All Articles