GraphQL und Golang

Die GraphQL-Technologie ist in den letzten Jahren, nachdem das Unternehmen Facebook sie in die Kategorie Open Source übertragen hat, sehr beliebt geworden. Der Autor des Materials, dessen Übersetzung wir heute veröffentlichen, sagt, er habe versucht, mit GraphQL in Node.js zu arbeiten, und war aus eigener Erfahrung davon überzeugt, dass diese Technologie aufgrund ihrer bemerkenswerten Fähigkeiten und Einfachheit nicht versehentlich so viel Aufmerksamkeit erregt. Kürzlich wechselte er bei einem neuen Projekt von Node.js zu Golang. Dann beschloss er, die Zusammenarbeit von Golang und GraphQL zu testen.



Vorläufige Informationen


Sie können der offiziellen GraphQL-Definition entnehmen, dass dies eine Abfragesprache für die API und eine Laufzeit für die Ausführung solcher Abfragen für vorhandene Daten ist. GraphQL bietet eine vollständige und verständliche Beschreibung der Daten in einer bestimmten API, ermöglicht es Kunden, genau die Informationen anzufordern, die sie benötigen, und nichts weiter vereinfacht die Entwicklung der API im Laufe der Zeit und bietet Entwicklern leistungsstarke Tools.

Es gibt nicht viele GraphQL-Bibliotheken für Golang. Insbesondere habe ich Bibliotheken wie Thunder , graphql , graphql-go und gqlgen ausprobiert . Ich sollte beachten, dass das Beste von allem, was ich versucht habe, die gqlgen-Bibliothek war.

Die gqlgen-Bibliothek befindet sich noch in der Beta- Phase . Zum Zeitpunkt des Schreibens dieses Materials war es Version 0.7.2 . Die Bibliothek entwickelt sich rasant weiter. Hier erfahren Sie mehr über Pläne für die Entwicklung. Jetzt ist der offizielle Sponsor von gqlgen das 99designs- Projekt, was bedeutet, dass sich diese Bibliothek möglicherweise noch schneller als zuvor entwickeln wird. Die Hauptentwickler dieser Bibliothek sind vektah und neelance , während neelance zusätzlich an der graphql-go-Bibliothek arbeitet.

Lassen Sie uns über die gqlgen-Bibliothek sprechen, basierend auf der Annahme, dass Sie bereits Grundkenntnisse in GraphQL haben.

Gqlgen Funktionen


In der Beschreibung von gqlgen finden Sie heraus, was wir vor uns haben, eine Bibliothek zum schnellen Erstellen streng typisierter GraphQL-Server in Golang. Dieser Satz erscheint mir sehr vielversprechend, da er bedeutet, dass ich bei der Arbeit mit dieser Bibliothek nicht auf so etwas wie map[string]interface{} , da hier ein Ansatz verwendet wird, der auf strenger Typisierung basiert.

Darüber hinaus verwendet diese Bibliothek einen Ansatz, der auf einem Datenschema basiert. Dies bedeutet, dass die APIs mithilfe der GraphQL Schema Definition Language beschrieben werden . Diese Sprache verfügt über eigene leistungsstarke Tools zur Codegenerierung, die automatisch GraphQL-Code erstellen. In diesem Fall kann der Programmierer nur die Grundlogik der entsprechenden Schnittstellenmethoden implementieren.

Dieser Artikel ist in zwei Teile gegliedert. Die erste widmet sich den grundlegenden Arbeitsmethoden und die zweite den fortgeschrittenen.

Die wichtigsten Arbeitsmethoden: Einrichten, Anforderungen zum Empfangen und Ändern von Daten, Abonnements


Als experimentelle Anwendung verwenden wir eine Website, auf der Benutzer Videos veröffentlichen, Screenshots und Bewertungen hinzufügen, nach Videos suchen und Listen mit Datensätzen anzeigen können, die mit anderen Datensätzen verknüpft sind. Beginnen wir mit der Arbeit an diesem Projekt:

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

