GraphQL et Golang

La technologie GraphQL au cours des derniÚres années, aprÚs que la société Facebook l'a transférée dans la catégorie open-source, est devenue trÚs populaire. L'auteur du document, dont nous publions la traduction aujourd'hui, dit qu'il a essayé de travailler avec GraphQL dans Node.js et, d'aprÚs sa propre expérience, était convaincu que cette technologie, en raison de ses capacités et de sa simplicité remarquables, n'attire pas accidentellement autant d'attention. Récemment, alors qu'il était engagé dans un nouveau projet, il est passé de Node.js à Golang. Puis il a décidé de tester la collaboration de Golang et GraphQL.



Informations préliminaires


Vous pouvez apprendre de la dĂ©finition officielle de GraphQL qu'il s'agit d'un langage de requĂȘte pour l'API et d'un runtime pour exĂ©cuter de telles requĂȘtes sur des donnĂ©es existantes. GraphQL fournit une description complĂšte et comprĂ©hensible des donnĂ©es dans une certaine API, permet aux clients de demander exactement les informations dont ils ont besoin, et rien de plus, simplifie le dĂ©veloppement de l'API au fil du temps et offre aux dĂ©veloppeurs des outils puissants.

Il n'y a pas beaucoup de bibliothÚques GraphQL pour Golang. En particulier, j'ai essayé des bibliothÚques comme Thunder , graphql , graphql-go et gqlgen . Je dois noter que le meilleur de tout ce que j'ai essayé était la bibliothÚque gqlgen.

La bibliothĂšque gqlgen est toujours en version bĂȘta, au moment de la rĂ©daction de ce document, il s'agissait de la version 0.7.2 . La bibliothĂšque Ă©volue rapidement. Ici vous pouvez dĂ©couvrir les plans de son dĂ©veloppement. Maintenant, le sponsor officiel de gqlgen est le projet 99designs , ce qui signifie que cette bibliothĂšque, trĂšs probablement, se dĂ©veloppera encore plus rapidement qu'auparavant. Les principaux dĂ©veloppeurs de cette bibliothĂšque sont vektah et neelance , tandis que neelance, en plus, travaille sur la bibliothĂšque graphql-go.

Parlons de la bibliothÚque gqlgen en supposant que vous avez déjà des connaissances de base de GraphQL.

Fonctionnalités de Gqlgen


Dans la description de gqlgen, vous pouvez découvrir ce que nous avons devant nous est une bibliothÚque pour créer rapidement des serveurs GraphQL strictement typés dans Golang. Cette phrase me semble trÚs prometteuse, car elle signifie que lorsque je travaille avec cette bibliothÚque, je ne rencontrerai pas quelque chose comme map[string]interface{} , car une approche basée sur un typage strict est utilisée ici.

De plus, cette bibliothÚque utilise une approche basée sur un schéma de données. Cela signifie que les API sont décrites à l'aide du langage de définition de schéma GraphQL. Ce langage possÚde ses propres outils de génération de code puissants qui créent automatiquement du code GraphQL. Dans ce cas, le programmeur ne peut implémenter que la logique de base des méthodes d'interface correspondantes.

Cet article est divisé en deux parties. Le premier est consacré aux méthodes de travail de base et le second aux méthodes avancées.

Les principales méthodes de travail: configuration, demandes de réception et de modification de données, abonnements


En tant qu'application expĂ©rimentale, nous utiliserons un site oĂč les utilisateurs pourront publier des vidĂ©os, ajouter des captures d'Ă©cran et des critiques, rechercher des vidĂ©os et afficher des listes d'enregistrements associĂ©s Ă  d'autres enregistrements. Commençons Ă  travailler sur ce projet:

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

Créez le fichier de schéma de données suivant ( schema.graphql ) dans le répertoire racine du projet:

 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 

