3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GoでGraphQLを導入する

Posted at

今回はgqlgenを使ってGraphQLのサーバーを立ち上げるまでのチュートリアルを作成していきます

GraphQLってどういうもの?

RESTとの違いを示します

REST

  • 複数のエンドポイント
  • エンドポイントごとに固定のデータを取得
  • HTTPメソッドによって実行する処理を決定
  • HTTPステータスコードでエラーハンドリング
  • 型定義があいまい

GraphQL

  • 単一のエンドポイント
  • エンドポイントから必要なデータを指定して取得
  • query, mutationによって実行する処理を決定
  • メッセージでエラーハンドリング
  • 型定義が厳密

RESTでは,クライアントが必要なデータを取得するために複数のエンドポイントから過剰なデータを取得していました

GraphQLでは,クライアントは単一のエンドポイントから必要な分だけデータを取得することが可能で,無駄なデータ通信やリクエストを削減できます

GoでGraphQLを使ってみる

GoのGraphQLライブラリとしてgqlgenがあります

インストール方法

    go get github.com/99designs/gqlgen@latest
    go install github.com/99designs/gqlgen@latest

    go get github.com/99designs/gqlgen/graphql/handler

GraphQLプロジェクトの作成

プロジェクトの初期化

    gqlgen init

デフォルトではこのような構成でファイルが生成されます

graph/は後で作り直すので,一旦削除しましょう

    .
    ├── gqlgen.yml
    ├── graph
    │   ├── generated.go # GraphQLサーバー (触らない)
    │   ├── model
    │   │   └── models_gen.go # スキーマで定義したmodel (触らない)
    │   ├── resolver.go # 雛形 (触る)
    │   ├── schema.graphqls # スキーマ (触る)
    │   └── schema.resolvers.go # 雛形 (触る)
    └── server.go

GraphQL スキーマの定義

gqlgen*.graphqlsに記述したGraphQLのスキーマから,雛形となるコードを自動生成してくれます

api/graphディレクトリを作成し,今回作成するGraphQLのスキーマとしてapi/graph/todo.graphqlsを用意します

  • (参考) https://graphql.org/learn/schema

  • GraphQLのデータ操作は,QueryMutationに分けられます

  • Queryはデータ取得に相当します

  • Mutationはそれ以外の操作に相当します

todo.graphqls
    # Todoモデル
    type Todo {
        id: ID!
        text: String!
        done: Boolean!
    }

    # データ取得
    type Query {
        todos: [Todo!]!
    }

    # mutationのinput
    input NewTodo {
        text: String!
    }

    # データ作成
    # ここにupdateTodoとかdeleteTodoとかも書いたりする
    type Mutation {
        createTodo(input: NewTodo!): Todo!
    }

このスキーマから雛形となるコードを自動生成してみましょう

自動生成されるコードの構成はgqlgen.ymlで指定できます

今回は次の構成にしてみます