Erstellen Sie die folgende Datenschemadatei ( schema.graphql ) im Stammverzeichnis des Projekts:

 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 

Hier werden die grundlegenden Datenmodelle beschrieben, eine Mutation ( Mutation , Beschreibung der Anforderung zur Datenänderung), mit der neue Videodateien auf der Site veröffentlicht werden, und eine Abfrage ( Query ), um eine Liste aller Videodateien abzurufen. Lesen Sie hier mehr über das GraphQL-Schema. Außerdem haben wir hier einen unserer eigenen skalaren Datentypen deklariert. Wir sind mit den 5 standardmäßigen skalaren Datentypen ( Int , Float , String , Boolean und ID ) in GraphQL nicht zufrieden.

Wenn Sie Ihre eigenen Typen verwenden müssen, können Sie diese in schema.graphql (in unserem Fall ist dieser Typ Timestamp ) und ihre Definitionen im Code schema.graphql . Wenn Sie die gqlgen-Bibliothek verwenden, müssen Sie Methoden zum Marshalling und Unmarshaling für alle Ihre eigenen Skalartypen bereitstellen und die Zuordnung mit gqlgen.yml konfigurieren.

Es ist zu beachten, dass es in der neuesten Version der Bibliothek eine wichtige Änderung gab. Die Abhängigkeit von kompilierten Binärdateien wurde nämlich entfernt. Daher sollte die Datei scripts/gqlgen.go folgenden Inhalt zum Projekt hinzugefügt werden:

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

Danach müssen Sie dep initialisieren:

 dep init 

Jetzt ist es Zeit, die Codegenerierungsfunktionen der Bibliothek zu nutzen. Mit ihnen können Sie den gesamten langweiligen Boilerplate-Code erstellen, der jedoch nicht als völlig uninteressant bezeichnet werden kann. Führen Sie den folgenden Befehl aus, um den automatischen Codegenerierungsmechanismus zu starten:

 go run scripts/gqlgen.go init 

Als Ergebnis seiner Ausführung werden die folgenden Dateien erstellt:

  • gqlgen.yml : Konfigurationsdatei zum Verwalten der Codegenerierung.
  • generated.go : generierter Code.
  • models_gen.go : Alle Modelle und Datentypen des bereitgestellten Schemas.
  • resolver.go : Hier ist der Code, den der Programmierer erstellt.
  • server/server.go : Einstiegspunkt mit http.Handler zum Starten des GraphQL-Servers.

Sehen Sie sich das generierte Modell für den Video (Datei 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"` } 

Hier können Sie sehen, dass die ID eine Zeichenfolge ist, CreatedAt ist auch eine Zeichenfolge. Andere verwandte Modelle sind entsprechend konfiguriert. In realen Anwendungen ist dies jedoch nicht erforderlich. Wenn Sie einen beliebigen Typ von SQL-Daten verwenden, benötigen Sie beispielsweise, dass das ID Feld je nach verwendeter Datenbank ein int64 oder int64 .

Zum Beispiel verwende ich PostgreSQL in dieser Demo-Anwendung, daher muss das ID Feld vom Typ int und CreatedAt Typ time.Time . Dies führt dazu, dass wir unser eigenes Modell definieren und gqlgen mitteilen müssen, dass wir unser Modell verwenden müssen, anstatt ein neues zu generieren. Hier ist der Inhalt der Datei 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 } 

Wir teilen der Bibliothek mit, dass sie diese Modelle verwenden soll (Datei 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 

Der Sinn all dessen ist, dass wir jetzt unsere eigenen Definitionen für ID und Timestamp mit Methoden zum Marshalling und Unmarshaling sowie zur Zuordnung in der Datei gqlgen.yml . UnmarshalID() der Benutzer die Zeichenfolge als ID , UnmarshalID() die UnmarshalID() -Methode diese Zeichenfolge in eine Ganzzahl. Beim Senden einer Antwort konvertiert die MarshalID() -Methode die Zahl in eine Zeichenfolge. Das gleiche passiert mit Timestamp oder einem anderen vom Programmierer deklarierten Skalartyp.

Jetzt ist es Zeit, die Anwendungslogik zu implementieren. Öffnen Sie die Datei resolver.go und fügen Sie Beschreibungen von Mutationen und Abfragen hinzu. Es gibt bereits einen automatisch generierten Boilerplate-Code, den wir mit Bedeutung füllen müssen. Hier ist der Code für diese Datei:

 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 } 

