LoginSignup
9
8

More than 5 years have passed since last update.

GoでGraphQLを話すサーバを作ってみた

Last updated at Posted at 2018-06-30

GoでGraphQLのサーバーサイド実装をしてみたので備忘録がてら記事に残す。

準備

今回は厳密に型付けされたgraphqlサーバーを素早く作成するためのライブラリ vektah/gqlgen を使ってみる

$ go get -u github.com/vektah/gqlgen

実装

まずはプロジェクト配下に./schema.graphqlを作成する。
それぞれUser型、Query/Mutationの定義、入力パラメータの定義になる。

schema.graphql
type User {
    id: ID!
    name: String!
}

type Query {
    user(id: String!): User
    users: [User!]!
}

input NewUser {
    name: String!
}

type Mutation {
    createUser(input: NewUser!): User!
}

次にgraphディレクトリを作成して以下2ファイルを作成する

graph/graph.go
package graph

type User struct {
    ID   string
    Name string
}
graph/type.json
{
  "User": "github.com/s-ichikawa/gql-todo/graph.User"
}

これでスキーマ定義は出来たので以下のコマンドを実行

$ cd path/to/project/graph
$ gqlgen -typemap type.json -schema ../schema.graphql

するとgraphディレクトリ内にgenerated.go, model_get.goが生成される
中身はスキーマ定義に従ってGraphQLサーバとして動作するためのコードで長いので割愛するが、大事なのはgenerated.goのResolverインターフェース

graph/generated.go
type Resolvers interface {
    Mutation_createUser(ctx context.Context, input NewUser) (User, error)
    Query_user(ctx context.Context, id string) (*User, error)
    Query_users(ctx context.Context) ([]User, error)
}

次にこれを実装していく

graph/graph.go
package graph

import (
    "context"
    "fmt"
    "math/rand"
    "github.com/pkg/errors"
)

type User struct {
    ID   string
    Name string
}

type Resolver struct {
    users []User
}

func (r *Resolver) Mutation_createUser(ctx context.Context, input NewUser) (User, error) {
    user := User{
        ID:   fmt.Sprintf("%d", rand.Int()),
        Name: input.Name,
    }
    r.users = append(r.users, user)
    return user, nil
}

func (r *Resolver) Query_user(ctx context.Context, id string) (*User, error) {
    for _, user := range r.users {
        if (user.ID == id) {
            return &user, nil
        }
    }
    return &User{}, errors.New("Sorry, Not Found.")
}

func (r *Resolver) Query_users(ctx context.Context) ([]User, error) {
    return r.users, nil
}

次にmain.goを作成
/でPlaygroundというクエリ実行が出来るページが開けるエンドポイント
/queryがGraphQLを受け取るエンドポイント

main.go
package main

import (
    "github.com/s-ichikawa/gql-todo/graph"
    "net/http"
    "github.com/vektah/gqlgen/handler"
    "fmt"
    "log"
)

func main() {
    resolvers := &graph.Resolver{}
    http.Handle("/", handler.Playground("Todo", "/query"))
    http.Handle("/query", handler.GraphQL(graph.MakeExecutableSchema(resolvers)))

    fmt.Println("Listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

で実行してみる

$ go run main.go 
Listening on :8080

localhost:8080にアクセス
スクリーンショット 2018-06-30 19.27.11.png

右側のSCHEMAタブを開くとスキーマ定義が確認出来て非常に便利
スクリーンショット 2018-06-30 19.27.45.png

mutation.createUserをリクエストしてみる
mutationのクエリを書いたらページ下の方のQUERY VARIABLESタブを開いてパラメータを入力して実行
スクリーンショット 2018-06-30 19.26.41.png

何度かユーザーを追加して次はquery.users
スクリーンショット 2018-06-30 19.36.14.png

最後にquery.user
スクリーンショット 2018-06-30 19.37.17.png

という感じで非常に簡単にGraphQLを話せるサーバを実装出来た。
Userの保存・取得をDBなどからするようにしたりすると、graph.goのr.users = append(r.users, user)の部分やfor _, user := range r.usersの辺りがSQL発行に置き換わる事になるのだろう

DB等を扱うのは今回はやめて、もう少しスキーマ定義で遊んでみる
Userに紐づくTodoを登録出来るようにしてみる

Todo用の型やQuery/Mutationを定義

schema.graphql
+ type Todo {
+     id: ID!
+     text: String!
+     done: Boolean!
+     user: User!
+ }

type Query {
    user(id: String!): User
    users: [User!]!
+     todo(id: String!): Todo!
+     todos: [Todo!]!
}

+ input NewTodo {
+     text: String!
+     userId: String!
+ }

type Mutation {
    createUser(input: NewUser!): User!
+    createTodo(input: NewTodo!): Todo!
}

type.jsonにTodoの定義を追加

graph/type.json
{
  "User": "github.com/s-ichikawa/gql-todo/graph.User",
  "Todo": "github.com/s-ichikawa/gql-todo/graph.Todo"
}

graph.goにTodo型を追加

graph/graph.go
type Todo struct {
    ID string
    Text string
    UserId string
}

再度gqlgenコマンドを実行

$ cd path/to/project/graph
$ gqlgen -typemap type.json -schema ../schema.graphql

追加されたResolverを実装する(長いので追加した部分だけ記載)
Userの時とほぼ変わらないが、スキーマ定義の try Todo {..., user: User!}を解決するためのfunc Todo_userを追加している

graph/graph.go
func (r *Resolver) Mutation_createTodo(ctx context.Context, input NewTodo) (Todo, error) {
    todo := Todo{
        ID:     fmt.Sprintf("T%d", rand.Int()),
        UserId: fmt.Sprintf("U%d", input.UserId),
        Text:   input.Text,
    }
    r.todos = append(r.todos, todo)
    return todo, nil
}

func (r *Resolver) Query_todo(ctx context.Context, id string) (Todo, error)  {
    for _, todo := range r.todos {
        if todo.ID == id {
            return todo, nil
        }
    }
    return Todo{}, errors.New("Not Found")
}

func (r *Resolver) Query_todos(ctx context.Context) ([]Todo, error) {
    return r.todos, nil
}

func (r *Resolver) Todo_user(ctx context.Context, obj *Todo) (User, error) {
    for _, user := range r.users {
        if (user.ID == obj.UserId) {
            return user, nil
        }
    }
    return User{}, errors.New("Sorry, Not Found.")
}

main.goを再起動して再度クエリを発行してみる

データの永続化はしていないので再度createUserを行いUser.Idを控える
そしてQUERY VARIABLESNewTodo.userIdに設定してクエリを発行
スクリーンショット 2018-06-30 20.23.25.png

成功してTodoとそれに紐づくUserの情報も取得出来ている。

一応query.todos
スクリーンショット 2018-06-30 20.24.00.png

ちなみに存在しないIDの場合はerrorsが返される
スクリーンショット 2018-06-30 20.33.19.png

ということで、ある型に関係する別の型の取得まで成功したので一旦ここまで。

追記

そういえばgo run main.goした時に以下のようなエラー出て 
$ go get github.com/gorilla/websocket したので一応それもメモ

$ go run main.go 
../../vektah/gqlgen/handler/graphql.go:10:2: cannot find package "github.com/gorilla/websocket" in any of:
...
9
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8