GraphQLの基本

公開日:2024-10-04


はじめに

  • GraphQLは以前より業務で利用する機会があり、基本的な概念は理解していたが実際に自身で実装したことがなかったので、GraphQLの基本を手を動かして学習しました。
  • Query, Mutation, Subscriptionの基本的な使い方を学習し、BBSのバックエンドを参考にGraphQLサーバーを構築してみました。

GraphQLとは

  • APIのための問い合わせ言語
  • クライアント/サーバ通信のための言語仕様

GraphQLの特徴

  • データ取得のQuery, データの変更のMutation, データの変更の監視のSubscriptionの3つの操作が可能
  • APIはRESTのエンドポイントの集合でなく、型の集合。新しいAPIを作成する前に、APIのデータ型を定義する必要があり、このデータ型の集合をスキーマと呼ぶ。
  • スキーマにデータ型を定義するのに対して、データを取得する作業はリゾルバの役割。

Quick Start

  • gqlgenというGolangのGraphQLライブラリを利用して、GraphQLサーバーを構築します。
  1. go moduleの初期化

    mkdir gqlgen_practice
    cd gqlgen_practice
    go mod init gqlgen_practice
    
  2. gqlgenを追加

    printf '//go:build tools\npackage tools\nimport (_ "github.com/99designs/gqlgen"\n _ "github.com/99designs/gqlgen/graphql/introspection")' | gofmt > tools.go
    go mod tidy # add missing and remove unused modules
    
  3. gqlgenの初期化

    go run github.com/99designs/gqlgen init
    go mod tidy
    
  4. graphqlのサーバーを起動

    go run server.go
    
  5. http://localhost:8080/ にアクセスして、GraphQL Playgroundが表示されれば成功

  6. ディレクトリについて

    .
    ├── go.mod
    ├── go.sum
    ├── gqlgen.yml # gqlgenコマンドでコードを生成する際の設定を記述するyamlファイル
    ├── graph
    │   ├── generated.go # リゾルバをサーバーで稼働させるためのコアロジック部分(gqlgenで自動生成)
    │   ├── model
    │   │   └── models_gen.go # GraphQLのスキーマオブジェクトがGoの構造体として定義される(gqlgenで自動生成)
    │   ├── resolver.go # ルートリゾルバ構造体の定義
    │   ├── schema.graphqls # GraphQLのスキーマ定義
    │   └── schema.resolvers.go # ビジネスロジックを実装するリゾルバコードが配置
    ├── server.go # サーバーのエントリーポイント
    └── tools.go
    