Testen wir nun die Mutation.

Mutation createVideo

Es funktioniert! Aber warum enthält die Benutzerinformation ( user ) nichts? Bei der Arbeit mit GraphQL gelten Konzepte, die dem "faulen" (faulen) und "gierigen" (eifrigen) Laden ähneln. Da dieses System erweiterbar ist, müssen Sie angeben, welche Felder "gierig" und welche "faul" ausgefüllt werden müssen.

Ich habe dem Team in der Organisation, in der ich arbeite, die folgende „goldene Regel“ vorgeschlagen, die für die Arbeit mit gqlgen gilt: „Nicht in die Modellfelder aufnehmen, die nur geladen werden müssen, wenn sie vom Kunden angefordert werden.“

In unserem Fall muss ich nur dann Daten zu verwandten Videoclips (und sogar Benutzerinformationen) herunterladen, wenn der Client diese Felder anfordert. Da wir diese Felder jedoch in das Modell aufgenommen haben, geht gqlgen davon aus, dass wir diese Daten bereitstellen, indem wir Informationen über das Video erhalten. Als Ergebnis erhalten wir jetzt leere Strukturen.

Manchmal kommt es vor, dass jedes Mal ein bestimmter Datentyp benötigt wird. Daher ist es unpraktisch, ihn über eine separate Anforderung herunterzuladen. Um die Leistung zu verbessern, können Sie dazu beispielsweise SQL-Joins verwenden. Einmal (dies gilt jedoch nicht für das hier betrachtete Beispiel) musste ich seine Metadaten zusammen mit dem Video hochladen. Diese Entitäten wurden an verschiedenen Orten gespeichert. Wenn mein System eine Anfrage zum Herunterladen eines Videos erhielt, musste ich eine weitere Anfrage stellen, um Metadaten abzurufen. Da ich jedoch über diese Anforderung Bescheid wusste (dh wusste, dass Client und Video sowie deren Metadaten immer auf der Clientseite benötigt werden), zog ich es vor, die Technik des gierigen Ladens zu verwenden, um die Leistung zu verbessern.

Lassen Sie uns das Modell neu schreiben und den gqlgen-Code erneut generieren. Um die Geschichte nicht zu komplizieren, schreiben wir nur Methoden für das user (Datei 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"` } 

Wir haben eine User UserID hinzugefügt und die User entfernt. Generieren Sie nun den Code neu:

 go run scripts/gqlgen.go -v 

Dank dieses Befehls werden die folgenden Schnittstellenmethoden erstellt, um undefinierte Strukturen aufzulösen. Darüber hinaus müssen Sie im Resolver ( generated.go Datei) Folgendes festlegen:

 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) } 

Hier ist die Definition ( resolver.go Datei):

 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 } 

Die Mutationstestergebnisse sehen nun wie unten gezeigt aus.


Mutation createVideo

Was wir gerade besprochen haben, sind die Grundlagen von GraphQL, nachdem Sie diese beherrschen, können Sie bereits etwas Eigenes schreiben. Bevor Sie jedoch in Experimente mit GraphQL und Golang eintauchen, sollten Sie über Abonnements sprechen, die in direktem Zusammenhang mit dem stehen, was wir hier tun.

▍ Abonnements


GraphQL bietet die Möglichkeit, Datenänderungen zu abonnieren, die in Echtzeit auftreten. Mit der gqlgen-Bibliothek können Sie mithilfe von Web-Sockets in Echtzeit mit Abonnementereignissen arbeiten.

Das Abonnement muss in der Datei schema.graphql werden. Hier ist die Beschreibung des Abonnements für das Video-Publishing-Ereignis:

 type Subscription {   videoPublished: Video! } 

