Querier

GoとgqlgenではじめるGraphQL入門・チュートリアル

2023.07.20に公開 | 2023.07.20に更新

Querier運営

@querier_io@querierinc

「Querier(クエリア)」は社内向け管理画面を圧倒的な速さで、かつビジネスのスケールに合わせて柔軟に構築することができるローコードツールです。

管理画面の構築もWeb上で完結
エンジニアのためのローコードツール

Querierについて詳しく見る

この記事では、Go言語とgqlgenライブラリを活用してGraphQLサーバーを立ち上げる方法を学んでいきます。

実践的な例を用いつつ、サーバーの構築からエラーハンドリングまで、幅広く解説していきます。そして、それぞれの実装段階で該当する公式ドキュメンテーションへのリンクも提供します。

それでは、GoでのGraphQLの世界を一緒に探求していきましょう。

GraphQLとは

GraphQLはFacebookが開発したデータクエリと操作の言語です。

一般的なAPIとは異なり、クライアントは必要なデータを具体的に指定し、それに対する正確なレスポンスをサーバから得ることができます。これによりデータ転送を最小限に抑えつつ、必要なデータのみを効率的に取得することができます。

詳しくはこちらの記事をご覧ください。

gqlgenとは

gqlgenは、Go言語でGraphQLサーバーを構築するためのライブラリです。強力な型安全性と、高度な自動化を備えており、開発者の生産性を向上させます。

gqlgenはスキーマ駆動(Schema-First)の開発アプローチを採用しています。これは、まずGraphQLスキーマを定義し、そのスキーマからGoのコードを自動生成するという方法です。このアプローチにより、開発者は冗長なボイラープレートコードを書く必要がなくなり、ビジネスロジックの実装に集中できます。

さらに、gqlgenはGraphQLスキーマとGoのコード間の型安全性を維持します。これにより、開発中の型ミスを早期に検出し、バグの発生を防ぐことができます。

セットアップ

gqlgenを使ってGoでGraphQLサーバーを構築するための初期セットアップは以下の手順になります。

1. gqlgenのインストール

gqlgenはGoのパッケージマネージャーを使ってインストールします。

ターミナルを開き、以下のコマンドを実行します。

go get github.com/99designs/gqlgen

2. プロジェクトの作成

新しいディレクトリを作成し、その中で次のコマンドを実行します。

これにより、新しいgqlgenプロジェクトが初期化されます。

go run github.com/99designs/gqlgen init

このコマンドを実行すると、必要なファイルが自動的に生成されます。これにはGraphQLスキーマの定義を含むschema.graphqlsファイル、自動生成されるGoのコードを格納するgenerated.goファイルなどが含まれます。

以上がgqlgenの初期セットアップの手順です。

次のセクションでは、これらのファイルをどのように使用するのか、詳しく解説していきます。さらに詳しい情報は、gqlgenの公式ドキュメンテーションをご覧ください。

スキーマの記述

スキーマ記述はGraphQL APIの中心的な要素であり、APIの構造と機能を定義します。gqlgenでは、まずスキーマを記述し、それから必要なGoのコードを自動生成します。これは"スキーマ駆動開発"と呼ばれるアプローチです。
セットアップ時に初期化したschema.graphqlsファイルが、GraphQLスキーマを記述するためのファイルです。

以下に基本的なスキーマの記述例を示します。

type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}

type User {
  id: ID!
  name: String!
  todos: [Todo!]!
}

type Query {
  todos: [Todo!]!
}

type Mutation {
  createTodo(text: String!): Todo!
}

上記の例では、TodoUserという2つの型を定義しています。また、QueryMutationという特殊な型を定義しています。これらはGraphQL APIのエントリーポイントとなります。Queryはデータの取得、Mutationはデータの変更(作成、更新、削除)を扱います。

このスキーマにより、クライアントは必要なデータを正確に要求し、サーバーはそれに従ってデータを返すことができます。