Il dĂ©crit les modĂšles de donnĂ©es de base, une mutation ( Mutation , description de la demande de modification des donnĂ©es), qui est utilisĂ©e pour publier de nouveaux fichiers vidĂ©o sur le site, et une requĂȘte ( Query ) pour obtenir une liste de tous les fichiers vidĂ©o. En savoir plus sur le schĂ©ma GraphQL ici . De plus, nous avons dĂ©clarĂ© ici l'un de nos propres types de donnĂ©es scalaires. Nous ne sommes pas satisfaits des 5 types de donnĂ©es scalaires standard ( Int , Float , String , Boolean et ID ) qui sont dans GraphQL.

Si vous devez utiliser vos propres types, vous pouvez les déclarer dans schema.graphql (dans notre cas, ce type est Timestamp ) et fournir leurs définitions dans le code. Lorsque vous utilisez la bibliothÚque gqlgen, vous devez fournir des méthodes de marshaling et de démarshaling pour tous vos propres types scalaires et configurer le mappage à l'aide de gqlgen.yml .

Il convient de noter que dans la derniĂšre version de la bibliothĂšque, il y a eu un changement important. À savoir, la dĂ©pendance Ă  l'Ă©gard des fichiers binaires compilĂ©s en a Ă©tĂ© supprimĂ©e. Par consĂ©quent, le fichier scripts/gqlgen.go doit ĂȘtre ajoutĂ© au projet scripts/gqlgen.go contenu suivant:

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

AprĂšs cela, vous devez initialiser dep :

 dep init 

Il est maintenant temps de profiter des capacitĂ©s de gĂ©nĂ©ration de code de la bibliothĂšque. Ils vous permettent de crĂ©er tout le code passe-partout ennuyeux, qui, cependant, ne peut pas ĂȘtre qualifiĂ© de complĂštement inintĂ©ressant. Pour dĂ©marrer le mĂ©canisme de gĂ©nĂ©ration automatique de code, exĂ©cutez la commande suivante:

 go run scripts/gqlgen.go init 

À la suite de son exĂ©cution, les fichiers suivants seront créés:

  • gqlgen.yml : fichier de configuration pour gĂ©rer la gĂ©nĂ©ration de code.
  • generated.go : code gĂ©nĂ©rĂ©.
  • models_gen.go : tous les modĂšles et types de donnĂ©es du schĂ©ma fourni.
  • resolver.go : voici le code que le programmeur crĂ©e.
  • server/server.go : point d'entrĂ©e avec http.Handler pour dĂ©marrer le serveur GraphQL.

Jetez un Ɠil au modĂšle gĂ©nĂ©rĂ© pour le type de Video (fichier 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"` } 

Ici, vous pouvez voir que l' ID est une chaßne, CreatedAt est également une chaßne. Les autres modÚles associés sont configurés en conséquence. Cependant, dans les applications réelles, cela n'est pas nécessaire. Si vous utilisez n'importe quel type de données SQL, vous devez, par exemple, que le champ ID soit, selon la base de données utilisée, de type int ou int64 .

Par exemple, j'utilise PostgreSQL dans cette application de démonstration, donc bien sûr, j'ai besoin que le champ ID soit de type int et CreatedAt type time.Time . Cela conduit au fait que nous devons définir notre propre modÚle et dire à gqlgen que nous devons utiliser notre modÚle au lieu d'en générer un nouveau. Voici le contenu du fichier 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 } 

Nous indiquons Ă  la bibliothĂšque qu'elle doit utiliser ces modĂšles (fichier 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 

Le point de tout cela est que nous avons maintenant nos propres dĂ©finitions d' ID et d' Timestamp avec des mĂ©thodes de marshaling et de dĂ©marshaling et de mappage dans le fichier gqlgen.yml . Maintenant que l'utilisateur fournit la chaĂźne en tant UnmarshalID() , la mĂ©thode UnmarshalID() convertit cette chaĂźne en un entier. Lors de l'envoi d'une rĂ©ponse, la mĂ©thode MarshalID() convertit le nombre en chaĂźne. La mĂȘme chose se produit avec Timestamp ou avec tout autre type scalaire dĂ©clarĂ© par le programmeur.

Il est maintenant temps de mettre en Ɠuvre la logique d'application. Ouvrez le fichier resolver.go et ajoutez-y des descriptions des mutations et des requĂȘtes. Il existe dĂ©jĂ  un code passe-partout gĂ©nĂ©rĂ© automatiquement que nous devons remplir de sens. Voici le code de ce fichier:

 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 } 

