GoでGraphQLのサーバーサイド実装をしてみたので備忘録がてら記事に残す。
準備
今回は厳密に型付けされたgraphqlサーバーを素早く作成するためのライブラリ vektah/gqlgen を使ってみる
$ go get -u github.com/vektah/gqlgen
実装
まずはプロジェクト配下に./schema.graphqlを作成する。
それぞれUser型、Query/Mutationの定義、入力パラメータの定義になる。
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ファイルを作成する
package graph
type User struct {
ID string
Name string
}
{
"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インターフェース
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)
}
次にこれを実装していく
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を受け取るエンドポイント
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
右側のSCHEMAタブを開くとスキーマ定義が確認出来て非常に便利
mutation.createUser
をリクエストしてみる
mutationのクエリを書いたらページ下の方のQUERY VARIABLES
タブを開いてパラメータを入力して実行
という感じで非常に簡単にGraphQLを話せるサーバを実装出来た。
Userの保存・取得をDBなどからするようにしたりすると、graph.goのr.users = append(r.users, user)
の部分やfor _, user := range r.users
の辺りがSQL発行に置き換わる事になるのだろう
DB等を扱うのは今回はやめて、もう少しスキーマ定義で遊んでみる
Userに紐づくTodoを登録出来るようにしてみる
Todo用の型やQuery/Mutationを定義
+ 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の定義を追加
{
"User": "github.com/s-ichikawa/gql-todo/graph.User",
"Todo": "github.com/s-ichikawa/gql-todo/graph.Todo"
}
graph.goにTodo型を追加
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
を追加している
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 VARIABLES
のNewTodo.userId
に設定してクエリを発行
成功してTodoとそれに紐づくUserの情報も取得出来ている。
ということで、ある型に関係する別の型の取得まで成功したので一旦ここまで。
追記
そういえば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:
...