Go で Apollo Federationするのは、まだ早いと思う。
見出し画像

Go で Apollo Federationするのは、まだ早いと思う。

Seiji

おはようございます!
スペースマーケットのバックエンドエンジニアの小見と申します。

今回は、dataloaderの番外編
gqlgenでdataloaderのサンプルを書いていたら、
ResolveReferenceの時にdataloaderが思った通りに動かなかった話と
gqlgenの使い方を自分なりに分かりやすく纏めていこうと思います。

参考にさせていただいた記事

https://qiita.com/sky0621/items/621f075e4257270a9e02

先に結論

gqlgen の以下issueが治るまではApollo Federationが直列で実行されるため
dataloaderの恩恵を受けられなさそうです。
Graphql開発をする時はdataloaderが必須だと思いますので、まだ早いと感じました。

今回お話すること

gqlgenの大まかな使い方とdataloaderが動かなかった悲劇をお話します。

Apollo Federationとは何かといった話は、以前に弊社北島が纏めてくださいましたのでこちらをご確認ください。

gqlgenの大まかな使い方

プロジェクト作成からサーバの立ち上げまでを行います。

まずプロジェクトを作成します。

go mod init github.com/[UserName]/[ProjectName]

gqlgen を取得

go get github.com/99designs/gqlgen

gqlgen の機能でテンプレを作成する。
テンプレの作成によって、graphフォルダ配下に色々と作成してくれます。

go run github.com/99designs/gqlgen init

server.goを削除してmain.goに以下内容を記述してください。
この時点で go run main.go でサーバーは一応立ち上がります。

package main

import (
	"net/http"

	"github.com/[UserName]/[ProjectName]/graph"
	"github.com/[UserName]/[ProjectName]/graph/generated"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"

	"github.com/99designs/gqlgen/graphql/handler"
	"github.com/99designs/gqlgen/graphql/playground"
)

func main() {
	e := echo.New()

	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Welcome!")
	})

	srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))
	e.POST("/query", func(c echo.Context) error {
		srv.ServeHTTP(c.Response(), c.Request())
		return nil
	})

	e.GET("/playground", func(c echo.Context) error {
		playground.Handler("GraphQL", "/query").ServeHTTP(c.Response(), c.Request())
		return nil
	})

	e.Logger.Fatal(e.Start(":4001"))
}

graph/schema.graphqls に記述されているgraphqlスキーマをお好きな形に変更してください。

type User @key(fields: "id") @extends {
 id: ID! @external
 todos: [Todo]
}

type Todo @key(fields: "id") {
 id: ID!
 userId: String!
 user: User
 body: String!
}

type Query {
 todos: [Todo]
}

gqlgen.yml に Apollo Federation の設定を追加する
(正確には、以下内容がコメントアウトされているためコメントアウトからもとに戻してあげてください。)

# Uncomment to enable federation
federation:
 filename: graph/generated/federation.go
 package: generated

gqlgen.yml に 項目レベルの Resolve を設定する。

models:
 User:
   fields:
     todos: # User Entityの todos 項目は Query で項目指定された時のみ呼び出されます。
       resolver: true
 Todo:
   fields:
     user: # Todo Entityの user 項目は Query で項目指定された時のみ呼び出されます。
       resolver: true

今までに設定した内容を基に gqlgen のコードを修正させる。
以下のコマンドで自動的に gqlgen が自動的に作成してくれる部分のコードを修正してくれます。

go run github.com/99designs/gqlgen

実装を行う。
全体的な実装はリポジトリを御覧ください。
gqlgen メソッドを用意してくれているので、
そのメソッドに実装内容を記述するだけです。

実装するのは主に2箇所で、
ApolloFederationのためのentity.resolvers.go と 
自APIで解決するスキーマのschema.resolvers.goの2つになります。

func (r *userResolver) Todos(ctx context.Context, obj *model.User) ([]*model.Todo, error) {
	panic(fmt.Errorf("not implemented"))
}

↓

func (r *userResolver) Todos(ctx context.Context, obj *model.User) ([]*model.Todo, error) {
	if obj == nil {
		panic("obj is null")
	}
	intID, err := strconv.Atoi(obj.ID)
	if err != nil {
		panic(err)
	}
	return For(ctx).TodosByUserIDs.Load(intID) // Dataloaderの呼び出し
}

先にDataloaderの呼び出しを記述してしまいましたが、
次にDataloaderを作成して行きます。

まずは、dataloader を取得します。

go get github.com/vektah/dataloaden

dataloader の作成を行います。
ここで作成されるのはdataloaderのkeyを纏めてくれる機能で
DBからデータを取得するロジックは後ほど作成します。

go run github.com/vektah/dataloaden [dataloader名] [纏めるkeyの型] [取得するデータの型]
例)
go run github.com/vektah/dataloaden TodoLoader int *github.com/SeijiOmi/user/graph/model.Todo

graph/dataloaders.go を作成します。
ここでDBからWhere IN で取得する部分のロジックを記述します。

main.go で Middlewareを使ってサーバーを初期化する設定を記述する。

	srv :=
		graph.Middleware( // 追加
			db,  // 追加  お好きなDBコネクションをお使いください。
			handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{DB: db}}))
		)

​お疲れさまでした!これで動くようになります!

dataloaderが動かない

dataloaderのサンプルが出来上がったと意気揚々に
queryを投げてみたらSQLが大量発行されている。

[4.700ms] [rows:1] SELECT * FROM `users` WHERE `users`.`id` = 1
[0.780ms] [rows:1] SELECT * FROM `users` WHERE `users`.`id` = 2
[0.580ms] [rows:1] SELECT * FROM `users` WHERE `users`.`id` = 3

発生しているのはUserのentity.resolversの部分で
FieldResolverである、Todoの方は以下の様にINを利用している。

[9.043ms] [rows:5] SELECT * FROM `todos` WHERE user_id IN (1,2,3)

つまりApollo Federationの挙動が関連している?
調べてみると先頭に記述したそれっぽいissueがある。
直列でApollo Federationを行っているから、他のEntityの実行を待つことが出来ず一括実行が来ていないと予想しております。​

じゃあ、ここまで作ったけど他のライブラリ使うか〜
と思ったのですが、gqlgen以外 のライブラリでFederationに親指が立っていませんので他のライブラリでFederationは出来ないと思われます。
つまりGo で Apollo Federationをするのはまだ早そうです。

画像1

まとめ

久しぶりにGoのAPIを作ってみたらやっぱり色々と速いというのが開発していて凄い気持ち良いと感じましたが
ライブラリが未熟なのは残念ポイントとしてありますが、これは少し気長にまとうかなと思います。

最後に弊社宣伝です。

弊社ではエンジニアを募集しております!
カジュアル面談も実施しておりますのでよろしければ以下よりご応募ください!


この記事が気に入ったら、サポートをしてみませんか?
気軽にクリエイターの支援と、記事のオススメができます!
Seiji