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"}]}}