【オレが】GraphQLを用いたgo実装について解説するぜ【考える】

この記事は eureka Engineering Advent Calendar 2017 の19日目の記事です。
18日目は 鈴木さん の「非エンジニアがSQLを学習する際の11の心得)」でした。

自己紹介

今年2回目のはじめまして。PairsのGlobalチームでエンジニアリングをカリカリしてます @takochuu です。

go advent calendarにてGraphQLのサーバー側の実装を行ってみましたので、今回は githubql を使用してリクエストを投げるクライアント側のソースコードを実装して使用感を確認してみます。

お手柔らかに!

PairsでAPIが抱えている課題

Pairsのサーバーサイドはjsonを返却するAPIサーバーとして実装されており、基本的な考え方としてRESTを利用して実装されています。

RESTはURLをリソースとみなしており、例えば GET /user/1 というリクエストであれば user_id = 1 のリソースの取得と解釈します。

デメリットとして、Pairsのように1画面に複数の要素が入っているWebサービス( = アプリ)を構築しようとした時に単純にリクエスト数が増えるという問題があります。

APIの実行速度を抜いて考えても、単純にリクエスト数が増える事によってコネクション関連の手続きが増えることや、サーバーの同時接続数などの面からサーバー・クライアントにとって好ましいことではありません。

もう一つの問題として挙げられるのはリソースに対する変更・作成処理と取得処理が同じタイミングであることがサービス要件上存在するということです。

例えば、リソースを作成した結果を取得したいという時にRESTの思想であれば POST /user を叩いたあとに GET /user を叩かなければならないということになります。APIをCallする順番が制限されるということは、クライアント側に制約を強いる事であり、サービスの規模や開発チームの規模によってはデメリットです。

また、Webサービスを作る上で同時に複数のリソースを操作しなければならないケースもあります。

原則に則るのであれば複数のリソースに対してそれぞれ作成・変更操作をしなければならないはずですが、DBのtransactionの事を考えると複数のリクエストで一連の処理のつながりがAtomicに実行されることを担保するのは難しいと考えています。

Pairsとしても、RESTでの実装において上記で述べたようにつらい部分がいくつか出てきています。

例えばAPIをCallする順番が決まっているという制約が各デバイスに存在してしまっているなどです。

上記のような状況において、WebAPIのinterfaceとして検討を開始しているのが山本さんが書いているgrpcやGraphQLを用いたAPIの構築方法です。

GraphQL(githubql)を使ったクライアントの実装

ここからはいよいよ実装に入ります。

サーバーサイドの実装については、こちらの記事にて執筆したのでごらんください。

今回の記事はクライアントの処理なので、 サンプルとして githubql を使用してgithubのApiから情報を取得してみたいと思います。

まずテストとして、githubqlのページにあるサンプルを実装してみます。

GITHUB_TOKENを環境変数にセットすることを忘れるとエラーるのでご注意を

package main

import (
    "context"
    "fmt"
    "os"

    "github.com/shurcooL/githubql"
    "golang.org/x/oauth2"
)

var query struct {
    Viewer struct {
        Login     githubql.String
        CreatedAt githubql.DateTime
    }
}

func main() {
    src := oauth2.StaticTokenSource(
        &oauth2.Token{AccessToken: os.Getenv("GITHUB_TOKEN")},
    )
    httpClient := oauth2.NewClient(context.Background(), src)

    client := githubql.NewClient(httpClient)

    err := client.Query(context.Background(), &query, nil)
    if err != nil {
        // Handle error.
    }
    fmt.Println("    Login:", query.Viewer.Login)
    fmt.Println("CreatedAt:", query.Viewer.CreatedAt)
}

このソースコードを go run main.go で実行するとレスポンスがちゃんと返ってきていることを確認できます。

> go run main.go
    Login: takochuu
CreatedAt: 2010-02-21 05:40:04 +0000 UTC

githubqlはgithubAPIを使用するためのライブラリなので、リクエスト先のURLが内部でハードコーディングされています。

func NewClient(httpClient *http.Client) *Client {
    return &Client{
        client: graphql.NewClient("https://api.github.com/graphql", httpClient),
    }
}

今回はgithubqlの構造体マッピングを使用したいだけなので、内部実装を利用して他のサーバーへ Mutation を発行してみます。


githubql.goは内部構造を読んでいるとgithub.com/shurcooL/graphql/graphql.goのwrapperであることがわかります。

そのままgithub.com/shurcooL/graphql/graphql.goを利用することで任意のURLへクエリを発行してホゲホゲしたりすることが可能のはず、ということで自前で実装した以下のサーバーへ Mutation を発行してみます。

  • サーバーのソースコード
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"

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

var q graphql.ObjectConfig = graphql.ObjectConfig{
    Name: "query",
    Fields: graphql.Fields{
        "id": &graphql.Field{
            Type: graphql.ID,
            Args: graphql.FieldConfigArgument{
                "id": &graphql.ArgumentConfig{
                    Type: graphql.Int,
                },
            },
            Resolve: resolveID,
        },
        "name": &graphql.Field{
            Type:    graphql.String,
            Resolve: resolveName,
        },
    },
}