Maintenant, testons la mutation.

Mutation createVideo

Ça marche! Mais pourquoi n'y a-t-il rien dans les informations user (objet user )? Lorsque vous travaillez avec GraphQL, des concepts similaires au chargement «paresseux» (paresseux) et «gourmand» (impatient) sont applicables. Étant donnĂ© que ce systĂšme est extensible, vous devez spĂ©cifier quels champs doivent ĂȘtre remplis «avec avidité» et lesquels sont «paresseux».

J'ai suggĂ©rĂ© Ă  l'Ă©quipe de l'organisation oĂč je travaille la «rĂšgle d'or» suivante qui s'applique lors de l'utilisation de gqlgen: «N'incluez pas dans le modĂšle les champs qui doivent ĂȘtre chargĂ©s uniquement s'ils sont demandĂ©s par le client.»

Dans notre cas, je dois tĂ©lĂ©charger des donnĂ©es sur les clips vidĂ©o associĂ©s (et mĂȘme des informations utilisateur) uniquement si le client demande ces champs. Mais puisque nous avons inclus ces champs dans le modĂšle, gqlgen suppose que nous fournissons ces donnĂ©es en recevant des informations sur la vidĂ©o. En consĂ©quence, nous obtenons maintenant des structures vides.

Parfois, il arrive qu'un certain type de données soit nécessaire à chaque fois, il est donc impossible de le télécharger à l'aide d'une demande distincte. Pour cela, afin d'améliorer les performances, vous pouvez utiliser quelque chose comme des jointures SQL. Une fois (cela ne s'applique toutefois pas à l'exemple considéré ici), j'ai dû télécharger ses métadonnées avec la vidéo. Ces entités étaient stockées à différents endroits. Par conséquent, si mon systÚme recevait une demande de téléchargement d'une vidéo, je devais faire une autre demande pour obtenir des métadonnées. Mais, comme je connaissais cette exigence (c'est-à-dire que je savais que le client et la vidéo et ses métadonnées sont toujours nécessaires du cÎté client), j'ai préféré utiliser la technique de chargement gourmand pour améliorer les performances.

Réécrivons le modÚle et générons à nouveau le code gqlgen. Afin de ne pas compliquer l'histoire, nous écrivons uniquement des méthodes pour le champ user (fichier 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"` } 

Nous avons ajouté un UserID et supprimé la structure User . Régénérez maintenant le code:

 go run scripts/gqlgen.go -v 

Grùce à cette commande, les méthodes d'interface suivantes seront créées pour résoudre les structures non définies. De plus, vous devrez déterminer les éléments suivants dans le résolveur (fichier généré.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) } 

Voici la définition (fichier 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 } 

Maintenant, les résultats du test de mutation se présenteront comme indiqué ci-dessous.


Mutation createVideo

Ce que nous venons de discuter, ce sont les principes fondamentaux de GraphQL, aprĂšs avoir maĂźtrisĂ© lequel, vous pouvez dĂ©jĂ  Ă©crire quelque chose de vous-mĂȘme. Cependant, avant de vous lancer dans des expĂ©riences avec GraphQL et Golang, il sera utile de parler des abonnements, qui sont directement liĂ©s Ă  ce que nous faisons ici.

▍ Abonnements


GraphQL offre la possibilité de s'abonner aux modifications de données qui se produisent en temps réel. La bibliothÚque gqlgen permet, en temps réel, à l'aide de sockets web, de travailler avec des événements d'abonnement.

L'abonnement doit ĂȘtre dĂ©crit dans le fichier schema.graphql . Voici la description de l'abonnement Ă  l'Ă©vĂ©nement de publication vidĂ©o:

 type Subscription {   videoPublished: Video! } 

Maintenant, exécutez à nouveau la génération automatique de code:

 go run scripts/gqlgen.go -v 

Comme dĂ©jĂ  mentionnĂ©, lors de la crĂ©ation automatique de code dans le fichier gĂ©nĂ©rĂ©.go, une interface est créée qui doit ĂȘtre implĂ©mentĂ©e dans le module de reconnaissance. Dans notre cas, cela ressemble Ă  ceci (fichier 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 } 

Maintenant, lorsque vous créez une nouvelle vidéo, vous devez déclencher un événement. Dans notre exemple, cela se fait dans la ligne for _, observer := range videoPublishedChannel .

Il est maintenant temps de vérifier votre abonnement.


Vérifier l'abonnement

GraphQL, bien sĂ»r, possĂšde certaines capacitĂ©s prĂ©cieuses, mais comme on dit, tout ce qui brille n'est pas or. À savoir, nous parlons du fait que quelqu'un qui utilise GraphQL doit prendre soin de l'autorisation, de la complexitĂ© des demandes, de la mise en cache, du problĂšme des demandes N + 1, de la limitation de la vitesse d'exĂ©cution des requĂȘtes et d'autres choses. Sinon, un systĂšme dĂ©veloppĂ© Ă  l'aide de GraphQL peut faire face Ă  une baisse sĂ©rieuse des performances.

Techniques avancĂ©es: authentification, chargeurs de donnĂ©es, complexitĂ© des requĂȘtes


Chaque fois que je lis des manuels comme celui-ci, j'ai l'impression qu'aprÚs les avoir maßtrisés, j'apprends tout ce que je dois savoir sur une certaine technologie et j'ai la capacité de résoudre des problÚmes de toute complexité.

Mais lorsque je commence Ă  travailler sur mes propres projets, je me retrouve gĂ©nĂ©ralement dans des situations imprĂ©vues qui ressemblent Ă  des erreurs de serveur ou Ă  des demandes en cours depuis des siĂšcles, ou Ă  d'autres situations de blocage. Par consĂ©quent, pour ce faire, je dois mieux me plonger dans ce qui, tout rĂ©cemment, semblait parfaitement comprĂ©hensible. Dans ce mĂȘme manuel, j'espĂšre que cela pourra ĂȘtre Ă©vitĂ©. C'est pourquoi dans cette section, nous examinerons quelques techniques avancĂ©es pour travailler avec GraphQL.

▍ Authentification


Lorsque vous travaillez avec l'API REST, nous avons un systĂšme d'authentification et des outils d'autorisation standard lorsque vous travaillez avec un certain point de terminaison. Mais lorsque vous utilisez GraphQL, un seul point de terminaison est utilisĂ©, par consĂ©quent, les tĂąches d'authentification peuvent ĂȘtre rĂ©solues Ă  l'aide de directives de schĂ©ma. Modifiez le fichier schema.graphql comme suit:

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

Nous avons créé la directive isAuthenticated et l' isAuthenticated appliquée à l'abonnement createVideo . AprÚs la prochaine session de génération automatique de code, vous devez définir une définition pour cette directive. Maintenant, les directives sont implémentées sous forme de méthodes de structures, et non sous forme d'interfaces, nous devons donc les décrire. J'ai édité le code généré automatiquement situé dans le fichier server.go et créé une méthode qui renvoie la configuration GraphQL pour le fichier server.go . Voici le fichier 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 } 