このスキーマの記述を元に、次のステップであるコード生成を行います。GraphQLスキーマの書き方に関するより詳しい解説はこちらの記事をご覧ください。

コードの生成

GraphQLスキーマの記述が完了したら、gqlgenを使用して必要なGoのコードを自動生成します。生成されるコードは、スキーマで定義した型に基づくリゾルバーのインターフェースや、それらの型に対応するGoの構造体などを含みます。

以下のコマンドでコードの生成を行います。

go run github.com/99designs/gqlgen generate

このコマンドを実行すると、schema.graphqlsに基づいて、以下のようなファイルが生成されます。

  • generated.go: スキーマで定義した型に対応するGoの構造体や、リゾルバーのインターフェースなどを定義したファイルです。このファイルは自動生成されるため、直接編集しないでください。
  • models_gen.go: スキーマで定義した各型に対応するGoの構造体を定義したファイルです。
  • resolver.go: 生成されるリゾルバーのスケルトン(骨組み)です。このファイルに実際のリゾルバーの実装を書いていきます。

自動生成されるコードにより、開発者は冗長なボイラープレートコードの記述を避けることができ、ビジネスロジックの実装に専念することが可能になります。

リゾルバの実装

リゾルバとは、GraphQLのクエリやミューテーションが具体的にどのようにデータを処理するかを定義する部分です。gqlgenでは、リゾルバの骨組みが自動生成され、開発者は具体的なロジックを追加するだけでよくなります。

まず、サービス層のインターフェースを定義します。UserServiceTodoServiceの二つを例にします。

type UserService interface {
    GetUser(id string) (*User, error)
    GetUsers() ([]*User, error)
}

type TodoService interface {
    GetTodo(id string) (*Todo, error)
    GetTodos() ([]*Todo, error)
    CreateTodo(text string, userID string) (*Todo, error)
}

次に、これらのサービスをResolverに注入します。

以下のように、Resolver構造体に追加しましょう。

type Resolver struct{
    user UserService
    todo TodoService
}

そして、各リゾルバ関数の実装を行います。以下に、いくつかの例を示します。

func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
    return r.todo.GetTodos()
}

func (r *mutationResolver) CreateTodo(ctx context.Context, text string) (*model.Todo, error) {
    // ここでは一時的にユーザーIDを固定します。実際のアプリケーションでは認証から取得します。
    userID := "user1"
    return r.todo.CreateTodo(text, userID)
}

func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
    return r.user.GetUser(obj.UserID)
}

以上のように、リゾルバの実装により、具体的なデータの取得や変更のロジックが実現します。これらのリゾルバ関数は、クライアントからのGraphQLクエリやミューテーションに対応します。

より詳しい情報は、gqlgenの公式ドキュメンテーションで確認できます。

サーバーの実装

サーバーの実装では、go-chiというパッケージを使用してHTTPサーバーを作成し、gqlgenで生成したGraphQLハンドラーを組み込むことでGraphQL APIを実現します。

まず、go-chiをプロジェクトに追加します。

go get github.com/go-chi/chi

次に、go-chiを用いてHTTPサーバーをセットアップします。この際、gqlgenで生成されたGraphQLハンドラーをサーバーに組み込むことが重要です。

package main

import (
   "net/http"

    "github.com/99designs/gqlgen/graphql/handler"
    "github.com/99designs/gqlgen/graphql/playground"
    "github.com/go-chi/chi"

    "your/app/graph"
    "your/app/graph/generated"
)

func main() {
    // Resolverをインスタンス化し、各サービスを注入します。
    // ここではサンプルとしてnilを渡しますが、実際には適切なサービスのインスタンスを生成・注入します。
    srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))

    // ルーターの作成
    r := chi.NewRouter()

    // GraphQLのエンドポイントの設定
    r.Handle("/query", srv)

    // playground(開発用のインタラクティブなクエリエディタ)の設定
    r.Handle("/", playground.Handler("GraphQL playground", "/query"))

    // サーバーの起動
    err := http.ListenAndServe(":8080", r)
    if err != nil {
        panic(err)
    }
}