var m graphql.ObjectConfig = graphql.ObjectConfig{
    Name: "User",
    Fields: graphql.Fields{
        "user": &graphql.Field{
            Type: graphql.NewObject(graphql.ObjectConfig{
                Name: "Params",
                Fields: graphql.Fields{
                    "id": &graphql.Field{
                        Type: graphql.Int,
                    },
                    "address": &graphql.Field{
                        Type: graphql.NewObject(graphql.ObjectConfig{
                            Name: "state",
                            Fields: graphql.Fields{
                                "state": &graphql.Field{
                                    Type: graphql.String,
                                },
                                "city": &graphql.Field{
                                    Type: graphql.String,
                                },
                            },
                        }),
                    },
                },
            }),
            Args: graphql.FieldConfigArgument{
                "id": &graphql.ArgumentConfig{
                    Type: graphql.Int,
                },
            },
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                // ここで更新処理をする
                return User{
                    Id: 10000,
                    Address: Address{
                        State: "三宿",
                        City:  "世田谷区",
                    },
                }, nil
            },
        },
    },
}

type User struct {
    Id      int64   `json:"id"`
    Address Address `json:"address"`
}

type Address struct {
    State string `json:"state"`
    City  string `json:"city"`
}

var schemaConfig graphql.SchemaConfig = graphql.SchemaConfig{
    Query:    graphql.NewObject(q),
    Mutation: graphql.NewObject(m),
}
var schema, _ = graphql.NewSchema(schemaConfig)

func executeQuery(query string, schema graphql.Schema) *graphql.Result {
    r := graphql.Do(graphql.Params{
        Schema:        schema,
        RequestString: query,
    })

    if len(r.Errors) > 0 {
        fmt.Printf("エラーがあるよ: %v", r.Errors)
    }

    j, _ := json.Marshal(r)
    fmt.Printf("%s \n", j)

    return r
}

func handler(w http.ResponseWriter, r *http.Request) {
    bufBody := new(bytes.Buffer)
    bufBody.ReadFrom(r.Body)
    query := bufBody.String()

    result := executeQuery(query, schema)
    json.NewEncoder(w).Encode(result)
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

func resolveID(p graphql.ResolveParams) (interface{}, error) {
    return p.Args["id"], nil
}

func resolveName(p graphql.ResolveParams) (interface{}, error) {
    return "hoge", nil
}
  • クライアントのソースコード

githubqlを流用してgithub.com/graphql-go/graphqlを用いたサーバーへクエリを送るにはちょっとコツが要り、以下のソースコードをそのまま使うと"mutation":{"query":"mutation{user(id: 100){id,address{city,state}}}"}こんな感じにクエリが生成されます。

func main() {
    httpClient := &http.Client{Timeout: time.Duration(10) * time.Second}
    client := graphql.NewClient("http://localhost:8080", httpClient)
    err := client.Mutate(context.Background(), &mutation, nil)
    if err != nil {
        // Handle error.
    }

    fmt.Println("  Id:", mutation.User.Id)
}

ただ、github.com/graphql-go/graphqlはクエリに"mutation":の部分を要しないのでエラーってしまいます
→ ハンドリングの方法が違うかもしれない
なので、graphql.goをよしなにこんな感じに直す必要がある(content-typeは若干適当だけど)

func (c *Client) do(ctx context.Context, op operationType, v interface{}, variables map[string]interface{}) error {
    var query string
    switch op {
    case queryOperation:
        query = constructQuery(v, variables)
    case mutationOperation:
        query = constructMutation(v, variables)
    }
    resp, err := ctxhttp.Post(ctx, c.httpClient, c.url, "application/json", bytes.NewBuffer([]byte(query)))
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("unexpected status: %v", resp.Status)
    }
    var out struct {
        Data   json.RawMessage
        Errors errors
        //Extensions interface{} // Unused.
    }
    err = json.NewDecoder(resp.Body).Decode(&out)
    if err != nil {
        return err
    }
    if len(out.Errors) > 0 {
        return out.Errors
    }
    err = jsonutil.UnmarshalGraphQL(out.Data, v)
    return err
}

これで結果が返ってくるようになりました。

> go run main.go
  Id: 10000

おわりに

いい感じにクエリを投げられるようになったのだけれど、内部で graphql.goに依存してしまっているためクエリの受け方によっては結構しんどい感じだな、というのが今回の印象です。

ただ、内部構造は結構簡単なので、Newするタイミングで各サービス用のクエリビルダをDIするようにしたりすればよしなな感じでクライアントが作れるんじゃないかと思いました。

年末年始を利用してforkして色んなクライアントを作ってみようかなと思います。

明日はレコメンドといえばこの男、SREチームのJamesです。お楽しみに!

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

いますぐ始める高負荷対策

regexpとの付き合い方 〜 Go言語標準の正規表現ライブラリのパフォーマンスとアルゴリズム〜