Voici le fichier server.go :

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

Nous lisons l' ID utilisateur dans le contexte. Ne trouvez-vous pas cela Ă©trange? Comment ce sens est-il entrĂ© dans son contexte et pourquoi est-il mĂȘme apparu dans son contexte? Le fait est que gqlgen fournit des contextes de requĂȘte uniquement au niveau de l'implĂ©mentation, nous n'avons donc aucun moyen de lire les donnĂ©es de requĂȘte HTTP, telles que les en-tĂȘtes ou les cookies, dans les programmes de reconnaissance ou les directives. Par consĂ©quent, vous devez ajouter vos propres mĂ©canismes intermĂ©diaires au systĂšme, recevoir ces donnĂ©es et les mettre en contexte.

Nous devons maintenant décrire notre propre mécanisme d'authentification intermédiaire pour obtenir les données d'authentification de la demande et les vérifier.

Aucune logique n'est définie ici. Au lieu de cela, pour les données d'autorisation, à des fins de démonstration, l' ID utilisateur ID simplement transmis ici. Ce mécanisme est ensuite combiné dans server.go avec une nouvelle méthode de chargement de configuration.

Maintenant, la description de la directive a du sens. Nous ne traitons pas les demandes d'utilisateurs non autorisés dans le code du middleware, car ces demandes seront traitées par la directive. Voici à quoi ça ressemble.