Führen Sie nun die automatische Codegenerierung erneut aus:

 go run scripts/gqlgen.go -v 

Wie bereits erwähnt, wird während der automatischen Erstellung von Code in der Datei generated.go eine Schnittstelle erstellt, die im Erkenner implementiert werden muss. In unserem Fall sieht es so aus ( resolver.go Datei):

 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 } 

Wenn Sie jetzt ein neues Video erstellen, müssen Sie ein Ereignis auslösen. In unserem Beispiel erfolgt dies in der Zeile for _, observer := range videoPublishedChannel .

Jetzt ist es Zeit, Ihr Abonnement zu überprüfen.


Abonnement überprüfen

GraphQL hat natürlich einige wertvolle Fähigkeiten, aber wie sie sagen, ist nicht alles, was glänzt, Gold. Wir sprechen nämlich über die Tatsache, dass sich jemand, der GraphQL verwendet, um die Autorisierung, die Komplexität von Anforderungen, das Caching, das Problem von N + 1-Anforderungen, die Begrenzung der Geschwindigkeit der Abfrageausführung und einige andere Dinge kümmern muss. Andernfalls kann ein mit GraphQL entwickeltes System einem erheblichen Leistungsabfall ausgesetzt sein.

Fortgeschrittene Techniken: Authentifizierung, Datenlader, Abfragekomplexität


Jedes Mal, wenn ich solche Handbücher lese, habe ich das Gefühl, dass ich, nachdem ich sie beherrsche, alles lerne, was ich über eine bestimmte Technologie wissen muss, und die Fähigkeit bekomme, Probleme jeder Komplexität zu lösen.

Wenn ich jedoch anfange, an meinen eigenen Projekten zu arbeiten, gerate ich normalerweise in unvorhergesehene Situationen, die wie Serverfehler oder Anforderungen aussehen, die seit Ewigkeiten ausgeführt werden, oder wie andere Deadlock-Situationen. Um dies zu tun, muss ich mich daher besser mit dem befassen, was in letzter Zeit vollkommen verständlich schien. In diesem Handbuch hoffe ich, dass dies vermieden werden kann. Aus diesem Grund werden wir uns in diesem Abschnitt mit einigen fortgeschrittenen Techniken für die Arbeit mit GraphQL befassen.

▍ Authentifizierung


Bei der Arbeit mit der REST-API verfügen wir über ein Authentifizierungssystem und Standardautorisierungstools, wenn Sie mit einem bestimmten Endpunkt arbeiten. Bei Verwendung von GraphQL wird jedoch nur ein Endpunkt verwendet. Daher können Authentifizierungsaufgaben mithilfe von Schemaanweisungen gelöst werden. Bearbeiten Sie die Datei schema.graphql wie folgt:

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

Wir haben die isAuthenticated Direktive erstellt und auf das createVideo Abonnement createVideo . Nach der nächsten Sitzung zur automatischen Codegenerierung müssen Sie eine Definition für diese Anweisung definieren. Jetzt werden Direktiven in Form von Methoden von Strukturen implementiert und nicht in Form von Schnittstellen, daher müssen wir sie beschreiben. Ich habe den automatisch generierten Code in der Datei server.go bearbeitet und eine Methode erstellt, die die GraphQL-Konfiguration für die Datei server.go . Hier ist die Datei 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 } 

Hier ist die Datei server.go :

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

Wir lesen die Benutzer- ID aus dem Kontext. Findest du das nicht seltsam? Wie kam diese Bedeutung in den Kontext und warum erschien sie überhaupt im Kontext? Tatsache ist, dass gqlgen Anforderungskontexte nur auf Implementierungsebene bereitstellt, sodass wir keine Möglichkeit haben, HTTP-Anforderungsdaten wie Header oder Cookies in Erkennern oder Anweisungen zu lesen. Daher müssen Sie dem System Ihre eigenen Zwischenmechanismen hinzufügen, diese Daten empfangen und in einen Kontext setzen.