上記の例では、/queryエンドポイントでGraphQL APIを提供し、/エンドポイントでplaygroundを提供しています。playgroundはブラウザから直接GraphQLクエリを試すことができるツールで、開発中に便利です。

また、GraphQL固有の設定については、handler.NewDefaultServer関数にgenerated.Configを渡すことで行います。ここでは特に設定を指定していませんが、ここでTransportやCacheなど、GraphQLサーバーの挙動をカスタマイズすることが可能です。

詳細はgqlgenの公式ドキュメンテーションをご覧ください。

N+1の回避

GraphQLを利用する際、よく遭遇する問題の一つがN+1問題です。これは、一つのリゾルバが多数のリゾルバを引き起こし、その結果として発生する多数のデータソースへのアクセスがパフォーマンス低下を引き起こすという問題です。

具体的には、例えば以下のようなクエリがあるとします。

{
  users {
    id
    name
    todos {
      id
      text
    }
  }
}

上記のクエリでは、各ユーザーのTodoリストを取得するために、全てのユーザーに対してTodoリストを取得するためのクエリが発生します。ユーザーが100人いる場合、Todoリストを取得するクエリが100回実行されることになり、これがN+1問題です。

この問題を解決するための一つの方法として、データローダー(Dataloader)パターンがあります。データローダーは、一つのリクエストの中で同じデータソースに対する複数のリクエストをバッチ処理することで、リクエストの数を大幅に減らすことができます。

ここでは、gqlgenと組み合わせてよく使われるgithub.com/graph-gophers/dataloader/v7というライブラリを用いてN+1問題を解決する方法を紹介します。

まず、dataloaderパッケージをインストールします。

go get github.com/graph-gophers/dataloader/v7

次に、Todoオブジェクトに対するユーザー情報のローディングを例に、dataloaderの使用方法を説明します。

まず、BatchFuncを実装します。この関数は、キーの集合を受け取り、それに対応する値の集合を返す役割を果たします。この関数の中で一度に全てのユーザーデータをロードします。

func UserLoaderBatchFn(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
    userIds := keys.Keys()
    // userStoreを用いて一度に全てのユーザーデータをロードします。
    users, _ := userStore.ListUsersByIDs(ctx, userIds)
    var results []*dataloader.Result
    for _, user := range users {
        results = append(results, &dataloader.Result{Data: user})
    }
    return results
}

そして、このBatchFuncを使って*dataloader.Loaderを作成し、これをリクエストのコンテキストに保存します。

loader := dataloader.NewBatchedLoader(UserLoaderBatchFn)
ctx = context.WithValue(ctx, "userLoader", loader)

最後に、各リゾルバでこのローダーを使用してユーザーデータをロードします。

func (r *todoResolver) User(ctx context.Context, obj *model.Todo) (*model.User, error) {
    // コンテキストからローダーを取り出します。
    loader := ctx.Value("userLoader").(*dataloader.Loader)
    // ローダーを使用してユーザーデータをロードします。
    thunk := loader.Load(ctx, dataloader.StringKey(obj.UserID))
    user, _ := thunk()
    return user.(*model.User), nil
}

以上のように、dataloaderを使用することで、一つのリクエスト中で同じデータソースへのアクセスをバッチ処理し、N+1問題を解消することができます。

詳細は公式ドキュメントをご覧ください。

認証

GraphQLサーバーでの認証は重要な概念で、go-chiのmiddlewareを活用して認証を行うことができます。以下では、JWT(Json Web Tokens)を用いた簡単な認証システムを実装する方法を説明します。

まず、JWTの生成と検証を行うためのヘルパーファンクションを作成します。

import (
    "github.com/dgrijalva/jwt-go"
)

var jwtKey = []byte("your-secret-key")

func GenerateToken(user *model.User) (string, error) {
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "userId": user.ID,
    })

    tokenString, err := token.SignedString(jwtKey)
    if err != nil {
        return "", err
    }

    return tokenString, nil
}

