golangでGraphQLの素振りを行ったので,詰まったところ実装周りを書いていきます.
GraphQLとは
一つのエンドポイントに独自のクエリ言語を投げることで,多くのリソースを一回のリクエストで大量にゲットできるすごいもの.
RESTだと一つのエンドポイントごとにリソースを取得するのが,普通だと思うのでどうしてもエンドポイントが増えていきます.他にもリソースが増えてくると,複数リクエストを送らないといけなかったりいろいろきついです.
そんな問題を解決するために,GraphQLは出てきました.
こちらの記事👇にすごく詳しく書かれているので,ぜひ読んでください.
golangでのGraphQL実装
ライブラリがあるので,それを使用します.
今回使用したライブラリ👇
書いたもの
実際に書いたコードは👇に置いています.
少し解説
まず main.go を見てみましょう.
package main
import (
"encoding/json"
"fmt"
"github.com/graphql-go/graphql"
"github.com/mitubaEX/graphQL_sample/application/graphql_util"
"io/ioutil"
"net/http"
)
func executeQuery(query string, schema graphql.Schema) *graphql.Result {
// schema と もらってきた query を入れて, graphql を実行
result := graphql.Do(graphql.Params{
Schema: schema,
RequestString: query,
})
if len(result.Errors) > 0 {
fmt.Printf("wrong result, unexpected errors: %v", result.Errors)
}
return result
}
func main() {
// POST /graphql のリクエストを受け取れるようにする
http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
// bodyの読み取り処理
body, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
// query 実行
result := executeQuery(fmt.Sprintf("%s", body), graphql_util.Schema)
json.NewEncoder(w).Encode(result)
})
fmt.Println("Now server is running on port 8080")
http.ListenAndServe(":8080", nil)
}
GraphQLを実行するためには,schema
とquery
が必要です.query
は,request
でもらったものをそのまま使っていて,schema
はgraphql_util.Schema
を利用しています.
schemaを見ていく
先ほどmain.goで使用していたgraphql_util.Schema
を見ていきます.
package graphql_util
import "github.com/graphql-go/graphql"
// define schema, with our rootQuery and rootMutation
var Schema, _ = graphql.NewSchema(graphql.SchemaConfig{
Query: rootQuery,
Mutation: rootMutation,
})
スッキリしてますね.rootQuery
とrootMutation
で構築されているようです.
rootQuery, rootMutationを見ていく
package graphql_util
import (
"github.com/mitubaEX/graphQL_sample/application/graphql_util/fields"
"github.com/graphql-go/graphql"
)
var rootQuery = graphql.NewObject(graphql.ObjectConfig{
Name: "RootQuery",
Fields: graphql.Fields{
"user": fields.UserField,
"userList": fields.UserListField,
"event": fields.EventField,
"eventList": fields.EventListField,
},
})
package graphql_util
import (
"github.com/mitubaEX/graphQL_sample/application/graphql_util/fields"
"github.com/graphql-go/graphql"
)
var rootMutation = graphql.NewObject(graphql.ObjectConfig{
Name: "RootMutation",
Fields: graphql.Fields{
"createUser": fields.CreateUserField,
"createEvent": fields.CreateEventField,
},
})
rootQuery
, rootMutation
共に,Fields
というkey
を持っています.これがRESTのリソースの概念に当たる部分です(多分).試しにfields.UserField
を見てみましょう.
fields.UserFieldを見てみる
package fields
import (
"errors"
"github.com/mitubaEX/graphQL_sample/application/graphql_util/types"
"github.com/mitubaEX/graphQL_sample/domain/model/user"
"github.com/mitubaEX/graphQL_sample/domain/service"
"github.com/mitubaEX/graphQL_sample/infrastructure"
"github.com/graphql-go/graphql"
)
// fetch single user
var UserField = &graphql.Field{
Type: types.UserType, // 返り値の型
Description: "Get single user",
Args: graphql.FieldConfigArgument{ //引数の定義
"id": &graphql.ArgumentConfig{
Type: graphql.String,
},
},
Resolve: func(params graphql.ResolveParams) (interface{}, error) { //実行関数
userId, isOK := params.Args["id"].(string) // 引数取り出し
if isOK {
return service.FindUserById(userId)
}
return nil, errors.New("no userId")
},
}
// fetch all user
var UserListField = &graphql.Field{
Type: graphql.NewList(types.UserType),
Description: "List of users",
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
return infrastructure.NewUserRepository().UserList(), nil
},
}
// create user
var CreateUserField = &graphql.Field{
Type: types.UserType,
Description: "Create new user",
Args: graphql.FieldConfigArgument{
"userName": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"description": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"photoURL": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"email": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
userName, _ := params.Args["userName"].(string)
description, _ := params.Args["description"].(string)
photoURL, _ := params.Args["photoURL"].(string)
email, _ := params.Args["email"].(string)
newUser, err := user.NewUser(userName, description, photoURL, email)
if err != nil {
panic(err)
}
infrastructure.NewUserRepository().Store(newUser)
return newUser, nil
},
}
いろいろ書いてますが,まずUserField
を見てみると,key
がType
と書かれている所に返り値の型が書かれています.そしてArgs
というkey
のvalue
に引数でもらってくる変数が書いています.返り値や引数という概念があるので,クエリは以下のように書けます.
{
user(id: "1") {// 中身は何かしらの欲しいkeyを指定}
}
まるで関数ですね.そして最後にResolve
で実際に行う処理の内容を書きます.
処理内容は,params.Args["id"]
で引数の値を受け取り,そのid
で検索をして結果を返すだけです.
こんな感じでどんどんリソースを増やしていってもエンドポイントは一つ(多分一つ)なので,多人数での開発でも大量のエンドポイントとリソースのドキュメントの共有などしなくて済みそうかなぁと思ったりしました.
データ追加について
GraphQLでデータを追加する時は,mutation
というキーワードをクエリの前に追加します.
先ほどのapplication/graphql_util/fields/UserField.go
のCreateUserField
は,rootMutation
の方で使用されています.👇のような感じです.
var rootMutation = graphql.NewObject(graphql.ObjectConfig{
Name: "RootMutation",
Fields: graphql.Fields{
"createUser": fields.CreateUserField, // ここ
"createEvent": fields.CreateEventField,
},
})
CreateUserField
は👇のような感じです.
// create user
var CreateUserField = &graphql.Field{
Type: types.UserType, // 返り値の型
Description: "Create new user",
Args: graphql.FieldConfigArgument{ // 引数の定義
"userName": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"description": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"photoURL": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"email": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
userName, _ := params.Args["userName"].(string)
description, _ := params.Args["description"].(string)
photoURL, _ := params.Args["photoURL"].(string)
email, _ := params.Args["email"].(string)
newUser, err := user.NewUser(userName, description, photoURL, email)
if err != nil {
panic(err)
}
infrastructure.NewUserRepository().Store(newUser)
return newUser, nil
},
}
処理内容は引数で値を受け取ってきて,NewUser
を呼んで新しいUser
を作成し,そのUser
を返すだけです.このCreateUserField
を実際にクエリで実行する時は👇のように書きます.
mutation{
createUser(userName:"mituba", description: "des", photoURL: "photo", email: "email"){
userId, userName, description, photoURL, email
}
}
mutation
というキーワードを先頭につけただけです.値の変更のクエリはmutation
を用いて,発行しましょう.
graphql-go/graphqlで起こったつらみ
引数にString型などを与える場合は問題なかったのですが,独自の型(User型など)を渡そうとしたらnilになってしまう問題が起こりました.👇の感じでクエリを投げたかった.
mutation{
createEvent(
user: {
userId: "1", userName:"mituba", description: "des", photoURL: "photo", email: "email"
},
eventName: "event",
description: "des",
location: "loc",
startTime: "start",
endTime: "end"){
eventId, user{userName}
}
}
issueを探したらそれっぽいのがあった.
解決策
NewObject
を使用して返り値用の型を作成して,NewInputObject
を使用して引数用の型を作成したらいけるらしい.
// 返り値用
var UserType = graphql.NewObject(graphql.ObjectConfig{
Name: "User",
Fields: graphql.Fields{
"userId": &graphql.Field{
Type: graphql.String,
},
"userName": &graphql.Field{
Type: graphql.String,
},
"description": &graphql.Field{
Type: graphql.String,
},
"photoURL": &graphql.Field{
Type: graphql.String,
},
"email": &graphql.Field{
Type: graphql.String,
},
},
})
// 引数用
var UserInput = graphql.NewInputObject(graphql.InputObjectConfig{
Name: "user",
Fields: graphql.InputObjectConfigFieldMap{
"userId": &graphql.InputObjectFieldConfig{
Type: graphql.String,
},
"userName": &graphql.InputObjectFieldConfig{
Type: graphql.String,
},
"description": &graphql.InputObjectFieldConfig{
Type: graphql.String,
},
"photoURL": &graphql.InputObjectFieldConfig{
Type: graphql.String,
},
"email": &graphql.InputObjectFieldConfig{
Type: graphql.String,
},
},
Description: "user input type",
})
他に良い方法があれば,ご助言いただけると助かります.
まとめ
とりあえず雑にですが,golangでGraphQLの素振りを行いました.
個人的に書いてて楽しいgolangとエンドポイント生やさなくてもたくさんリソースの処理が書けるGraphQLを今後も触っていきたいと思います.