レッスンのマスターデータAPIをGraphQLで構築!問題点と対策
ライフイズテックのサービス開発部 学校プロダクトグループに所属している程です。
ライフイズテックが提供する様々なサービス、例えばテックレッスン、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