func ValidateToken(tokenString string) (string, error) {
    claims := &jwt.MapClaims{}

    token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
        return jwtKey, nil
    })

    if err != nil {
        return "", err
    }

    if claims, ok := token.Claims.(*jwt.MapClaims); ok && token.Valid {
        userId := (*claims)["userId"].(string)
        return userId, nil
    }

    return "", fmt.Errorf("invalid token")
}

次に、go-chiのmiddlewareを作成してJWTを検証し、その結果をコンテキストに保存します。

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        authHeader := r.Header.Get("Authorization")
        tokenString := strings.TrimPrefix(authHeader, "Bearer ")

        userId, err := ValidateToken(tokenString)
        if err != nil {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }

        ctx := context.WithValue(r.Context(), "userId", userId)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

最後に、このmiddlewareをgo-chiのルーターに追加します。

r := chi.NewRouter()
r.Use(AuthMiddleware)
r.Handle("/", handler.GraphQL(
    gqlgen.NewExecutableSchema(gqlgen.Config{Resolvers: &gqlgen.Resolver{}}),
))

これで、各リクエストでJWTが検証され、その結果がリクエストのコンテキストに保存されます。そのため、各リゾルバ内でctx.Value("userId")を呼び出すことで、認証されたユーザーのIDを取得することができます。

以上が認証の基本的な流れです。必要に応じて機能を拡張したり、セキュリティを強化したりしてください。より詳しい認証の実装方法については、公式ドキュメントを参照してください。

エラーハンドリング

GraphQLのエラーハンドリングは、エラーが発生した場合にクライアントにエラーメッセージを適切に伝えるための重要な機能です。

まず、エラーを返す基本的な方法は、リゾルバからerror型を返すことです。gqlgenはこれを自動的に検出し、エラーメッセージとともにエラーレスポンスを生成します。

以下は、リゾルバからエラーを返す基本的な例です。

func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
    user, err := r.user.LoadUser(ctx, id)
    if err != nil {
        return nil, fmt.Errorf("failed to load user: %v", err)
    }
    return user, nil
}

これはエラーメッセージのみを含むシンプルなエラーレスポンスを生成しますが、エラーメッセージだけでは不十分な場合もあります。そのような場合は、gqlerror.Errorを使用して、エラーオブジェクトに追加情報を含めることができます。

return nil, &gqlerror.Error{
    Message: "failed to load user",
    Extensions: map[string]interface{}{
        "userID": id,
    },
}

このエラーオブジェクトは、エラーメッセージとともに追加のキーと値のペアを含むExtensionsフィールドを提供します。これを使用して、エラーのコンテキストに関する追加情報を提供することができます。

また、一つの操作中に複数のエラーが発生した場合、それらをすべて一度にクライアントに返すことができます。これは、クライアントが必要な修正を一度に行うことを可能にします。

これは、エラーレスポンスのerrorsフィールドが配列であるため可能です。各リゾルバは独立して動作し、それぞれがエラーを返すことが可能で、それらのエラーはすべてクライアントに返されます。

詳細なエラーハンドリングの方法については、gqlgenの公式ドキュメンテーションをご覧ください。

さいごに

GraphQLとgqlgenを使用したAPIの設計と構築は、強力で効率的なツールを提供します。

しかし、これらのツールを最大限に活用するには、その基本的な概念と仕組みを理解することが不可欠です。

この記事があなたのGraphQLとgqlgenに対する理解を深めるのに役立つことを願っています。

「Querier(クエリア)」は社内向け管理画面を圧倒的な速さで、かつビジネスのスケールに合わせて柔軟に構築することができるローコードツールです。

最新の記事

【告知】値の参照時の仕様変更のお知らせ

このたび2024年11月11日に値の参照に関する仕様変更を予定しておりますので詳細について報告いたします。

more

管理画面の構築もWeb上で完結
エンジニアのためのローコードツール

Querierについて詳しく見る