GraphQL
GraphQL
Section titled “GraphQL”Railsでは、graphql-rubyを使用してGraphQL APIを実装できます。
graphql-rubyの設定
Section titled “graphql-rubyの設定”Gemの追加
Section titled “Gemの追加”# Gemfilegem 'graphql'スキーマの定義
Section titled “スキーマの定義”module Types class UserType < Types::BaseObject field :id, ID, null: false field :name, String, null: false field :email, String, null: false endend
# app/graphql/types/query_type.rbmodule Types class QueryType < Types::BaseObject field :user, Types::UserType, null: true do argument :id, ID, required: true end
def user(id:) User.find(id) end endend⚠️ Rails実装における「まさかり」ポイント
Section titled “⚠️ Rails実装における「まさかり」ポイント”提供されたコードは入門用としては正解ですが、**「実務でそのまま書くと死ぬ」**ポイントが3つあります。
① User.find(id)直撃による「N+1」の猛威
Section titled “① User.find(id)直撃による「N+1」の猛威”GraphQLは構造上、ネストしたデータを簡単に取得できるため、REST以上にN+1問題が深刻化します。
重要な理解:
User.find(id)と書くのは良いですが、もしUserTypeの中にpostsフィールドを追加し、クエリで「全ユーザーの全投稿」を取得しようとすると、ユーザーの数だけSQLが発行されます。
# ❌ 危険: N+1問題が発生module Types class UserType < Types::BaseObject field :id, ID, null: false field :name, String, null: false field :email, String, null: false field :posts, [Types::PostType], null: false endend
# クエリ: { users { id name posts { id title } } }# 100人のユーザーで101回のSQLが発行される解決策:
Railsではdataloader(GraphQL-Ruby 1.12〜)またはgoldiloaderなどのGemを使い、バッチ処理でデータをロードする設計が必須です。
# ✅ 安全: dataloaderを使用# Gemfilegem 'graphql'gem 'graphql-batch' # dataloaderの実装
# app/graphql/types/user_type.rbmodule Types class UserType < Types::BaseObject field :id, ID, null: false field :name, String, null: false field :email, String, null: false field :posts, [Types::PostType], null: false
def posts dataloader.with(Sources::ActiveRecord, Post, :user_id).load(object.id) end endend
# app/graphql/sources/active_record.rbmodule Sources class ActiveRecord < GraphQL::Dataloader::Source def initialize(model, association_name) @model = model @association_name = association_name end
def fetch(ids) @model.where(@association_name => ids).group_by(&@association_name) end endend② 型定義の冗長化(DRY原則の破壊)
Section titled “② 型定義の冗長化(DRY原則の破壊)”UserTypeでid, name, emailを定義していますが、これ、Railsのmodels/user.rbやdb/schema.rbですでに定義されていますよね?
問題点: フィールドが増えるたびに、Migration → Model → GraphQL Typeの3箇所を修正するのは苦行です。
改善案: 本番投入時は、モデルから自動でGraphQLの型を推測する仕組みや、型定義を整理する共通基盤(BaseObjectの拡張)をしっかり設計しないと、開発スピードが落ちます。
# ✅ 良い例: 共通基盤を活用module Types class BaseObject < GraphQL::Schema::Object # 共通のフィールドを定義 endend
# app/graphql/types/user_type.rbmodule Types class UserType < Types::BaseObject # モデルから自動推測(graphql-rubyの機能) # または、共通メソッドで自動生成 def self.fields_from_model(model_class) model_class.column_names.each do |column| field column.to_sym, String, null: true end end endend③ セキュリティと「クエリの深さ」制限
Section titled “③ セキュリティと「クエリの深さ」制限”GraphQLは自由度が高すぎるため、悪意のあるユーザーに「攻撃」されやすいです。
重要な理解:
user { posts { user { posts { ... } } } }のように、無限にネストしたクエリを投げられると、サーバーのリソースが瞬時に枯渇します。
解決策:
- Complexity(複雑度): クエリの重さを計算し、一定以上の重いリクエストを拒否する
- Max Depth(最大深度): ネストの深さを制限する
MySchema = GraphQL::Schema.define do query QueryType mutation MutationType
# 最大深度を制限 max_depth 10
# 複雑度を制限 max_complexity 1000end
# または、クエリごとに複雑度を設定module Types class QueryType < Types::BaseObject field :user, Types::UserType, null: true, complexity: 5 do argument :id, ID, required: true end
field :users, [Types::UserType], null: false, complexity: 10 endendより詳細な設計:APIの「入り口」を分ける
Section titled “より詳細な設計:APIの「入り口」を分ける”実務ではQueryTypeだけでなく、Mutation(更新)とSubscription(リアルタイム通知)の設計が重要になります。
module Types class MutationType < Types::BaseObject field :create_user, Types::UserType, null: false do argument :name, String, required: true argument :email, String, required: true end
def create_user(name:, email:) User.create!(name: name, email: email) end endend
# app/graphql/types/subscription_type.rbmodule Types class SubscriptionType < GraphQL::Schema::Object field :user_created, Types::UserType, null: false
def user_created # ActionCableなどでリアルタイム通知 end endend🛠️ database.ymlとの関連性
Section titled “🛠️ database.ymlとの関連性”GraphQLを使用すると、1回のリクエストで複数のモデルを叩くため、DBコネクションを保持する時間がRESTより長くなる傾向があります。
重要な理解:
database.ymlのpool数が少ないと、GraphQLの複雑なクエリを処理している間にコネクションを使い切り、他のユーザーがタイムアウトします。
設定:
pool数は余裕を持って設計してください。GraphQLを使用する場合は、通常のREST APIよりも多くのコネクションが必要になる可能性があります。
default: &default adapter: postgresql encoding: unicode # GraphQLを使用する場合は、pool数を多めに設定 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i + 5 %> checkout_timeout: 5 reaping_frequency: 10🛠️ エラーハンドリングの設計
Section titled “🛠️ エラーハンドリングの設計”GraphQLは、エラーが起きてもHTTPステータス200 OKを返し、レスポンス内のerrors配下に詳細を載せるのが標準です。
重要な理解:
React側でresponse.okだけを見ていると、エラーを見逃します。
改善案:
Rails側でGraphQL::ExecutionErrorを適切に使い分け、フロントエンドが「バリデーションエラーなのか」「認証エラーなのか」を判別できるように、専用のエラー拡張(extensions)を設計しましょう。
# ❌ 問題: エラーの種類が判別できないmodule Types class MutationType < Types::BaseObject def create_user(name:, email:) user = User.new(name: name, email: email) unless user.save raise GraphQL::ExecutionError, "Validation failed" # 問題: バリデーションエラーなのか、認証エラーなのか分からない end user end endend
# ✅ 安全: エラー拡張を設計module Types class MutationType < Types::BaseObject def create_user(name:, email:) user = User.new(name: name, email: email) unless user.save raise GraphQL::ExecutionError.new( "Validation failed", extensions: { code: "VALIDATION_ERROR", errors: user.errors.full_messages } ) end user rescue ActiveRecord::RecordNotFound => e raise GraphQL::ExecutionError.new( "Resource not found", extensions: { code: "NOT_FOUND", message: e.message } ) end endend
# React側でのエラーハンドリングconst { data, errors } = await client.mutate({ mutation: CREATE_USER });
if (errors) { errors.forEach(error => { if (error.extensions?.code === 'VALIDATION_ERROR') { // バリデーションエラーの処理 console.error(error.extensions.errors); } else if (error.extensions?.code === 'NOT_FOUND') { // リソースが見つからないエラーの処理 console.error(error.extensions.message); } });}推奨されるエラーコード:
VALIDATION_ERROR: バリデーションエラーAUTHENTICATION_ERROR: 認証エラーAUTHORIZATION_ERROR: 認可エラーNOT_FOUND: リソースが見つからないINTERNAL_ERROR: サーバー内部エラー
この設計により、フロントエンドが適切にエラーハンドリングを行えます。