Travailler avec un utilisateur non autorisé


Travailler avec un utilisateur autorisé

Lorsque vous travaillez avec des directives de schĂ©ma, vous pouvez mĂȘme passer des arguments:

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

▍ Chargeurs de donnĂ©es


Il me semble que tout cela semble assez intéressant. Vous téléchargez des données lorsque vous en avez besoin. Les clients ont la capacité de gérer les données; exactement ce qui est nécessaire provient du stockage. Mais tout a un prix.

Quel est le prix Ă  payer pour ces opportunitĂ©s? Jetez un Ɠil aux journaux de tĂ©lĂ©chargement de toutes les vidĂ©os. A savoir, nous parlons du fait que nous avons 8 vidĂ©os et 5 utilisateurs.

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


Détails du téléchargement de la vidéo

 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 

Que se passe-t-il ici? Pourquoi y a-t-il 9 demandes (1 demande est associĂ©e Ă  la table vidĂ©o et 8 - Ă  la table utilisateur)? Ça a l'air horrible. Mon cƓur s'est presque arrĂȘtĂ© quand j'ai pensĂ© que notre API existante devrait ĂȘtre remplacĂ©e par ceci ... Vrai, les chargeurs de donnĂ©es peuvent complĂštement faire face Ă  ce problĂšme.

Ceci est connu comme le problĂšme N + 1. Nous parlons du fait qu'il y a une requĂȘte pour obtenir toutes les donnĂ©es et pour chaque Ă©lĂ©ment de donnĂ©es (N) il y aura une autre requĂȘte dans la base de donnĂ©es.

Il s'agit d'un problĂšme trĂšs grave en termes de performances et de ressources: bien que ces demandes soient parallĂšles, elles drainent les ressources systĂšme.

Pour résoudre ce problÚme, nous utiliserons la bibliothÚque dataloaden de l'auteur de la bibliothÚque gqlgen. Cette bibliothÚque vous permet de générer du code Go. Tout d'abord, générez un chargeur de données pour l'entité User :

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

Nous avons à notre disposition le fichier userloader_gen.go , qui a des méthodes comme Fetch , LoadAll et Prime .

Maintenant, pour obtenir des résultats généraux, nous devons définir la méthode Fetch (fichier 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) }) } 

Ici, nous attendons 1 ms. avant d'exécuter la demande et de collecter les demandes dans des packages contenant jusqu'à 100 demandes. Maintenant, au lieu d'exécuter une demande pour chaque utilisateur individuellement, le chargeur attendra le temps spécifié avant d'accéder à la base de données. Ensuite, vous devez modifier la logique de reconnaissance en la reconfigurant à l'aide de la demande d'utilisation du chargeur de données (fichier 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 } 

Voici comment les journaux s'en occupent dans une situation similaire à celle décrite ci-dessus:

 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) 

Seules deux requĂȘtes de base de donnĂ©es sont exĂ©cutĂ©es ici, par consĂ©quent, tout le monde est maintenant satisfait. Il est intĂ©ressant de noter que seuls 5 identifiants d'utilisateurs sont envoyĂ©s Ă  la demande, bien que des donnĂ©es soient demandĂ©es pour 8 vidĂ©os. Cela suggĂšre que le chargeur de donnĂ©es supprime les enregistrements en double.

▍


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 . , , , , .

Résumé


, , GitHub . . , , .

Chers lecteurs! GraphQL , Go?

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


All Articles