emahiro/b.log

勉強記録と書評とたまに長めの呟きを書きます

GraphQLを書いてみた話

仕事ではjson-shemaを使ったRESTを使っています。

API定義がドキュメント化されていることの意義

マイクロサービスアーキテクチャを元に開発していると、コンポーネント間のインターフェースの定義なしにはやっていけないと感じています。
もしjson-schemaがなかったら、各コンポーネントのroutingを見て、対応するhandlerがどんなjsonを返すのかそのコードを見に行くしかないです。
また、server sideの側の実装次第ではResponseの内容が急に変わってしまってクライアント側が予期せぬjsonが帰って来るなんてことがあるかもしれないです。

全てをserverサイドで行ったり、モノリシックなアーキテクチャで大規模サービスを運用することが難しくなってきている今、コンポーネント間のインターフェース定義を外出して、それを見ればインターフェースの内容がわかるようになるというのは開発の効率化を鑑みても必要な事になりつつあるように感じます。

幾つか試そうと思ったのですが、少し前から話題になっていて、個人的にも興味があった GraphQL を実際にgoで書きながら実験的に実装してみようと思います。

graphqlの基礎単語

Query

データの取得をする時に使う。
RESTでいうGETの時に使うと思われるクエリ。

Mutation

データの更新をする時に使う。
RESTでいうPOST/PUT(PATCH)/DELETE のときに使うと思われる。

Sample

https://github.com/graphql/graphiql でGraphiQLのライブデモ を触ることが出来る。 このライブでもを元にQueryを勉強してみると、

まずRootのQueryがあります。

f:id:ema_hiro:20180130014423p:plain

この中で allFilms のQueryの定義の中をみると

f:id:ema_hiro:20180130014653p:plain

以下のように引数の各型とResponseの型が決まっています。(ResponseはFilmsCollection型)

f:id:ema_hiro:20180130014710p:plain

I/Fの定義は

  • args(引数)
    • after: String型
    • first: int型
    • before: String型
    •  last: int型

※ FilmのListを取得するメソッドなので、◯話~□話以内とかを指定するための first/last、◯話以内/以上全てを指定するための before/after みたいな定義方法なのかと思います。なんでbefore/afterがStringかは不明(intでいいじゃん)

  • I/FのResponse
    • FilmsCollection(アプリ内で定義)

と読むことが出来ます。 また FillsCollection もどういう定義なのかを追っていくことが出来ます。

graphqlを動かす環境の用意

今回は毎度おなじみgolangで書いてみようと思います。

http://graphql.org/code/#go によると公式にgoのライブラリがあるのでこれを使います。

使用ライブラリ: https://github.com/graphql-go/graphql
Godoc: https://godoc.org/github.com/graphql-go/graphql

depでgraphql-goをinstall

$ mkdir graphql_samples
# direnvでプロジェクトルートをGOPATHに指定します。
$ echo "export GOPATH=$(pwd)" >> .envrc
$ direnv allow
$ mkdir src && mkdir src/graphql_samples && cd src/graphql_samples
# dep を使ってインストール
$ dep init
$ dep ensure -add github.com/graphql-go/graphql                                         
Fetching sources...

"github.com/graphql-go/graphql" is not imported by your project, and has been temporarily added to Gopkg.lock and vendor/.
If you run "dep ensure" again before actually importing it, it will disappear from Gopkg.lock and vendor/.

これで準備完了です。

実際にスキーマを書いてみてる

graphql-go のREALDMEを参考にして書いてみます。

package main

import (
    "encoding/json"
    "fmt"
    "log"

    "github.com/graphql-go/graphql"
)

func main() {
    // schema定義
    fields := graphql.Fields{
        "hello": &graphql.Field{
            Type: graphql.String,
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                return "world", nil
            },
        },
    }

    rootQuery := graphql.ObjectConfig{
        Name:   "RootQuery",
        Fields: fields,
    }

    schemaConfig := graphql.SchemaConfig{
        Query: graphql.NewObject(rootQuery),
    }

    schema, err := graphql.NewSchema(schemaConfig)
    if err != nil {
        log.Fatalf("failed to create schema error. err: %+v", err)
    }

    query := `{
      hello
  }`

    params := graphql.Params{
        Schema:        schema,
        RequestString: query,
    }

    r := graphql.Do(params)

    if len(r.Errors) > 0 {
        log.Fatalf("Failed to execute graphql operation. err:%+v", r.Errors)
    }

    j, err := json.Marshal(r)
    if err != nil {
        log.Fatalf("Failed to marshal json. err: %+v", err)
    }

    fmt.Printf("%s, \n", j)
}

動作させてみると

$ go run main.go
{"data":{"hello":"world"}},

Query(GETのリクエスト)では data というプロパティにGETした結果が入ってきます。
schemaFields で定義した "hello" プロパティに返さえるデータの型と結果 Resolve (ここではfuncで文字列を返しているだけですが)が入ってきます。

Getの結果をFiledType指定出来るのは非常に安全にリクエストを定義できていいなと思います。
このあたりは json-schema でも似たような異してますね。

ちなみに以下のような形で int型を入れてみた場合でも

// schema定義
fields := graphql.Fields{
  "hello": &graphql.Field{
    Type: graphql.String,
    Resolve: func(p graphql.ResolveParams) (interface{}, error) {
      return 1, nil
    },
  },
}

実行していみると

# int型
$ go run main.go
{"data":{"hello":"1"}},

nilにしてみます。

// schema定義
fields := graphql.Fields{
  "hello": &graphql.Field{
    Type: graphql.String,
    Resolve: func(p graphql.ResolveParams) (interface{}, error) {
      return nil, nil
    },
  },
}

出力結果は以下

$ go run main.go
{"data":{"hello":null}},

intの場合はjsonでも "1" とString型に、nilの場合は null がちゃんとjsonのdataプロパティ内の hello プロパティに入ってきました。
GraphQLにおいてはResponseの定義は FieldType に絶対変換されてくるので、定義を外出しておけば、クライアントサイドのエンジニアは定義を読めばクライアント側の実装には迷うことなさそうです。

まとめ

まずは簡単なクエリから書いてみました。
普段仕事でjson-schemaを使っているのですが、json形式でなく、コードの形で定義を管理できるのはこれはコレで見やすいと感じました。

次は簡単なAPIを書いてみたり、定義の外出しをやってみようと思います。

勉強記録はこちらに書いていこうと思います。

github.com

参考

developers.eure.jp

speakerdeck.com