GraphQL dan Golang

Teknologi GraphQL selama beberapa tahun terakhir, setelah perusahaan Facebook memindahkannya ke kategori open-source, telah menjadi sangat populer. Penulis materi, terjemahan yang kami terbitkan hari ini, mengatakan bahwa ia mencoba bekerja dengan GraphQL di Node.js dan, dari pengalamannya sendiri, yakin bahwa teknologi ini, berkat kemampuan dan kesederhanaannya yang luar biasa, tidak sengaja menarik begitu banyak perhatian. Baru-baru ini, saat terlibat dalam proyek baru, ia beralih dari Node.js ke Golang. Kemudian dia memutuskan untuk menguji kolaborasi Golang dan GraphQL.



Informasi awal


Anda bisa belajar dari definisi GraphQL resmi bahwa ini adalah bahasa permintaan untuk API dan runtime untuk mengeksekusi pertanyaan seperti itu pada data yang ada. GraphQL memberikan deskripsi data yang lengkap dan dapat dipahami dalam API tertentu, memungkinkan pelanggan untuk meminta informasi yang dibutuhkan secara tepat, dan tidak lebih, menyederhanakan pengembangan API dari waktu ke waktu dan memberi pengembang alat yang kuat.

Tidak banyak perpustakaan GraphQL untuk Golang. Secara khusus, saya mencoba perpustakaan seperti Thunder , graphql , graphql-go , dan gqlgen . Saya harus mencatat bahwa yang terbaik dari semua yang saya coba adalah perpustakaan gqlgen.

Pustaka gqlgen masih dalam versi beta, pada saat penulisan materi ini, versi 0.7.2 . Perpustakaan berkembang pesat. Di sini Anda dapat mengetahui tentang rencana pengembangannya. Sekarang sponsor resmi gqlgen adalah proyek desain 99, yang berarti bahwa perpustakaan ini, sangat mungkin, akan berkembang lebih cepat daripada sebelumnya. Pengembang utama perpustakaan ini adalah vektah dan neelance , sementara neelance, selain itu, berfungsi di perpustakaan graphql-go.

Mari kita bicara tentang perpustakaan gqlgen berdasarkan asumsi bahwa Anda sudah memiliki pengetahuan dasar tentang GraphQL.

Fitur Gqlgen


Dalam deskripsi gqlgen, Anda dapat mengetahui apa yang kami miliki sebelum kami adalah pustaka untuk membuat server GraphQL yang diketik dengan ketat di Golang. Frasa ini tampaknya sangat menjanjikan bagi saya, karena itu berarti bahwa ketika bekerja dengan perpustakaan ini saya tidak akan menemukan sesuatu seperti map[string]interface{} , karena pendekatan berdasarkan pengetikan ketat digunakan di sini.

Selain itu, perpustakaan ini menggunakan pendekatan yang didasarkan pada skema data. Ini berarti bahwa API dijelaskan menggunakan Bahasa Definisi Skema GraphQL. Bahasa ini memiliki alat pembuat kode sendiri yang kuat yang secara otomatis membuat kode GraphQL. Dalam hal ini, programmer hanya dapat mengimplementasikan logika dasar dari metode antarmuka yang sesuai.

Artikel ini dibagi menjadi dua bagian. Yang pertama dikhususkan untuk metode kerja dasar, dan yang kedua untuk yang maju.

Metode kerja utama: pengaturan, permintaan untuk menerima dan mengubah data, langganan


Kami, sebagai aplikasi eksperimental, akan menggunakan situs tempat pengguna dapat mempublikasikan video, menambahkan tangkapan layar dan ulasan, mencari video dan melihat daftar rekaman yang terkait dengan catatan lain. Mari kita mulai mengerjakan proyek ini:

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

Buat file skema data berikut ( schema.graphql ) di direktori root proyek:

 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 

Berikut ini dijelaskan model data dasar, satu mutasi ( Mutation , deskripsi permintaan untuk perubahan data), yang digunakan untuk mempublikasikan file video baru di situs, dan satu permintaan ( Query ) untuk mendapatkan daftar semua file video. Baca lebih lanjut tentang skema GraphQL di sini . Selain itu, di sini kami mendeklarasikan salah satu tipe data skalar kami sendiri. Kami tidak puas dengan 5 tipe data skalar standar ( Int , Float , String , Boolean dan ID ) yang ada di GraphQL.

Jika Anda perlu menggunakan tipe Anda sendiri, Anda dapat mendeklarasikannya di schema.graphql (dalam kasus kami, tipe ini adalah Timestamp ) dan memberikan definisi mereka dalam kode. Saat menggunakan pustaka gqlgen, Anda perlu menyediakan metode untuk membuat dan meng-unmarshaling semua jenis skalar Anda sendiri dan mengonfigurasi pemetaan menggunakan gqlgen.yml .