Jetzt müssen wir unseren eigenen Zwischenauthentifizierungsmechanismus beschreiben, um Authentifizierungsdaten aus der Anforderung zu erhalten und diese zu überprüfen.

Hier ist keine Logik definiert. Stattdessen wird zu Berechtigungsdaten zu Demonstrationszwecken die Benutzer- ID einfach hier übergeben. Dieser Mechanismus wird dann in server.go mit einer neuen Konfigurationslademethode kombiniert.

Nun macht die Beschreibung der Richtlinie Sinn. Wir verarbeiten keine nicht autorisierten Benutzeranfragen im Middleware-Code, da solche Anfragen von der Direktive verarbeitet werden. So sieht es aus.


Arbeiten Sie mit einem nicht autorisierten Benutzer


Arbeiten Sie mit einem autorisierten Benutzer

Wenn Sie mit Schema-Direktiven arbeiten, können Sie sogar Argumente übergeben:

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

▍Datenlader


Es scheint mir, dass das alles ziemlich interessant aussieht. Sie laden Daten herunter, wenn Sie sie benötigen. Clients haben die Möglichkeit, Daten zu verwalten. Genau das, was benötigt wird, wird aus dem Speicher entnommen. Aber alles hat einen Preis.

Was ist der Preis für diese Möglichkeiten? Schauen Sie sich die Download-Protokolle aller Videos an. Wir sprechen nämlich über die Tatsache, dass wir 8 Videos und 5 Benutzer haben.

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


Video-Download-Details

 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 

Was ist hier los? Warum gibt es 9 Anfragen (1 Anfrage ist der Videotabelle zugeordnet und 8 - der Benutzertabelle)? Es sieht schrecklich aus. Mein Herz blieb fast stehen, als ich dachte, dass unsere vorhandene API durch diese ersetzt werden müsste ... Richtig, Datenlader können dieses Problem vollständig bewältigen.

Dies ist als N + 1-Problem bekannt. Wir sprechen über die Tatsache, dass es eine Abfrage gibt, um alle Daten abzurufen, und für jedes Datenelement (N) gibt es eine andere Abfrage an die Datenbank.

Dies ist ein sehr ernstes Problem in Bezug auf Leistung und Ressourcen: Obwohl diese Anforderungen parallel sind, belasten sie die Systemressourcen.

Um dieses Problem zu lösen, verwenden wir die dataloaden- Bibliothek des Autors der gqlgen-Bibliothek. Mit dieser Bibliothek können Sie Go-Code generieren. Generieren Sie zunächst einen Datenlader für die User :

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

Wir verfügen über die Datei userloader_gen.go , die Methoden wie Fetch , LoadAll und Prime .

Um allgemeine Ergebnisse zu erhalten, müssen wir nun die Fetch Methode (Datei dataloader.go ) definieren:

 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) }) } 

Hier warten wir 1 ms. bevor Sie die Anforderung ausführen und die Anforderungen in Paketen mit bis zu 100 Anforderungen sammeln. Anstatt jetzt eine Anforderung für jeden Benutzer einzeln auszuführen, wartet der Loader jetzt auf die angegebene Zeit, bevor er auf die Datenbank zugreift. Als Nächstes müssen Sie die Erkennungslogik ändern, indem Sie sie mithilfe der Anforderung zur Verwendung des Datenladers (Datei resolver.go ) neu konfigurieren:

 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 } 

So sehen die Protokolle danach in einer ähnlichen Situation wie oben beschrieben aus:

 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) 

Hier werden nur zwei Datenbankabfragen ausgeführt, sodass alle zufrieden sind. Es ist interessant festzustellen, dass nur 5 Benutzer-IDs an die Anforderung gesendet werden, obwohl Daten für 8 Videos angefordert werden. Dies deutet darauf hin, dass der Datenlader doppelte Datensätze entfernt.


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

Zusammenfassung


, , GitHub . . , , .

Liebe Leser! GraphQL , Go?

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


All Articles