Skip to content

GraphQL

Railsでは、graphql-rubyを使用してGraphQL APIを実装できます。

# Gemfile
gem 'graphql'
app/graphql/types/user_type.rb
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
field :email, String, null: false
end
end
# app/graphql/types/query_type.rb
module 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
end
end

⚠️ 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
end
end
# クエリ: { users { id name posts { id title } } }
# 100人のユーザーで101回のSQLが発行される

解決策: Railsではdataloader(GraphQL-Ruby 1.12〜)またはgoldiloaderなどのGemを使い、バッチ処理でデータをロードする設計が必須です。

# ✅ 安全: dataloaderを使用
# Gemfile
gem 'graphql'
gem 'graphql-batch' # dataloaderの実装
# app/graphql/types/user_type.rb
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
def posts
dataloader.with(Sources::ActiveRecord, Post, :user_id).load(object.id)
end
end
end
# app/graphql/sources/active_record.rb
module 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
end
end
② 型定義の冗長化(DRY原則の破壊)
Section titled “② 型定義の冗長化(DRY原則の破壊)”

UserTypeid, name, emailを定義していますが、これ、Railsのmodels/user.rbdb/schema.rbですでに定義されていますよね?

問題点: フィールドが増えるたびに、Migration → Model → GraphQL Typeの3箇所を修正するのは苦行です。

改善案: 本番投入時は、モデルから自動でGraphQLの型を推測する仕組みや、型定義を整理する共通基盤(BaseObjectの拡張)をしっかり設計しないと、開発スピードが落ちます。

app/graphql/types/base_object.rb
# ✅ 良い例: 共通基盤を活用
module Types
class BaseObject < GraphQL::Schema::Object
# 共通のフィールドを定義
end
end
# app/graphql/types/user_type.rb
module 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
end
end
③ セキュリティと「クエリの深さ」制限
Section titled “③ セキュリティと「クエリの深さ」制限”

GraphQLは自由度が高すぎるため、悪意のあるユーザーに「攻撃」されやすいです。

重要な理解: user { posts { user { posts { ... } } } }のように、無限にネストしたクエリを投げられると、サーバーのリソースが瞬時に枯渇します。

解決策:

  • Complexity(複雑度): クエリの重さを計算し、一定以上の重いリクエストを拒否する
  • Max Depth(最大深度): ネストの深さを制限する
config/initializers/graphql.rb
MySchema = GraphQL::Schema.define do
query QueryType
mutation MutationType
# 最大深度を制限
max_depth 10
# 複雑度を制限
max_complexity 1000
end
# または、クエリごとに複雑度を設定
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
end
end
より詳細な設計:APIの「入り口」を分ける
Section titled “より詳細な設計:APIの「入り口」を分ける”

実務ではQueryTypeだけでなく、Mutation(更新)とSubscription(リアルタイム通知)の設計が重要になります。

app/graphql/types/mutation_type.rb
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
end
end
# app/graphql/types/subscription_type.rb
module Types
class SubscriptionType < GraphQL::Schema::Object
field :user_created, Types::UserType, null: false
def user_created
# ActionCableなどでリアルタイム通知
end
end
end

GraphQLを使用すると、1回のリクエストで複数のモデルを叩くため、DBコネクションを保持する時間がRESTより長くなる傾向があります。

重要な理解: database.ymlpool数が少ないと、GraphQLの複雑なクエリを処理している間にコネクションを使い切り、他のユーザーがタイムアウトします。

設定: pool数は余裕を持って設計してください。GraphQLを使用する場合は、通常のREST APIよりも多くのコネクションが必要になる可能性があります。

config/database.yml
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
end
end
# ✅ 安全: エラー拡張を設計
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
end
end
# 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: サーバー内部エラー

この設計により、フロントエンドが適切にエラーハンドリングを行えます。