Perlu dicatat bahwa dalam versi terbaru perpustakaan ada satu perubahan penting. Yaitu, ketergantungan pada file biner yang dikompilasi dihapus dari itu. Oleh karena itu, file scripts/gqlgen.go harus ditambahkan ke proyek scripts/gqlgen.go konten berikut:

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

Setelah itu, Anda perlu menginisialisasi dep :

 dep init 

Sekarang saatnya untuk mengambil keuntungan dari kemampuan pembuatan kode perpustakaan. Mereka memungkinkan Anda untuk membuat semua kode boilerplate yang membosankan, yang, bagaimanapun, tidak dapat disebut sepenuhnya tidak menarik. Untuk memulai mekanisme pembuatan kode otomatis, jalankan perintah berikut:

 go run scripts/gqlgen.go init 

Sebagai hasil dari eksekusi, file-file berikut akan dibuat:

  • gqlgen.yml : file konfigurasi untuk mengelola pembuatan kode.
  • generated.go : kode yang dihasilkan.
  • models_gen.go : semua model dan tipe data dari skema yang disediakan.
  • resolver.go : ini akan menjadi kode yang dibuat oleh programmer.
  • server/server.go : titik masuk dengan http.Handler untuk memulai server GraphQL.

Lihatlah model yang dibuat untuk jenis Video (file 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"` } 

Di sini Anda dapat melihat bahwa ID adalah sebuah string, CreatedAt juga sebuah string. Model terkait lainnya dikonfigurasi sesuai. Namun, dalam aplikasi nyata ini tidak diperlukan. Jika Anda menggunakan semua jenis data SQL, maka Anda perlu, misalnya, bahwa bidang ID akan, tergantung pada database yang digunakan, tipe int atau int64 .

Sebagai contoh, saya menggunakan PostgreSQL dalam aplikasi demo ini, jadi tentu saja saya memerlukan bidang ID untuk menjadi tipe int dan CreatedAt jenis time.Time . Waktu. Ini mengarah pada fakta bahwa kita perlu mendefinisikan model kita sendiri dan memberi tahu gqlgen bahwa kita perlu menggunakan model kita alih-alih menghasilkan yang baru. Berikut ini isi file 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 } 

Kami memberi tahu perpustakaan bahwa itu harus menggunakan model ini (file 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 

Maksud dari semua ini adalah bahwa kita sekarang memiliki definisi kita sendiri untuk ID dan Timestamp dengan metode untuk marshaling dan unmarshaling dan memetakannya dalam file gqlgen.yml . Sekarang pengguna menyediakan string sebagai ID , metode UnmarshalID() mengubah string itu menjadi integer. Saat mengirim respons, metode MarshalID() mengubah angka menjadi string. Hal yang sama terjadi dengan Timestamp atau dengan jenis skalar lain yang dideklarasikan oleh programmer.

Sekarang saatnya mengimplementasikan logika aplikasi. Buka file resolver.go dan tambahkan deskripsi mutasi dan kueri ke dalamnya. Sudah ada kode boilerplate yang dihasilkan secara otomatis yang harus kita isi dengan maknanya. Ini kode untuk file ini:

 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 } 

Sekarang mari kita uji mutasi.

Mutasi createVideo

Itu berhasil! Tetapi mengapa tidak ada informasi user (objek user )? Saat bekerja dengan GraphQL, konsep yang mirip dengan pemuatan "malas" (lazy) dan "serakah" (bersemangat) dapat diterapkan. Karena sistem ini dapat dikembangkan, Anda perlu menentukan bidang mana yang harus diisi "dengan rakus" dan mana yang "malas".

Saya menyarankan kepada tim di organisasi tempat saya bekerja dengan "aturan emas" berikut yang berlaku ketika bekerja dengan gqlgen: "Jangan memasukkan dalam model bidang yang perlu dimuat hanya jika diminta oleh klien."

Dalam kasus kami, saya perlu mengunduh data tentang klip video terkait (dan bahkan informasi pengguna) hanya jika klien meminta bidang ini. Tetapi karena kami memasukkan bidang ini dalam model, gqlgen mengasumsikan bahwa kami menyediakan data ini dengan menerima informasi tentang video. Hasilnya, sekarang kita mendapatkan struktur kosong.