gqlgen.yml
    # スキーマファイルへのパス
    schema:
        - api/graph/*.graphqls

    # 自動生成されるコード (サーバー内部の動作など)
    exec:
        filename: api/graph/generated.go
        package: graph

    # 自動生成されるコード (モデル)
    model:
        filename: api/graph/types.gen.go
        package: graph

    # 自動生成される雛形 (これを実装する)
    resolver:
        layout: follow-schema
        dir: internal/resolver
        package: resolver
        filename_template: "{name}.resolvers.go"

    (略)

変更したgqlgen.ymlをもとにコードを再生成します

  • server.goがエラーを吐いてしまうので中身をすべてコメントアウトしておいたほうが良いかもしれません
    gqlgen generate

プロジェクトの構成を変えたので,合うようにserver.goを修正します

server.go
    package main

    import (
        "log"
        "net/http"
        "os"

        "github.com/99designs/gqlgen/graphql/handler"
        "github.com/99designs/gqlgen/graphql/playground"
        "github.com/tf63/go-graph-exp/api/graph"         // 修正
        "github.com/tf63/go-graph-exp/internal/resolver" // 修正
    )

    const defaultPort = "8080"

    func main() {
        port := os.Getenv("PORT")
        if port == "" {
            port = defaultPort
        }

        srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: &resolver.Resolver{}})) // 修正

        http.Handle("/", playground.Handler("GraphQL playground", "/query"))
        http.Handle("/query", srv)

        log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
        log.Fatal(http.ListenAndServe(":"+port, nil))
    }

次の構成になっているはずです

.
├── api
│   └── graph
│       ├── generated.go # 触らない
│       ├── todo.graphqls # スキーマファイル
│       └── types.gen.go # 触らない
├── gqlgen.yml
├── internal
│   └── resolver
│       ├── resolver.go # 触る
│       └── todo.resolvers.go # 触る
└── server.go # 触る

Resolverの実装

いよいよResolverを実装していきましょう.internal/resolver/のファイルをいじっていきます

デフォルトでは次のようになっています

resolver.go
    package resolver

    type Resolver struct{}
todo.resolvers.go
    package resolver

    import (
        "context"
        "fmt"

        "github.com/tf63/go-graph-exp/api/graph"
    )

    // CreateTodo is the resolver for the createTodo field.
    func (r *mutationResolver) CreateTodo(ctx context.Context, input graph.NewTodo) (*graph.Todo, error) {
        panic(fmt.Errorf("not implemented: CreateTodo - createTodo"))
    }

    // Todos is the resolver for the todos field.
    func (r *queryResolver) Todos(ctx context.Context) ([]*graph.Todo, error) {
        panic(fmt.Errorf("not implemented: Todos - todos"))
    }

    // Mutation returns graph.MutationResolver implementation.
    func (r *Resolver) Mutation() graph.MutationResolver { return &mutationResolver{r} }

    // Query returns graph.QueryResolver implementation.
    func (r *Resolver) Query() graph.QueryResolver { return &queryResolver{r} }

    type mutationResolver struct{ *Resolver }
    type queryResolver struct{ *Resolver }

todo.resolvers.goCreateTodo, Todosを実装していきます

本記事では,実際にDBに接続して動作するところまでは紹介しません

(Github上では実装しているので興味があれば)

todo.resolvers.go
    // CreateTodo is the resolver for the createTodo field.
    func (r *mutationResolver) CreateTodo(ctx context.Context, input graph.NewTodo) (*graph.Todo, error) {
        // inputを元にTodoを生成
        // -----------------------------------
        // (今回は実装しません)
        // -----------------------------------

        // 作成したTodoを返す
        todo1 := graph.Todo{
            ID:   "1",
            Text: "todo 1",
            Done: false,
        }
        return &todo1, nil
    }

    // Todos is the resolver for the todos field.
    func (r *queryResolver) Todos(ctx context.Context) ([]*graph.Todo, error) {
        // テストデータを返す
        todos := []*graph.Todo{
            {ID: "1", Text: "todo 1", Done: false},
            {ID: "2", Text: "todo 2", Done: false},
            {ID: "3", Text: "todo 3", Done: false},
        }

        return todos, nil
    }

GraphQLサーバーの起動

GraphQLのサーバーを起動してみます

一応Dockerfileを用意してあります

    go run server.go

http://localhost:8080にアクセスするとGraphQL PlayGroundが表示されます
graph.png

先程作成したcreateTodoを実行してみます

実行結果としてid, textを取得してみます

意味はないですが,引数にはtext: "todo 1"を指定します

    mutation {
        createTodo (input: {text: "todo 1"}) {
            id # idを返す
            text # textを返す
        }
    }

id, textを含んだレスポンスが返ってきます

    {
        "data": {
            "createTodo": {
                "id": "1",
                "text": "todo 1"
            }
        }
    }

queryも実行してみましょう

todoのid, text, done属性を選んで取得してみます

    query {
        todos {
            id,
            text,
            done
        }
    }

先程登録したテストデータが返ってきます

    {
        "data": {
            "todos": [
                {
                    "id": "1",
                    "text": "todo 1",
                    "done": false
                },
                {
                    "id": "2",
                    "text": "todo 2",
                    "done": false
                },
                {
                    "id": "3",
                    "text": "todo 3",
                    "done": false
                }
            ]
        }
    }

graph-1.png

以上です.余裕があればロジック部分も記事にします

参考

gqlgen公式チュートリアル

GraphQL スキーマ

GraphQLベストプラクティス

API設計の参考

3
3
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
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?