https://tech.hicustomer.jp/posts/go-graphql/
シェアしました。
この記事は GraphQL Advent Calendar 2018 の22日目です。
どうも、HiCustomerのエンジニアの@m4s4k3xです。 GraphQLが広く使われるようになって久しいですが、弊社でも、以下の理由などからGraphQLを導入することにしました。
- BFFを立てるほどのエンジニアリソースを割かなくてよい
- Union TypesやEnumと言ったスキーマの型表現が可能である
- エコシステムが豊富
- 大量のエンドポイントを増やさなくても良い
- 一度のリクエストでフロントエンドで必要なリソースが取得可能
Goにおいての採用事例もちらほら見かけるようになりました。GoにおけるサーバーサイドにおけるGraphQLライブラリはメジャーなものが5つほどあります。各々のライブラリの横断的に紹介した後、弊社での選定理由を紹介します。
GoにてGraphQLの導入を検討している方への助けとなれば幸いです。
Goにおける主要なGraphQLライブラリ
graphql.orgの紹介によると、下記が主要なライブラリのようです。
- graphql-go: An implementation of GraphQ for Go / Golang.
- graph-gophers/graphql-go: An active implementation of GraphQL in Golang (was https://github.com/neelance/graphql-go).
- GQLGen - Go generate based graphql server library.
- graphql-relay-go: A Go/Golang library to help construct a graphql-go server supporting react-relay.
- machinebox/graphql: An elegant low-level HTTP client for GraphQL.(クライアントライブラリなので除外)
- samsarahq/thunder: A GraphQL implementation with easy schema building, live queries, and batching.
詳細な実装方法などは、各々のドキュメントやExampleに譲るとして、俯瞰したときにどのような分類に分けられるか?、あるいは特徴が見られるか紹介します。
(便宜上、ライブラリ名の重複があるため、上述の名称で話を進行します)
1. リフレクションベース
コンパイル時点では厳密な型付けをせずに、実行時にリフレクションベースで処理する設計のライブラリです。そのため、interface{}
が大量に出現します。
1.1.graphql-go/graphql
記事執筆時点において、スター数が最も多く、ドキュメントが充実しており、日本語情報が多いライブラリです。そのため、情報量の多さからハマった場合に解決が容易です。
ライブラリの設計として、GoのコードとしてSchemaとResolverなどを定義する設計となっています。そのため、Schema定義として独立しておらず、バックエンドと密結合になり、フロントエンドとのスキーマ共有がGraphQLの形式では難しいです(以下に例を引用します)。
// define custom GraphQL ObjectType `todoType` for our Golang struct `Todo`
// Note that
// - the fields in our todoType maps with the json tags for the fields in our struct
// - the field type matches the field type in our struct
var todoType = graphql.NewObject(graphql.ObjectConfig{
Name: "Todo",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.String,
},
"text": &graphql.Field{
Type: graphql.String,
},
"done": &graphql.Field{
Type: graphql.Boolean,
},
},
})
引用元 https://github.com/graphql-go/graphql/blob/master/examples/todo/main.go#L43-L56
Resolverの記述も下記のようになり、GoのStructベースのコードです。
var rootQuery = graphql.NewObject(graphql.ObjectConfig{
Name: "RootQuery",
Fields: graphql.Fields{
/*
curl -g 'http://localhost:8080/graphql?query={todo(id:"b"){id,text,done}}'
*/
"todo": &graphql.Field{
Type: todoType,
Description: "Get single todo",
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.String,
},
},
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
idQuery, isOK := params.Args["id"].(string)
if isOK {
// Search for el with id
for _, todo := range TodoList {
if todo.ID == idQuery {
return todo, nil
}
}
}
return Todo{}, nil
},
},
...
引用元 https://github.com/graphql-go/graphql/blob/master/examples/todo/main.go#L135-L189
1.2.graphql-relay-go
名前の通り、GraphQLのRelay対応を謳ったライブラリです。 Schema定義はGoのStrutベースで行い、その後地道にQuery, Mutaionを実装していく設計になっています。
欠点として、どうしてもinterface{}
多くなってしまい、且つボイラープレートが多くなってしまう点です。 また、2017/12/8以降更新メンテンスがされていない模様です(記事執筆時点)
下記のチュートリアルが一連の流れを掴みやすいです。
Learn Golang + GraphQL + Relay #2
https://wehavefaces.net/learn-golang-graphql-relay-2-a56cbcc3e341
Starwars example
https://github.com/graphql-go/relay/tree/master/examples/starwars
下記Schema定義の例です。
type Ship struct {
ID string `json:"id"`
Name string `json:"name"`
}
type Faction struct {
ID string `json:"id"`
Name string `json:"name"`
Ships []string `json:"ships"`
}
引用元 https://github.com/graphql-go/relay/blob/master/examples/starwars/data.go
以下、Mutaionの例です。
/**
* This will return a GraphQLField for our ship
* mutation.
*
* It creates these two types implicitly:
* input IntroduceShipInput {
* clientMutationID: string!
* shipName: string!
* factionId: ID!
* }
*
* input IntroduceShipPayload {
* clientMutationID: string!
* ship: Ship
* faction: Faction
* }
*/
shipMutation := relay.MutationWithClientMutationID(relay.MutationConfig{
Name: "IntroduceShip",
InputFields: graphql.InputObjectConfigFieldMap{
"shipName": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.String),
},
"factionId": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.ID),
},
},
OutputFields: graphql.Fields{
"ship": &graphql.Field{
Type: shipType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if payload, ok := p.Source.(map[string]interface{}); ok {
return GetShip(payload["shipId"].(string)), nil
}
return nil, nil
},
},
"faction": &graphql.Field{
Type: factionType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if payload, ok := p.Source.(map[string]interface{}); ok {
return GetFaction(payload["factionId"].(string)), nil
}
return nil, nil
},
},
},
MutateAndGetPayload: func(inputMap map[string]interface{}, info graphql.ResolveInfo, ctx context.Context) (map[string]interface{}, error) {
// `inputMap` is a map with keys/fields as specified in `InputFields`
// Note, that these fields were specified as non-nullables, so we can assume that it exists.
shipName := inputMap["shipName"].(string)
factionId := inputMap["factionId"].(string)
// This mutation involves us creating (introducing) a new ship
newShip := CreateShip(shipName, factionId)
// return payload
return map[string]interface{}{
"shipId": newShip.ID,
"factionId": factionId,
}, nil
},
})
引用元 https://github.com/graphql-go/relay/blob/master/examples/starwars/schema.go#L270-L314
2. スキーマドリブン設計
続いて、スキーマを最初に定義し、それを元にコードを生成、あるいは実装するような設計のものです。
2.1. GQLGen
Schema定義に沿った、ボイラープレートを自動生成し、エンジニアがResolverのメインのロジックの実装に注力できるライブラリです。また、デフォルトで GraphiQL が使用可能なコードも生成してくれるため、ブラウザですばやく実行することができます。 Getting Started.comを見ると大まかな流れが把握できます。
Getting Started
https://gqlgen.com/getting-started/
また、既存コードとのヒモ付も可能で、既存システムへのGraphQL導入もスムーズです。 https://gqlgen.com/config/
懸念点としては、自動生成されたボイラープレートが生成されるため、エッジケースなどでデバッグするときに大変かもしれません。
詳細は、GQLgen自身のREADMEにある他ライブラリとの比較表が大変わかりやすいです。
https://github.com/99designs/gqlgen
TODOのExample
https://github.com/99designs/gqlgen/tree/master/example/todo
一例として、自動生成されたResolverのコードを掲載します。panic("not implemented")
の箇所を実装していく流れです。
package graph
import
"context"
"github.com/hicustomer/hicustomer-lambda/src/mvp/variable_settings/request_handler"
)
type Resolver struct{}
func (r *Resolver) Mutation() MutationResolver {
return &mutationResolver{r}
}
func (r *Resolver) Query() QueryResolver {
return &queryResolver{r}
}
type mutationResolver struct{ *Resolver }
func (r *mutationResolver) CreateCustomer(ctx context.Context, input NewCustomer) (Customer, error) {
panic("not implemented")
}
func (r *mutationResolver) CreateVariable(ctx context.Context, input request_handler.VariableInput) (string, error) {
panic("not implemented")
}
type queryResolver struct{ *Resolver }
func (r *queryResolver) Customers(ctx context.Context, input *CustomerDetailInput) ([]*Customer, error) {
panic("not implemented")
}
func (r *queryResolver) Customer(ctx context.Context, input *CustomerDetailInput) (*Customer, error) {
panic("not implemented")
}
func (r *queryResolver) Variables(ctx context.Context, input request_handler.VariableListInput) (request_handler.VariableListOutput, error) {
panic("not implemented")
}
2.2. graph-gophers/graphql-go
こちらもSchema定義を元に開発すすめるのは変わらないのですが、GQLGenと違い、ランタイム時にスキーマとResolverとの整合性との比較を行います。そのため、IDEの補完やコンパイル時にエラーの発見などはできません。
小規模なAPIであれば、こちらのほうが向いているかもしれません。
ただし、ドキュメントがないようなので、コードを読んで実装していく形になるのが難点かもしれません。 (といっても、有志によるExampleがREADMEにいくつか書いてあります)
スキーマ定義例
type human struct {
ID graphql.ID
Name string
Friends []graphql.ID
AppearsIn []string
Height float64
Mass int
Starships []graphql.ID
}
引用元 https://github.com/graph-gophers/graphql-go/blob/master/example/starwars/starwars.go#L146-L154
Resolver例
func (r *Resolver) Hero(args struct{ Episode string }) *characterResolver {
if args.Episode == "EMPIRE" {
return &characterResolver{&humanResolver{humanData["1000"]}}
}
return &characterResolver{&droidResolver{droidData["2001"]}}
}
引用元 https://github.com/graph-gophers/graphql-go/blob/master/example/starwars/starwars.go#L289-L294
2.3. samsarahq/thunder
上2つとは設計思想が違い、GoのStructをベースに実装を進める設計となっています。
ただし、実際のスキーマ定義とのズレやフロントエンドとのスキーマ共有が直接できないなどのデメリットが考えられます。
Resolver例
func (s *Server) registerMutation(schema *schemabuilder.Schema) {
object := schema.Mutation()
object.FieldFunc("echo", func(ctx context.Context, args struct{ Text string }) (string, error) {
return args.Text, nil
})
object.FieldFunc("echoEnum", func(ctx context.Context, args struct {
EnumField RoleType
}) (RoleType, error) {
return args.EnumField, nil
})
}
引用元 https://github.com/samsarahq/thunder/blob/master/example/minimal/main.go#L70-L82
結局どのライブラリを採用すべき?
つらつらとGoの主要の5大ライブラリを書きました。弊社では、下記の理由によりGQLGenを採用しました。
- Type Safe(せっかくGoを使っているの)
- IDEの補完が聞きやすい
- 開発が活発であり、Open Tracingやdataloaderなどの対応に積極的
- スキーマドリブンであり、自動生成に注力している
なぜスキーマドリブンが重要であるかを、アーキテクチャの変化と開発体験の変化を交えて説明します。
アーキテクチャの変化と必要とされるライブラリの変化
モノリシックなアーキテクチャが主流でしたが、現在はマイクロサービスなアーキテクチャが浸透してきました。ApiGatewayを挟んで、バックエンドでは複数のサービスが連携している構成なども見かけるようになりました。弊社でも 以下のようなサーバレスアーキテクチャを採用しております。
( プロダクトとアーキテクチャの詳しい説明はこちらの記事にて)
変化に伴い発生する課題
アーキテクチャの変化に伴い、様々な課題が顕著になってきました。
- バックエンドとフロントエンド感のコミュニケーションコストの増加
- APIの定義とスキーマのデグレによるバグの発生(リクエストのパラメータの齟齬によるエラーなど)
- Schema定義を複数のフロントエンド、ないしはバックエンドで使用するために、サービスに依存せずに独立したSchema定義が必要
- Schema定義からすばやくドキュメントが生成可能
そこで、先程紹介したスキーマドリブンの考え方を導入して、下記のようなレイヤー構造にして、課題の解決を図ります
導入前
バックエンド <- フロントエンド
(<- 依存の向き先)
↓
導入後
バックエンド-> API定義 <- フロントエンド
(<- 依存の向き先)
(所謂、 抽象的に見れば、中間層、あるいはDDDにおける腐敗防止層的な役割を果たすレイヤですね)
開発体験(DX)の向上
スキーマドリブンにすることで下記のような開発フローです。
- スキーマを定義
- コードジェネレート
- 自動生成できないメインロジックの実装
課題の解決が図られ、以下のようなDX向上が見込めます。
- ボイラーな処理を自動で行ってくれるため、ドメイン知識の実装などに注力できる
- CIなどでSchemaと実装の乖離の発見によるバグの減少
- Schema定義が独立しているので、フロントエンドとバックエンドが独立して開発が進められる
- ドキュメントの生成が容易
最後に
おおまかに主要ライブリの紹介をしました。今後は、Schemaを中心としたSchema駆動な開発が広まっていくと予想しています。実際に、gRRPC-webではTypescriptの生成機能が実装されていたり、 フロントエンド側でもコード生成のアプローチは多く採用されており、apollographql/apollo-toolingやdotansimha/graphql-code-generatorなどがあります。
また、今回は考慮しませんでしたが、GraphQLを採用するときの課題として:
- dataloaderによる n+1 の解消
- Apollo Tracing, Open Tracingやjaegerなどの分散トレーシング
などがあります。また、AppSync や Prisma などの所謂 Baas も採用する手段もあります。
Goでは型パラメータを用いるなど、抽象的なコードが書けないため、GQLGenのようにコードジェネレートの文化が盛んに思われます。例えば、goaでは、DSLによってAPIの仕様を定義し、それをボイラーコードを生成して、必要な箇所だけをエンジニアが実装するアプローチを採用しています。
map[string]interface{}
と戦いたくないのであれば、GQLGenおすすめです。
機会があれば上記の事柄も記事にできればと思います。
広告
HiCustomer ではエンジニアを募集しています。この記事を読んで少しでも興味をお持ちいただけた方、ぜひオフィスに遊びにきてください。弊社のVP of Engineeringの@hizeny まで、もしくは Wantedly 経由でご連絡をお待ちしています!
参考
GraphQL Concepts Visualized(図解されていて大変わかりやすい)
https://blog.apollographql.com/the-concepts-of-graphql-bc68bd819be3
Tutorial: Designing a GraphQL API(ベストプラクティスとして参考になる)
https://github.com/Shopify/graphql-design-tutorial/blob/master/TUTORIAL.md
Choosing a GraphQL server library in Go(シンプルに比較が書いてある)
https://medium.com/open-graphql/choosing-a-graphql-server-library-in-go-8836f893881b
GraphQL の情報を雑にまとめる(フロントエンド周りが参考になる)
0 件のコメント:
コメントを投稿