Kadang-kadang terjadi bahwa jenis data tertentu diperlukan setiap kali, sehingga tidak praktis untuk mengunduhnya menggunakan permintaan terpisah. Untuk ini, untuk meningkatkan kinerja, Anda dapat menggunakan sesuatu seperti SQL join. Sekali (ini, bagaimanapun, tidak berlaku untuk contoh yang dipertimbangkan di sini), saya perlu mengunggah metadata bersama dengan video. Entitas-entitas ini disimpan di tempat yang berbeda. Akibatnya, jika sistem saya menerima permintaan untuk mengunduh video, saya harus membuat permintaan lain untuk mendapatkan metadata. Tetapi, karena saya tahu tentang persyaratan ini (yaitu, saya tahu bahwa klien dan video dan metadata-nya selalu diperlukan di sisi klien), saya lebih suka menggunakan teknik pemuatan serakah untuk meningkatkan kinerja.

Mari kita menulis ulang model dan menghasilkan kode gqlgen lagi. Agar tidak menyulitkan cerita, kami hanya menulis metode untuk bidang user (file 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"` } 

Kami menambahkan UserID dan menghapus struktur User . Sekarang buat ulang kode:

 go run scripts/gqlgen.go -v 

Berkat perintah ini, metode antarmuka berikut akan dibuat untuk menyelesaikan struktur yang tidak ditentukan. Selain itu, Anda perlu menentukan yang berikut ini di resolver (file 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) } 

Berikut ini definisi (file 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 } 

Sekarang, hasil tes mutasi akan terlihat seperti yang ditunjukkan di bawah ini.


Mutasi createVideo

Apa yang baru saja kita bahas adalah dasar-dasar GraphQL, setelah menguasainya, Anda sudah bisa menulis sesuatu sendiri. Namun, sebelum Anda terjun ke eksperimen dengan GraphQL dan Golang, akan berguna untuk berbicara tentang langganan, yang secara langsung terkait dengan apa yang kami lakukan di sini.

โ– Berlangganan


GraphQL menyediakan kemampuan untuk berlangganan perubahan data yang terjadi secara real time. Perpustakaan gqlgen memungkinkan, secara real time, menggunakan soket web, untuk bekerja dengan acara berlangganan.

Langganan harus dijelaskan dalam file schema.graphql . Berikut ini deskripsi berlangganan ke acara penerbitan video:

 type Subscription {   videoPublished: Video! } 

Sekarang, jalankan pembuatan kode otomatis lagi:

 go run scripts/gqlgen.go -v 

Seperti yang telah disebutkan, selama pembuatan kode secara otomatis dalam file generated.go , sebuah antarmuka dibuat yang harus diimplementasikan dalam pengenal. Dalam kasus kami, sepertinya ini (file 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 } 

Sekarang, saat membuat video baru, Anda perlu memicu suatu peristiwa. Dalam contoh kami, ini dilakukan pada baris for _, observer := range videoPublishedChannel .

Sekarang saatnya untuk memeriksa langganan Anda.


Verifikasi Berlangganan

GraphQL, tentu saja, memiliki kemampuan berharga tertentu, tetapi seperti yang mereka katakan, tidak semua yang berkilau itu emas. Yaitu, kita berbicara tentang fakta bahwa seseorang yang menggunakan GraphQL perlu mengurus otorisasi, kompleksitas permintaan, caching, masalah permintaan N +1, keterbatasan kecepatan eksekusi permintaan, dan beberapa hal lainnya. Jika tidak, sistem yang dikembangkan menggunakan GraphQL dapat menghadapi penurunan kinerja yang serius.

Teknik Lanjut: Otentikasi, Pemuat Data, Kompleksitas Kueri


Setiap kali saya membaca manual seperti ini, saya merasa bahwa, setelah menguasainya, saya mempelajari semua yang perlu saya ketahui tentang teknologi tertentu dan mendapatkan kemampuan untuk menyelesaikan masalah kompleksitas apa pun.

Tetapi ketika saya mulai mengerjakan proyek saya sendiri, saya biasanya masuk ke situasi yang tidak terduga yang terlihat seperti kesalahan server atau seperti permintaan yang telah berjalan lama, atau seperti beberapa situasi jalan buntu lainnya. Sebagai akibatnya, untuk melakukan ini, saya harus mempelajari apa yang baru-baru ini tampak sangat bisa dimengerti. Dalam manual yang sama ini, saya berharap ini bisa dihindari. Itulah sebabnya di bagian ini kita akan melihat beberapa teknik canggih untuk bekerja dengan GraphQL.

โ– Otentikasi


Saat bekerja dengan REST API, kami memiliki sistem otentikasi dan alat otorisasi standar saat bekerja dengan titik akhir tertentu. Tetapi ketika menggunakan GraphQL, hanya satu titik akhir yang digunakan, oleh karena itu, tugas otentikasi dapat diselesaikan menggunakan arahan skema. Edit file schema.graphql sebagai berikut:

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

