Help us understand the problem. What is going on with this article?

golangでGraphQLの素振りを行った

More than 1 year has passed since last update.

golangでGraphQLの素振りを行ったので,詰まったところ実装周りを書いていきます.

GraphQLとは

一つのエンドポイントに独自のクエリ言語を投げることで,多くのリソースを一回のリクエストで大量にゲットできるすごいもの.
RESTだと一つのエンドポイントごとにリソースを取得するのが,普通だと思うのでどうしてもエンドポイントが増えていきます.他にもリソースが増えてくると,複数リクエストを送らないといけなかったりいろいろきついです.
そんな問題を解決するために,GraphQLは出てきました.

こちらの記事👇にすごく詳しく書かれているので,ぜひ読んでください.

GraphQL入門 - 使いたくなるGraphQL

golangでのGraphQL実装

ライブラリがあるので,それを使用します.

今回使用したライブラリ👇

graphql-go/graphql

書いたもの

実際に書いたコードは👇に置いています.

mitubaEX/graphQL_sample

少し解説

まず main.go を見てみましょう.

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を実行するためには,schemaqueryが必要です.queryは,requestでもらったものをそのまま使っていて,schemagraphql_util.Schemaを利用しています.

schemaを見ていく

先ほどmain.goで使用していたgraphql_util.Schemaを見ていきます.

application/graphql_util/Schema.go
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,
})

スッキリしてますね.rootQueryrootMutationで構築されているようです.

rootQuery, rootMutationを見ていく

application/graphql_util/Query.go
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,
    },
})
application/graphql_util/Mutation.go
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を見てみる

application/graphql_util/fields/UserField.go
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を見てみると,keyTypeと書かれている所に返り値の型が書かれています.そしてArgsというkeyvalueに引数でもらってくる変数が書いています.返り値や引数という概念があるので,クエリは以下のように書けます.

{
  user(id: "1") {// 中身は何かしらの欲しいkeyを指定}
}

まるで関数ですね.そして最後にResolveで実際に行う処理の内容を書きます.
処理内容は,params.Args["id"]で引数の値を受け取り,そのidで検索をして結果を返すだけです.

こんな感じでどんどんリソースを増やしていってもエンドポイントは一つ(多分一つ)なので,多人数での開発でも大量のエンドポイントとリソースのドキュメントの共有などしなくて済みそうかなぁと思ったりしました.

データ追加について

GraphQLでデータを追加する時は,mutationというキーワードをクエリの前に追加します.
先ほどのapplication/graphql_util/fields/UserField.goCreateUserFieldは,rootMutationの方で使用されています.👇のような感じです.

var rootMutation = graphql.NewObject(graphql.ObjectConfig{
    Name: "RootMutation",
    Fields: graphql.Fields{
        "createUser":  fields.CreateUserField, // ここ
        "createEvent": fields.CreateEventField,
    },
})

CreateUserFieldは👇のような感じです.

application/graphql_util/fields/UserField.go
// 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を今後も触っていきたいと思います.

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした