🎃

レッスンのマスターデータAPIをGraphQLで構築!問題点と対策

2023/09/22に公開

ライフイズテックのサービス開発部 学校プロダクトグループに所属している程です。


ライフイズテックが提供する様々なサービス、例えばテックレッスン、AIドリル、共通テスト模試などは、教材チームが作成したマスターデータを基にしています。このデータをもっと簡単に扱うために、GraphQL APIを導入することに決めました。この記事では、その過程で出会った問題点や、実際の経験について詳しく共有していきます。

1. N+1問題

課題:

GraphQLを使った初めてのマスターデータの取得で、N+1問題に直面しました。他社様の記事でも同じ課題と対策がたくさん公開されています。具体的に、複数の関連するデータを一度のクエリで取得する利点がある一方で、データベースのクエリが予期せず増える問題が発生しました。

解決策:

N+1問題の解決のために、graphql-batchとdataloaderの2つのオプションを検討しました。

graphql-batch: Shopifyが提供しているRuby向けのライブラリ。PromiseベースのAPIを提供しており、ローダーを定義することでバッチ処理をサポートします。
dataloader: Facebookが提供しているJavaScript向けのライブラリ。キャッシュやバッチ処理をサポートしており、多数の言語実装が存在します。
我々のプロジェクトはRuby on Railsをベースとしていたため、graphql-batchを選定しました。これにより、関連するデータの取得を効率的に行うことができました。

query {
  categories {
    id
    name
    products {
      id
      name
    }
  }
}

上のようなクエリでも、N+1問題を解決して効率的にデータを取得できるようになりました。

2. 型の不一致

課題:

マスターデータは、他のシステムや外部のデータベースと連携しているため、データの型が一致しない場面に遭遇しました。

解決策:

カスタムスカラを導入して、特定の型のデータをGraphQLの型システムに合わせることで、型の不一致を解消しました。

scalar CustomDate

type Product {
  id: ID!
  releaseDate: CustomDate
}

このCustomDateスカラを用いることで、特定の日付フォーマットや扱いに対応することができました。

3. クエリの複雑性

課題:

一部のクライアントが非常に複雑なGraphQLクエリを発行することがあり、これがサーバのパフォーマンスに悪影響を与える可能性がありました。

解決策:

graphql-ruby ライブラリ(多くのRailsプロジェクトで使用されています)は、クエリの深さや複雑性を制限する機能を提供しています。以下は、そのサンプルコードです。

クエリの深さを制限する

# app/graphql/my_app_schema.rb
class MyAppSchema < GraphQL::Schema
  query(Types::QueryType)
  
  # クエリの深さが10を超えた場合はエラーとする
  max_depth 10
end

クエリのコストを計算して制限する

まず、コスト計算のメソッドを定義します。

# app/graphql/base_field.rb
class BaseField < GraphQL::Schema::Field
  def resolve_field_method
    ->(obj, args, ctx) {
      # コストをインクリメント
      ctx[:query_cost] ||= 0
      ctx[:query_cost] += self.cost || 1

      # 制限を超えた場合はエラー
      if ctx[:query_cost] > 1000
        raise GraphQL::ExecutionError.new("Query cost too high")
      else
        # 通常の解決処理
        obj.public_send(self.method)
      end
    }
  end
end

次に、各フィールドでコストを設定します。

# app/graphql/types/user_type.rb
class Types::UserType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :name, String, null: false, cost: 2
  field :posts, [Types::PostType], null: false, cost: 10
end

以上のように設定することで、クエリのコストが計算され、設定値を超えるクエリは実行されなくなります。

ミドルウェアを使う

もしさらに柔軟な制御が必要な場合は、ミドルウェアを使って独自のロジックを追加することも可能です。

# app/graphql/middleware/query_limiter.rb
class Middleware::QueryLimiter
  def call(parent_type, parent_object, field_definition, field_args, query_ctx)
    query_cost = query_ctx[:query_cost] || 0
    query_cost += field_definition.metadata[:cost] || 1
    raise GraphQL::ExecutionError, "Query cost too high" if query_cost > 1000
    query_ctx[:query_cost] = query_cost
    yield
  end
end

ミドルウェアをスキーマに追加する:

# app/graphql/my_app_schema.rb
class MyAppSchema < GraphQL::Schema
  query(Types::QueryType)
  use(Middleware::QueryLimiter)
end

これで、クエリの複雑性に対する対策が施されました。クエリがある深さやコストを超えると、クライアントにエラーレスポンスが返されます。これにより、サーバの過度な負荷を防ぐことができます。

まとめ

GraphQLでのマスターデータAPIの構築は数々の課題を伴いましたが、その都度適切な解決策を取ることで、効率的なデータ取得を実現しました。適切なツールやライブラリの導入、そしてGraphQLの柔軟性を最大限に活用することで、これらの課題を克服することができました。


ライフイズテック サービス開発部では、気軽にご参加いただけるカジュアルなイベントを実施しています。開催予定のイベントは、 connpass のグループからご確認ください。興味のあるイベントがあったらぜひ参加登録をお願いいたします。皆さんのご参加をお待ちしています!

Discussion