Kami membuat arahan isAuthenticated dan menerapkannya pada langganan createVideo . Setelah sesi pembuatan kode otomatis berikutnya, Anda perlu menentukan definisi untuk arahan ini. Sekarang arahan diterapkan dalam bentuk metode struktur, dan bukan dalam bentuk antarmuka, jadi kita perlu menggambarkannya. Saya mengedit kode yang dibuat secara otomatis yang terletak di file server.go dan membuat metode yang mengembalikan konfigurasi GraphQL untuk file server.go . Ini adalah file 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 } 

Ini adalah file server.go :

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

Kami membaca ID pengguna dari konteksnya. Apakah Anda tidak menemukan ini aneh? Bagaimana makna ini masuk ke dalam konteks dan mengapa itu muncul dalam konteks? Faktanya adalah bahwa gqlgen menyediakan konteks permintaan hanya pada tingkat implementasi, jadi kami tidak memiliki cara untuk membaca data permintaan HTTP apa pun, seperti header atau cookie, dalam pengenal atau arahan. Akibatnya, Anda perlu menambahkan mekanisme perantara Anda sendiri ke sistem, menerima data ini dan memasukkannya ke dalam konteks.

Sekarang kita perlu menggambarkan mekanisme otentikasi perantara kita sendiri untuk mendapatkan data otentikasi dari permintaan dan memverifikasinya.

Tidak ada logika yang didefinisikan di sini. Sebagai gantinya, untuk data otorisasi, untuk tujuan demonstrasi, ID pengguna hanya diteruskan di sini. Mekanisme ini kemudian digabungkan dalam server.go dengan metode pemuatan konfigurasi baru.

Sekarang deskripsi arahan masuk akal. Kami tidak memproses permintaan pengguna yang tidak sah dalam kode middleware, karena permintaan tersebut akan diproses oleh arahan. Ini tampilannya.


Bekerja dengan pengguna yang tidak sah


Bekerja dengan pengguna yang diotorisasi

Saat bekerja dengan arahan skema, Anda bahkan dapat menyampaikan argumen:

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

โ–Data loader


Menurut saya semua ini terlihat cukup menarik. Anda mengunduh data saat Anda membutuhkannya. Klien memiliki kemampuan untuk mengelola data, persis apa yang dibutuhkan diambil dari penyimpanan. Tapi semuanya punya harga.

Berapa harga yang harus dibayar untuk peluang ini? Lihatlah log unduhan semua video. Yaitu, kita berbicara tentang fakta bahwa kita memiliki 8 video dan 5 pengguna.

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


Detail Pengunduhan 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 

Apa yang sedang terjadi di sini? Mengapa ada 9 permintaan (1 permintaan dikaitkan dengan tabel video dan 8 - dengan tabel pengguna)? Terlihat mengerikan. Hati saya hampir berhenti ketika saya berpikir bahwa API kami yang ada harus diganti dengan ini ... Benar, pemuat data dapat sepenuhnya mengatasi masalah ini.

Ini dikenal sebagai masalah N + 1. Kita berbicara tentang fakta bahwa ada satu query untuk mendapatkan semua data dan untuk setiap bagian data (N) akan ada query lain ke database.

Ini adalah masalah yang sangat serius dalam hal kinerja dan sumber daya: meskipun permintaan ini paralel, mereka menguras sumber daya sistem.

Untuk mengatasi masalah ini, kita akan menggunakan pustaka dataloaden dari penulis pustaka gqlgen. Perpustakaan ini memungkinkan Anda untuk menghasilkan kode Go. Pertama, buat pemuat data untuk entitas User :

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

Kami siap menggunakan file userloader_gen.go , yang memiliki metode seperti Fetch , LoadAll dan Prime .

Sekarang, untuk mendapatkan hasil umum, kita perlu mendefinisikan metode Fetch (file 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) }) } 

Di sini kita tunggu 1 ms. sebelum menjalankan permintaan dan mengumpulkan permintaan dalam paket hingga 100 permintaan. Sekarang, alih-alih mengeksekusi permintaan untuk setiap pengguna secara individual, loader akan menunggu waktu yang ditentukan sebelum mengakses database. Selanjutnya, Anda perlu mengubah logika pengenal dengan mengkonfigurasi ulang menggunakan permintaan untuk menggunakan pemuat data (file 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 } 

Begini cara log terlihat setelah itu dalam situasi yang mirip dengan yang dijelaskan di atas:

 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) 

Hanya dua permintaan basis data yang dieksekusi di sini, sebagai hasilnya, semua orang sekarang senang. Sangat menarik untuk dicatat bahwa hanya 5 pengidentifikasi pengguna yang dikirim ke permintaan, meskipun data diminta untuk 8 video. Ini menunjukkan bahwa pemuat data menghapus catatan duplikat.

โ–


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

Ringkasan


, , GitHub . . , , .

Pembaca yang budiman! GraphQL , Go?

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


All Articles