バックエンドの実装

  • 以前REST APIで実装したBBSのバックエンドをGraphQLに移行してみます。
  • Github 出来上がったものはこちらのリポジトリにまとめています。
  1. ディレクトリの作成

    # リポジトリのクローン
    git clone [email protected]:ys39/Gin_bbs.git bbs-gql-project
    cd bbs-gql-project
    
    # リポジトリ設定
    git remote set-url origin [email protected]:ys39/Gin_gql_bbs.git
    
  2. モジュール名の変更

    go mod edit -module bbs-gql-project
    
  3. gqlgenの追加

    go install github.com/99designs/[email protected]
    go get -u github.com/99designs/[email protected]
    go run github.com/99designs/gqlgen init # gqlgenの初期化
    
  4. スキーマを記述

  • BBSへの投稿、取得、更新、削除の機能についてのスキーマを記述

    vim graph/schema.graphqls
    
    type Post {
      id: ID!
      title: String!
      content: String!
    }
    
    type Query {
      getAllPosts(page: Int!, per_page: Int!): [Post!]!
      getPost(id: ID!): Post!
    }
    
    input NewPost {
      title: String!
      content: String!
    }
    
    input updatePost {
      title: String!
      content: String!
    }
    
    type Mutation {
      createPost(input: NewPost!): Post!
      updatePost(id: ID!, input: updatePost!): Post!
      deletePost(id: ID!): Boolean!
    }
    
  1. ディレクトリ構成
  • 下記ファイルは必要ないため削除しました

    • controllers/
    • views/
    • openapi.yaml
    • server.go
  • 結果的に下記のようなディレクトリ構成になりました

    ├── README.md
    ├── go.mod
    ├── go.sum
    ├── gqlgen.yml
    ├── graph
    │   ├── generated.go
    │   ├── model
    │   │   └── models_gen.go
    │   ├── resolver.go
    │   ├── schema.graphqls # スキーマ定義
    │   └── schema.resolvers.go # リゾルバの実装
    ├── main.go # サーバーのエントリーポイント
    ├── models
    │   ├── error.go # エラーレスポンスの定義
    │   └── post.go # Postの構造体定義, 返すデータを定義
    ├── routers
    │   └── router.go # ルーティングの設定
    └── test
        └── graph_test.go # リゾルバのテスト
    
  1. リゾルバの実装
  • graph/schema.resolvers.goにリゾルバの実装を記述

    package graph
    
    import (
      "bbs-gql-project/graph/model"
      "bbs-gql-project/models"
      "context"
      "strconv"
    )
    
    // 投稿の詳細取得のリゾルバ
    func (r *queryResolver) GetPost(ctx context.Context, id string) (*model.Post, error) {
      postID, err := strconv.Atoi(id)
      if err != nil {
        return nil, models.BadRequestError("invalid ID format", "invalid ID format")
      }
    
      for _, post := range models.Posts {
        if post.ID == postID {
          return &model.Post{
            ID:      strconv.Itoa(post.ID),
            Title:   post.Title,
            Content: post.Content,
          }, nil
        }
      }
      return nil, models.NotFoundError("post not found", "post not found")
    }
    
    // Mutation returns MutationResolver implementation.
    func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
    
    // Query returns QueryResolver implementation.
    func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
    
    type mutationResolver struct{ *Resolver }
    type queryResolver struct{ *Resolver }
    
  1. エラーレスポンスの定義
  • GraphQLのレスポンスのmessageとして返すエラーメッセージを定義

    package models
    
    import (
      "fmt"
      "net/http"
    )
    
    // カスタムエラー構造体
    type AppError struct {
      Code    int    `json:"code"`    // HTTPステータスコード
      Message string `json:"message"` // エラーメッセージ
      Detail  string `json:"detail"`  // エラー詳細
    }
    
    // Errorメソッドを実装して、エラーメッセージを返す
    func (e *AppError) Error() string {
      return fmt.Sprintf("code: %d, message: %s, detail: %s", e.Code, e.Message, e.Detail)
    }
    
    // 新しいエラーを作成
    func NewAppError(code int, message string, detail string) *AppError {
      return &AppError{
        Code:    code,
        Message: message,
        Detail:  detail,
      }
    }
    
    // 404 Not Found
    func NotFoundError(message string, detail string) *AppError {
      return NewAppError(http.StatusNotFound, message, detail)
    }
    
    // 400 Bad Request
    func BadRequestError(message string, detail string) *AppError {
      return NewAppError(http.StatusBadRequest, message, detail)
    }
    
    // 500 Internal Server Error
    func InternalServerError(message string, detail string) *AppError {
      return NewAppError(http.StatusInternalServerError, message, detail)
    }
    
  1. ファイル生成
  • graph/generated.gograph/model/models_gen.goを生成するために以下のコマンドを実行

    go run github.com/99designs/gqlgen generate
    go mod tidy
    
  1. 実行と確認

    go run main.go
    # `http://localhost:8080/v1/gql/` へアクセスして下記を実行し、データ取得や更新ができるかを確認する
    
  • getPostクエリで投稿の詳細を取得

    query getPost {
      getPost(id: 1) {
        id
        title
        content
      }
    }
    
  • deletePostミューテーションで投稿を削除

    mutation deletePost {
      deletePost(id: 3)
    }
    
  • getAllPostsクエリで投稿一覧を取得

    query getAllPosts {
      getAllPosts(page: 1, per_page: 10) {
        id
        title
        content
      }
    }
    
  • updatePostミューテーションで投稿を更新

    mutation updatePost {
      updatePost(id: 4, input: { title: "qqq", content: "qqq2" }) {
        id
        title
        content
      }
    }
    
  • createPostミューテーションで投稿を作成

    mutation createPost {
      createPost(input: { title: "hoge", content: "peco" }) {
        id
        title
        content
      }
    }
    
  1. テストの実行

    # テスト
    cd ~/bbs-gql-project/test
    go test
    
    [GIN] 2024/10/04 - 19:07:40 | 200 |     183.993µs |                 | POST     "/v1/gql/query"
    [GIN] 2024/10/04 - 19:07:40 | 200 |      30.487µs |                 | POST     "/v1/gql/query"
    [GIN] 2024/10/04 - 19:07:40 | 200 |     255.297µs |                 | POST     "/v1/gql/query"
    [GIN] 2024/10/04 - 19:07:40 | 200 |      34.103µs |                 | POST     "/v1/gql/query"
    [GIN] 2024/10/04 - 19:07:40 | 200 |      22.703µs |                 | POST     "/v1/gql/query"
    PASS
    ok      bbs-gql-project/test    0.004s
    

全体的な所感

  • まだgo modあたりの使い方が身についておらず、gqlgenを使う際にも少し手間取っている。
  • go run main.goで起動したPlay Groundでクエリを実行することで、データの取得や更新ができるかを確認できるので、開発がしやすい。(途中でPostmanを利用してみたが、localhost:8080のアクセスに対してconnect ECONNREFUSED 127.0.0.1:8080のエラーが発生してしまい、うまくいかなかった。)
  • REST APIの際はコントローラーを作成して、エンドポイントに対して処理を記述していたが、GraphQLの場合はリゾルバを作成して、スキーマに対して処理を記述するのでディレクトリ構成が少し異なる。ここのベストプラクティスを学びたい。

まとめ

  • GraphQLの基本的な使い方をgqlgenを用いて学習し、BBSのバックエンドをGraphQLに移行してみました。
  • スキーマファースト or コードファーストのどちらがいいのかなどの考察ができていないので、今後はそのあたりも学習していきたいです。

参考


😄 END 😀