Go言語で GraphQL - graph-gophers/graphql-go で Interface を試す
前回の graph-gophers/graphql-go を使って、GraphQL の Interface を扱ってみます。
ソースは http://github.com/fits/try_samples/tree/master/blog/20210125/
はじめに
GraphQL には Interface と Union という類似の機能が用意されており、共通のフィールドを設けるかどうかによって使い分けるようになっています。
graph-gophers/graphql-go では、具体的な型(Interface の実装型や Union の要素型)への変換メソッドを用意する事で Interface や Union を扱えるようになっています。
具体型への変換メソッド
func To<GraphQLの型名>() (<Goの型>, bool)
Go 側の実装方法はいくつか考えられるので、試しに 3通りで実装してみました。
(1) 基本形
まずは、graph-gophers/graphql-go の examples で使われている実装方法です。
Go の実装方法 | |
---|---|
GraphQL Interface | interface 埋め込み struct |
GraphQL Interface 実装型 | struct |
GraphQL 実装型への変換 | struct へのキャスト |
sample1.go
package main import ( "context" "encoding/json" graphql "github.com/graph-gophers/graphql-go" ) const ( // GraphQL スキーマ定義 schemaString = ` interface Event { id: ID! } type Created implements Event { id: ID! title: String! } type Deleted implements Event { id: ID! reason: String } type Query { events: [Event!]! } ` ) // GraphQL の Event に対応 type event interface { ID() graphql.ID } // GraphQL の Created に対応 type created struct { id string title string } func (c *created) ID() graphql.ID { return graphql.ID(c.id) } func (c *created) Title() string { return c.title } // GraphQL の Deleted に対応 type deleted struct { id string reason string } func (d *deleted) ID() graphql.ID { return graphql.ID(d.id) } func (d *deleted) Reason() *string { if d.reason == "" { return nil } return &d.reason } // GraphQL の Event に対応 type eventResolver struct { event } // GraphQL の Created 型への変換処理 func (r *eventResolver) ToCreated() (*created, bool) { c, ok := r.event.(*created) return c, ok } // GraphQL の Deleted 型への変換処理 func (r *eventResolver) ToDeleted() (*deleted, bool) { d, ok := r.event.(*deleted) return d, ok } type resolver struct{} func (r *resolver) Events() []*eventResolver { return []*eventResolver{ {&created{id: "i-1", title: "sample1"}}, {&deleted{id: "i-1"}}, {&created{id: "i-2", title: "sample2"}}, {&created{id: "i-3", title: "sample3"}}, {&deleted{id: "i-3", reason: "test"}}, } } func main() { schema := graphql.MustParseSchema(schemaString, new(resolver)) q := ` { events { __typename id ... on Created { title } ... on Deleted { reason } } } ` r := schema.Exec(context.Background(), q, "", nil) b, err := json.Marshal(r) if err != nil { panic(err) } println(string(b)) }
実行結果は以下の通りです。
実行結果
> go build sample1.go > sample1 {"data":{"events":[{"__typename":"Created","id":"i-1","title":"sample1"},{"__typename":"Deleted","id":"i-1","reason":null},{"__typename":"Created","id":"i-2","title":"sample2"},{"__typename":"Created","id":"i-3","title":"sample3"},{"__typename":"Deleted","id":"i-3","reason":"test"}]}}
Union の場合
Interface の代わりに Union を使った場合は以下のようになります。
sample1_union.go
・・・ const ( schemaString = ` union Event = Created | Deleted type Created { id: ID! title: String! } type Deleted { id: ID! reason: String } type Query { events: [Event!]! } ` ) type event interface{} ・・・ func main() { schema := graphql.MustParseSchema(schemaString, new(resolver)) q := ` { events { __typename ... on Created { id title } ... on Deleted { id reason } } } ` ・・・ }
(2) OneOf
次は、gRPC の oneof を参考にした実装です。
こちらは Interface よりも Union の実装に向いているかもしれません。
Go の実装方法 | |
---|---|
GraphQL Interface | GraphQL 実装型毎にフィールドを用意した struct |
GraphQL Interface 実装型 | struct |
GraphQL 実装型への変換 | nil では無いフィールド値を返す |
sample2.go
・・・ // GraphQL の Created に対応 type created struct { id string title string } func (c *created) ID() graphql.ID { return graphql.ID(c.id) } func (c *created) Title() string { return c.title } // GraphQL の Deleted に対応 type deleted struct { id string reason string } func (d *deleted) ID() graphql.ID { return graphql.ID(d.id) } func (d *deleted) Reason() *string { if d.reason == "" { return nil } return &d.reason } // GraphQL の Event に対応 type event struct { created *created deleted *deleted } func (e *event) ID() graphql.ID { if e.created == nil { return e.deleted.ID() } return e.created.ID() } // GraphQL の Created 型への変換処理 func (e *event) ToCreated() (*created, bool) { if e.created == nil { return nil, false } return e.created, true } // GraphQL の Deleted 型への変換処理 func (e *event) ToDeleted() (*deleted, bool) { if e.deleted == nil { return nil, false } return e.deleted, true } type resolver struct{} func (r *resolver) Events() []*event { return []*event{ {created: &created{id: "i-1", title: "sample1"}}, {deleted: &deleted{id: "i-1"}}, {created: &created{id: "i-2", title: "sample2"}}, {created: &created{id: "i-3", title: "sample3"}}, {deleted: &deleted{id: "i-3", reason: "test"}}, } } func main() { ・・・ }
実行結果
> go build sample2.go > sample2 {"data":{"events":[{"__typename":"Created","id":"i-1","title":"sample1"},{"__typename":"Deleted","id":"i-1","reason":null},{"__typename":"Created","id":"i-2","title":"sample2"},{"__typename":"Created","id":"i-3","title":"sample3"},{"__typename":"Deleted","id":"i-3","reason":"test"}]}}
(c) オールインワン
最後に、GraphQL Interface の実装型を単一の struct へ集約してみました。
実装内容が分かり難くなりそうで微妙かもしれません。
Go の実装方法 | |
---|---|
GraphQL Interface | GraphQL 実装型の全フィールドを備えた struct |
GraphQL Interface 実装型 | interface |
GraphQL 実装型への変換 | フラグやフィールド値の有無で判定して自身を返す |
sample3.go
・・・ // GraphQL の Created に対応 type created interface { ID() graphql.ID Title() string } // GraphQL の Deleted に対応 type deleted interface { ID() graphql.ID Reason() *string } // GraphQL の Event に対応 type event struct { id string title string reason string del bool // Created と Deleted の判定用 } func (e *event) ID() graphql.ID { return graphql.ID(e.id) } func (e *event) Title() string { return e.title } func (e *event) Reason() *string { if e.reason == "" { return nil } return &e.reason } // GraphQL の Created 型への変換処理 func (e *event) ToCreated() (created, bool) { if e.del { return nil, false } return e, true } // GraphQL の Deleted 型への変換処理 func (e *event) ToDeleted() (deleted, bool) { if e.del { return e, true } return nil, false } type resolver struct{} func (r *resolver) Events() []*event { return []*event{ {id: "i-1", title: "sample1"}, {id: "i-1", del: true}, {id: "i-2", title: "sample2"}, {id: "i-3", title: "sample3"}, {id: "i-3", reason: "test", del: true}, } } func main() { ・・・ }
実行結果
> go build sample3.go > sample3 {"data":{"events":[{"__typename":"Created","id":"i-1","title":"sample1"},{"__typename":"Deleted","id":"i-1","reason":null},{"__typename":"Created","id":"i-2","title":"sample2"},{"__typename":"Created","id":"i-3","title":"sample3"},{"__typename":"Deleted","id":"i-3","reason":"test"}]}}