Go
GraphQL
GoDay 15

最近流行っている(?)GraphQLについて書くぜ

この記事は go Advent Calendar 2017 の15日目の記事です。

introduction

goでapiを書いている皆さんは、apiのインターフェースとして何を使用していますか?
json形式でレスポンスを返すのが一般的だと思いますが、grpcやGraphQLなど様々な技術が最近登場しています。

今回は、GraphQLの概要とgoでの実装について解説したいと思います。

GraphQLの概要

GraphQLはFacebookが開発したクエリ言語です。2015年のReact.js Confで発表されました。
国内での流行りはそれほどではありませんがgithubが採用したりいくつかの企業が採用したことによって知名度が上がりつつあります。

GraphQLの大まかな特徴として、クライアントが欲しい情報を指定することが可能ということと、1度のリクエストで多く(任意)のリソースの取得が可能だということの2つがあります。

ためしにgithubのAPIを叩いて情報を取得してみるコードをGithub api Explorerで試してみましょう

{
  viewer {
    login,
    avatarUrl,
    company,
    name,
    url
  }
}

リンク先でこのようなクエリを書くと、下記のようなレスポンスが返ってきます。


{
  "data": {
    "viewer": {
      "login": "takochuu",
      "avatarUrl": "https://avatars1.githubusercontent.com/u/207675?v=4",
      "company": "Japan",
      "name": "",
      "url": "https://github.com/takochuu"
    }
  }
}

ね?簡単でしょう。

今回は、goでGraphQLを使用したAPIサーバーのソースコードをgoで作ります。

GraphQLを使ったサーバーの実装

ここからはいよいよ実装に入ります。
introductionとしてはこちらのtutorialを実施してもらうのが良いと思います。

今回、GraphQLのライブラリとして github.com/graphql-go/graphql を利用します。
1ヶ月程masterにcommitされていないのが気になりますが、今のところgoでGraphQLを扱う際のライブラリではstar数が最大となっているのでこちらで。

Query(読み込み)

まずは、GraphQLを用いて読み込みを行うサーバーサイドの処理を実装してみます。

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,
            Resolve: resolveID,
        },
        "name": &graphql.Field{
            Type:    graphql.String,
            Resolve: resolveName,
        },
    },
}

var schemaConfig graphql.SchemaConfig = graphql.SchemaConfig{
    Query: graphql.NewObject(q),
}
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 1, nil
}

func resolveName(p graphql.ResolveParams) (interface{}, error) {
    return "hoge", nil
}

goにおけるGraphQLのサーバーサイドの実装では、このように schemaを定義して graphql.Do にschemaとクエリを渡す事で処理します。
go run main.go でサーバーを立てて、curlでこのようにデータを取得してみます。

#!/bin/bash
curl -X POST -d '{ id }' http://localhost:8080/
curl -X POST -d '{ name }' http://localhost:8080/
curl -X POST -d '{ id, name }' http://localhost:8080/

サーバーから返却されたレスポンスはこちらです

{"data":{"id":"1"}}
{"data":{"name":"hoge"}}
{"data":{"id":"1","name":"hoge"}}

上記のように、curlで指定したfieldと対応する値が返却されていることがわかります。

"id": &graphql.Field{
            Type:    graphql.ID,
            Resolve: resolveID,
        },
},

指定されたfieldに対するvalueはResolveに対して定義している resolveIDresolveName で値を生成して返却しています。

さらに、Queryに引数を食わせることもできます。
先程記述した、サーバーサイドのコードを一部書き換えて下記のようにします。

        "id": &graphql.Field{
            Type: graphql.ID,
            Args: graphql.FieldConfigArgument{
                "id": &graphql.ArgumentConfig{
                    Type: graphql.Int,
                },
            },
            Resolve: resolveID,
        },
func resolveID(p graphql.ResolveParams) (interface{}, error) {
    return p.Args["id"], nil
}

この状態で、先程実行したcurlを変更し、このようなクエリを投げます。

curl -X POST -d '{ id(id: 100), name }' http://localhost:8080/

レスポンスを見ると、このように引数として渡した 100 が idのvalueとして帰ってきていることがわかります。

{"data":{"id":"100","name":"hoge"}}

今回は端折っていますが、具体的にサービスを実装する際は Resolveにマッピングした関数に渡ってきた引数を利用して、データを引っ張ることになります。

Mutation(書き込み / 変更)

GraphQLでは、読み込み時はqueryを使用しますが、書き込み等のWrite操作を行う際にはmutation を使用します。
先程提示したソースコードは、queryを用いてデータの取得処理を行うものでしたが、次は mutation を用いたサーバーの実装を紹介します。

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),
}

先程のソースコードを改造して、graphql.SchemaConfig に対してMutationフィールドを定義をします。
その上で、同じように graphql.ObjectConfigを定義してあげます。

上記ソースコードのように構造体を定義しtagでマッピングすることも可能です。
早速クエリを投げてみましょう。

  • リクエスト
curl -X POST -d 'mutation { user(id: 100){ id, address{ state, city }}}' http://localhost:8080/
  • レスポンス
{"data":{"user":{"address":{"city":"世田谷区","state":"三宿"},"id":10000}}}

おわりに

あまり国内ではGraphQLが盛り上がっているという話を聞かないので
これから国内での導入事例が増えていくといいなーとおもっています。

簡単ですが、GraphQLの紹介として以上とさせてもらいます。翌日もお楽しみに!