予備情報
公式のGraphQL定義から、それがAPIのクエリ言語であり、既存のデータに対してそのようなクエリを実行するためのランタイムであることがわかります。 GraphQLは、特定のAPIのデータの完全で理解可能な説明を提供し、顧客が必要な情報を正確に要求することを可能にします。
Golang用のGraphQLライブラリは多くありません。 特に、 Thunder 、 graphql 、 graphql-go 、およびgqlgenのようなライブラリをテストしました 。 私が試した中で一番良かったのはgqlgenライブラリーだったことに注意してください。
gqlgenライブラリはまだベータ版であり、この資料の執筆時点ではバージョン0.7.2でした 。 ライブラリは急速に進化しています。 ここでは、その開発計画について調べることができます。 現在、gqlgenの公式スポンサーは99designsプロジェクトです。つまり、このライブラリは、おそらく以前よりもさらに高速に開発されます。 このライブラリの主な開発者はvektahとneelanceであり、さらにneelanceはgraphql-goライブラリで動作します。
GraphQLの基本的な知識があることを前提に、gqlgenライブラリについて話しましょう。
Gqlgenの機能
gqlgenの説明では、Golangで厳密に型指定されたGraphQLサーバーを迅速に作成するためのライブラリである以前のバージョンを確認できます。 このフレーズは、このライブラリで作業するときに
map[string]interface{}
ようなものに出くわすことがないことを意味するため、非常に有望だと思われます。
さらに、このライブラリは、データスキーマに基づくアプローチを使用します。 つまり、APIはGraphQL スキーマ定義言語を使用して記述されます。 この言語には、GraphQLコードを自動的に作成する独自の強力なコード生成ツールがあります。 この場合、プログラマは対応するインターフェイスメソッドの基本ロジックのみを実装できます。
この記事は2つのパートに分かれています。 1つ目は基本的な作業方法、2つ目は高度な方法です。
主な作業方法:セットアップ、データの受信と変更のリクエスト、サブスクリプション
実験的なアプリケーションとして、ユーザーが動画を公開したり、スクリーンショットやレビューを追加したり、動画を検索したり、他の録画に関連する録画のリストを表示したりできるサイトを使用します。 このプロジェクトの作業を始めましょう:
mkdir -p $GOPATH/src/github.com/ridhamtarpara/go-graphql-demo/
プロジェクトのルートディレクトリに次のデータスキーマファイル(
schema.graphql
)を作成します。
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
ここでは、基本的なデータモデル、サイトに新しいビデオファイルを公開するために使用される1つのミュー
Mutation
(
Mutation
、データ変更のリクエストの説明)、およびすべてのビデオファイルのリストを取得するための1つのクエリ(
Query
)について説明します。 GraphQLスキーマについて詳しくはこちらをご覧ください 。 さらに、ここでは、独自のスカラーデータ型の1つを宣言しました。 GraphQLにある5つの標準スカラーデータ型 (
Int
、
Float
、
String
、
Boolean
および
ID
)に満足していません。
独自の型を使用する必要がある場合、
schema.graphql
(この場合、この型は
Timestamp
)で宣言し、コードで定義を提供できます。 gqlgenライブラリを使用する場合は、独自のすべてのスカラー型のマーシャリングおよびアンマーシャリングのメソッドを提供し、gqlgen.ymlを使用してマッピングを構成する必要があります。
ライブラリの最新バージョンでは、1つの重要な変更があったことに注意してください。 つまり、コンパイルされたバイナリファイルへの依存関係は削除されました。 したがって、プロジェクトに次のコンテンツの
scripts/gqlgen.go
を追加する必要があり
scripts/gqlgen.go
。
// +build ignore package main import "github.com/99designs/gqlgen/cmd" func main() { cmd.Execute() }
その後、
dep
を初期化する必要があります:
dep init
次に、ライブラリのコード生成機能を利用します。 それらはすべての退屈な定型コードを作成することを可能にしますが、完全に面白くないとは言えません。 自動コード生成メカニズムを開始するには、次のコマンドを実行します。
go run scripts/gqlgen.go init
実行の結果、次のファイルが作成されます。
-
gqlgen.yml
:コード生成を管理するための構成ファイル。
-
generated.go
:生成されたコード。
-
models_gen.go
:提供されたスキーマのすべてのモデルとデータ型。
-
resolver.go
:プログラマーが作成するコードです。
-
server/server.go
:GraphQLサーバーを起動するためのhttp.Handlerを持つエントリポイント。
タイプ
Video
用に生成されたモデル(ファイル
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"` }
ここでは、
ID
が文字列であり、
CreatedAt
も文字列であることがわかります。 その他の関連モデルはそれに応じて構成されます。 ただし、実際のアプリケーションではこれは必要ありません。 任意のタイプのSQLデータを使用している場合、たとえば、使用するデータベースに応じて、
ID
フィールドは
int
または
int64
ます。
たとえば、このデモアプリケーションではPostgreSQLを使用しているため、当然、
ID
フィールドは
int
型で、
CreatedAt
フィールドは
time.Time
型である
time.Time
ます。 これは、独自のモデルを定義し、新しいモデルを生成する代わりにモデルを使用する必要があることをgqlgenに伝える必要があるという事実につながります。
models.go
ファイルの内容は次の
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 }
ライブラリにこれらのモデル(
gqlgen.yml
ファイル)を使用するように
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
これのすべてのポイントは、
gqlgen.yml
ファイルでマーシャリングおよびアンマーシャリングとマッピングのためのメソッドを備えた
ID
と
Timestamp
独自の定義を持っていることです。 ユーザーが文字列を
ID
として提供したので、
UnmarshalID()
メソッドはその文字列を整数に変換します。 応答を送信するとき、
MarshalID()
メソッドは数値を文字列に変換します。 同じことが
Timestamp
でも、プログラマーによって宣言された他のスカラー型でも起こります。
次に、アプリケーションロジックを実装します。
resolver.go
ファイルを開き、突然変異とクエリの説明を追加します。 すでに自動生成されたテンプレートコードがあり、意味を埋める必要があります。 このファイルのコードは次のとおりです。
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 }
それでは、突然変異をテストしましょう。
突然変異createVideo
うまくいく! しかし、なぜユーザー情報(
user
オブジェクト)に何もないのですか? GraphQLを使用する場合、「遅延」(遅延)および「貪欲」(熱心)ロードに類似した概念が適用可能です。 このシステムは拡張可能であるため、どのフィールドに「貪欲に」入力する必要があり、どのフィールドに「遅延」を指定する必要があります。
私は、gqlgenで作業するときに適用される次の「ゴールデンルール」を扱う組織のチームに提案しました。「クライアントから要求された場合にのみロードする必要があるモデルフィールドに含めないでください。」
この場合、クライアントがこれらのフィールドを要求した場合にのみ、関連するビデオクリップに関するデータ(およびユーザー情報も)をダウンロードする必要があります。 しかし、これらのフィールドをモデルに含めたため、gqlgenは、ビデオに関する情報を受信することでこのデータを提供すると想定しています。 その結果、空の構造が得られます。
時々、特定のタイプのデータが毎回必要になることがあるため、別のリクエストを使用してダウンロードすることは実用的ではありません。 このため、パフォーマンスを向上させるために、SQL結合などを使用できます。 一度(ただし、ここで検討する例には適用されません)、メタデータをビデオとともにアップロードする必要がありました。 これらのエンティティは異なる場所に保存されていました。 その結果、システムがビデオのダウンロード要求を受信した場合、メタデータを取得するために別の要求を行う必要がありました。 しかし、私はこの要件を知っていたので(つまり、クライアント側では常にビデオとそのメタデータの両方が必要であることを知っていました)、欲張りなダウンロード技術を使用してパフォーマンスを向上させることを好みました。
モデルを書き直して、gqlgenコードを再度生成しましょう。 話を複雑にしないために、
user
フィールド(
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
を追加し、
User
構造を削除しました。 次に、コードを再生成します。
go run scripts/gqlgen.go -v
このコマンドのおかげで、未定義の構造を解決するために次のインターフェースメソッドが作成されます。 さらに、リゾルバー(
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) }
定義は次のとおりです(
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 }
これで、突然変異テストの結果は次のようになります。
突然変異createVideo
先ほど説明したのは、GraphQLの基礎であり、すでにマスターしているので、既に独自のものを作成できます。 ただし、GraphQLとGolangの実験に突入する前に、ここで行っていることに直接関連するサブスクリプションについて説明しておくと便利です。
▍サブスクリプション
GraphQLは、リアルタイムで発生するデータ変更をサブスクライブする機能を提供します。 gqlgenライブラリを使用すると、リアルタイムでWebソケットを使用して、サブスクリプションイベントを処理できます。
サブスクリプションは
schema.graphql
ファイルで説明する必要があります。 ビデオ公開イベントのサブスクリプションの説明は次のとおりです。
type Subscription { videoPublished: Video! }
次に、自動コード生成を再度実行します。
go run scripts/gqlgen.go -v
既に述べたように、
generated.go
ファイルでのコードの自動作成中に、認識機能に実装する必要があるインターフェースが作成されます。 この場合、次のようになります(
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 }
ここで、新しいビデオを作成するときに、イベントをトリガーする必要があります。 この例では、これは
for _, observer := range videoPublishedChannel
ます。
さあ、サブスクリプションをチェックしましょう。
サブスクリプションを確認する
もちろん、GraphQLには特定の価値のある機能がありますが、彼らが言うように、きらきら輝くのは金だけではありません。 つまり、GraphQLを使用する人は、承認、リクエストの複雑さ、キャッシュ、N + 1リクエストの問題、クエリ実行速度の制限などを処理する必要があるという事実について話している。 そうしないと、GraphQLを使用して開発されたシステムのパフォーマンスが大幅に低下する可能性があります。
高度な手法:認証、データローダー、クエリの複雑さ
このようなマニュアルを読むたびに、それらを習得したことで、特定の技術について知っておく必要のあるすべてのことを学び、複雑な問題を解決する能力を得ることができます。
しかし、自分のプロジェクトで作業を開始すると、通常、サーバーエラーのように見える、予期せぬ状況が発生します。 結果として、これを行うには、ごく最近完全に理解できると思われたものをよりよく調査する必要があります。 この同じマニュアルで、これを回避できることを願っています。 そのため、このセクションでは、GraphQLを操作するための高度なテクニックについて説明します。
▍認証
REST APIを使用する場合、特定のエンドポイントを使用する場合、認証システムと標準の承認ツールがあります。 ただし、GraphQLを使用する場合、1つのエンドポイントのみが使用されるため、スキーマディレクティブを使用して認証タスクを解決できます。
schema.graphql
ファイルを次のように編集します。
type Mutation { createVideo(input: NewVideo!): Video! @isAuthenticated } directive @isAuthenticated on FIELD_DEFINITION
isAuthenticated
ディレクティブを作成し、
createVideo
サブスクリプションに適用しました。 次の自動コード生成セッションの後、このディレクティブの定義を定義する必要があります。 現在、ディレクティブはインターフェースの形式ではなく、構造のメソッドの形式で実装されているため、それらを記述する必要があります。
server.go
ファイルにある自動生成コードを編集し、
server.go
ファイルのGraphQL構成を返すメソッドを作成しました。
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 }
server.go
ファイルは次の
server.go
です。
rootHandler:= dataloaders.DataloaderMiddleware( db, handler.GraphQL( go_graphql_demo.NewExecutableSchema(go_graphql_demo.NewRootResolvers(db) ) ) http.Handle("/query", auth.AuthMiddleware(rootHandler))
コンテキストからユーザー
ID
を読み取ります。 これはおかしいと思いませんか? この意味はどのように文脈になったのでしょうか? 実際のところ、gqlgenは実装レベルでのみリクエストコンテキストを提供するため、レコグナイザーまたはディレクティブでヘッダーやCookieなどのHTTPリクエストデータを読み取る方法はありません。 その結果、独自の中間メカニズムをシステムに追加し、このデータを受信してコンテキストに入れる必要があります。
次に、リクエストから認証データを取得して検証するための独自の中間認証メカニズムを説明する必要があります。
ここではロジックは定義されていません。 代わりに、許可データの場合、デモンストレーションの目的で、ユーザー
ID
単にここに渡されます。 このメカニズムは、
server.go
で新しい構成読み込みメソッドと組み合わされます。
これで、ディレクティブの説明が理にかなっています。 ミドルウェアコードで許可されていないユーザーリクエストは処理されません。そのようなリクエストはディレクティブによって処理されるためです。 外観は次のとおりです。
権限のないユーザーと連携する
許可されたユーザーと連携する
スキーマディレクティブを使用する場合、引数を渡すこともできます。
directive @hasRole(role: Role!) on FIELD_DEFINITION enum Role { ADMIN USER }
▍データローダー
これはすべて非常に興味深いように思えます。 必要なときにデータをダウンロードします。 クライアントにはデータを管理する機能があり、必要なものはストレージから取得されます。 しかし、すべてに価格があります。
これらの機会に支払う代価はいくらですか? すべての動画のダウンロードログをご覧ください。 つまり、8つのビデオと5人のユーザーがいるということです。
query{ Videos(limit: 10){ name user{ name } } }
ビデオのダウンロードの詳細
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
ここで何が起こっていますか? 9つのリクエストがあるのはなぜですか(1つのリクエストがビデオテーブルに関連付けられ、8つのリクエストがユーザーテーブルに関連付けられています)? ひどいですね。 既存のAPIをこれに置き換える必要があると思ったとき、私の心はほぼ停止しました...確かに、データローダーはこの問題に完全に対処できます。
これはN + 1問題と呼ばれ、すべてのデータを取得するクエリが1つあり、データ(N)ごとにデータベースへの別のクエリが存在するという事実について話します。
これは、パフォーマンスとリソースに関して非常に深刻な問題です。これらの要求は並行していますが、システムリソースを消費します。
この問題を解決するために、gqlgenライブラリの作成者のdataloadenライブラリを使用します。 このライブラリを使用すると、Goコードを生成できます。 まず、
User
エンティティのデータローダーを生成します。
go get github.com/vektah/dataloaden dataloaden github.com/ridhamtarpara/go-graphql-demo/api.User
Fetch
、
LoadAll
Prime
などのメソッドを持つファイル
userloader_gen.go
自由に使用できます。
次に、一般的な結果を取得するために、
Fetch
メソッド(
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) }) }
ここでは1ミリ秒待機します。 リクエストを実行する前に、最大100リクエストのパッケージでリクエストを収集します。 これで、ローダーは各ユーザーに対して個別にリクエストを実行する代わりに、データベースにアクセスする前に指定された時間待機します。 次に、データローダー(
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 }
上記のような状況で、ログがどのように見えるかを次に示します。
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)
ここでは2つのデータベースクエリのみが実行され、その結果、誰もが満足しています。 8つのビデオのデータが要求されますが、5つのユーザー識別子のみが要求に送信されることに注意してください。 , .
▍
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
. , , , , .
まとめ
, , GitHub . . , , .
親愛なる読者! GraphQL , Go?