GraphQL と gRPC — RESTの先へ
RESTの限界
「このAPIを使うと、必要なデータを取るのに5回リクエストが必要です」
パートナー企業D社は、モバイルアプリを作っていた。ユーザー情報、記事一覧、コメント数、タグ、ブックマーク状態を取るために5つのAPIを叩く。モバイル回線では深刻な遅延だ。
「GraphQLを検討してほしい」
サクラはGraphQLの学習を始めた。同時に、マイクロサービス間通信に向いているgRPCも調査することにした。
REST vs GraphQL vs gRPC
Loading diagram...
| 項目 | REST | GraphQL | gRPC |
|---|---|---|---|
| プロトコル | HTTP/1.1 | HTTP/1.1 | HTTP/2 |
| データ形式 | JSON | JSON | Protocol Buffers |
| 型定義 | OpenAPI(任意) | スキーマ必須 | .proto必須 |
| オーバーフェッチ | 発生する | 発生しない | 発生しない |
| キャッシュ | HTTP標準 | 複雑 | 難しい |
| ストリーミング | 限定的 | Subscription | ネイティブ |
| 学習コスト | 低 | 中 | 高 |
GraphQL: 概念
GraphQLでは、クライアントが「欲しいものだけ」を宣言的に指定する。
# クライアントが指定するクエリ
query {
user(id: 1) {
name
email
articles(limit: 5) {
title
publishedAt
commentCount
}
}
}// 1回のリクエストで必要なデータだけが返る
{
"data": {
"user": {
"name": "田中太郎",
"email": "tanaka@example.com",
"articles": [
{
"title": "Railsの基本",
"publishedAt": "2024-01-15",
"commentCount": 12
}
]
}
}
}graphql-rubyの実装
# Gemfile
gem 'graphql'
gem 'graphql-batch' # N+1対策
# スキーマ生成
rails generate graphql:install型定義
# app/graphql/types/user_type.rb
module Types
class UserType < Types::BaseObject
description "ユーザー"
field :id, ID, null: false
field :name, String, null: false
field :email, String, null: false
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :articles, [Types::ArticleType], null: false,
description: "ユーザーの記事一覧"
def articles
# N+1対策: DataLoaderを使う
dataloader.with(Sources::ArticleSource).load(object.id)
end
end
end
# app/graphql/types/article_type.rb
module Types
class ArticleType < Types::BaseObject
field :id, ID, null: false
field :title, String, null: false
field :body, String, null: true
field :published_at, GraphQL::Types::ISO8601DateTime, null: true
field :comment_count, Integer, null: false
def comment_count
object.comments.count
end
end
endクエリタイプ
# app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
# ユーザー一覧
field :users, [Types::UserType], null: false do
description "ユーザー一覧"
argument :status, String, required: false
argument :page, Integer, required: false, default_value: 1
argument :per_page, Integer, required: false, default_value: 20
end
def users(status: nil, page:, per_page:)
scope = User.all
scope = scope.where(status: status) if status
scope.page(page).per(per_page)
end
# 単一ユーザー
field :user, Types::UserType, null: true do
description "ユーザー詳細"
argument :id, ID, required: true
end
def user(id:)
User.find_by(id: id)
end
end
endミューテーション
# app/graphql/mutations/create_user.rb
module Mutations
class CreateUser < BaseMutation
description "ユーザーを作成する"
argument :name, String, required: true
argument :email, String, required: true
field :user, Types::UserType, null: true
field :errors, [Types::ErrorType], null: false
def resolve(name:, email:)
user = User.new(name: name, email: email)
if user.save
{ user: user, errors: [] }
else
{
user: nil,
errors: user.errors.map { |e|
{ field: e.attribute.to_s, message: e.full_message }
}
}
end
end
end
end
# app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
field :create_user, mutation: Mutations::CreateUser
field :update_user, mutation: Mutations::UpdateUser
field :delete_user, mutation: Mutations::DeleteUser
end
endサブスクリプション(リアルタイム)
# app/graphql/types/subscription_type.rb
module Types
class SubscriptionType < Types::BaseObject
field :article_created, Types::ArticleType, null: false do
description "新しい記事が作成されたときに通知"
argument :user_id, ID, required: false
end
def article_created(user_id: nil)
object # イベント発火時に渡されるオブジェクト
end
end
end
# 記事作成時にイベントを発火
class ArticlesController < ApplicationController
def create
@article = Article.create!(article_params)
# Subscriptionにイベントを伝搬
TechbridgeApiSchema.subscriptions.trigger(
:article_created,
{ user_id: @article.user_id },
@article
)
end
endDataLoaderでN+1を解決
# app/graphql/sources/article_source.rb
class Sources::ArticleSource < GraphQL::Dataloader::Source
def fetch(user_ids)
articles = Article.where(user_id: user_ids).group_by(&:user_id)
user_ids.map { |id| articles[id] || [] }
end
endコントローラー
# app/controllers/api/graphql_controller.rb
class Api::GraphqlController < ApplicationController
skip_before_action :verify_authenticity_token
def execute
variables = prepare_variables(params[:variables])
query = params[:query]
operation_name = params[:operationName]
result = TechbridgeApiSchema.execute(
query,
variables: variables,
context: {
current_partner: @current_partner,
current_user: @current_user
},
operation_name: operation_name
)
render json: result
rescue StandardError => e
raise e unless Rails.env.development?
render json: { errors: [{ message: e.message }] }, status: :internal_server_error
end
private
def prepare_variables(variables_param)
case variables_param
when String
variables_param.present? ? JSON.parse(variables_param) : {}
when Hash
variables_param
else
{}
end
end
endWARNING
GraphQLは強力ですが、複雑なクエリで意図せずDBに大量の負荷をかけることがあります。クエリの深さ制限とコスト計算を必ず設定してください。
gRPC: マイクロサービス間通信
gRPCはRailsのパブリックAPIよりも、マイクロサービス間の通信に適している。
Protocol Buffers定義
// proto/user_service.proto
syntax = "proto3";
package techbridge.v1;
service UserService {
rpc GetUser(GetUserRequest) returns (UserResponse);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser(CreateUserRequest) returns (UserResponse);
rpc StreamUsers(ListUsersRequest) returns (stream UserResponse);
}
message GetUserRequest {
int64 id = 1;
}
message ListUsersRequest {
int32 page = 1;
int32 per_page = 2;
string status = 3;
}
message CreateUserRequest {
string name = 1;
string email = 2;
}
message UserResponse {
int64 id = 1;
string name = 2;
string email = 3;
string created_at = 4;
}
message ListUsersResponse {
repeated UserResponse users = 1;
int32 total = 2;
}gRPCサーバー実装(Ruby)
# Gemfile
gem 'grpc'
gem 'grpc-tools'
# lib/grpc/user_service_impl.rb
class UserServiceImpl < Techbridge::V1::UserService::Service
def get_user(request, _call)
user = User.find(request.id)
to_user_response(user)
rescue ActiveRecord::RecordNotFound
raise GRPC::NotFound, "User #{request.id} not found"
end
def list_users(request, _call)
users = User.page(request.page).per(request.per_page)
users = users.where(status: request.status) if request.status.present?
Techbridge::V1::ListUsersResponse.new(
users: users.map { |u| to_user_response(u) },
total: users.total_count
)
end
def stream_users(request, _call)
return enum_for(:stream_users, request, _call) unless block_given?
User.find_each(batch_size: 100) do |user|
yield to_user_response(user)
end
end
private
def to_user_response(user)
Techbridge::V1::UserResponse.new(
id: user.id,
name: user.name,
email: user.email,
created_at: user.created_at.iso8601
)
end
endどれを選ぶか
Loading diagram...
| 選ぶ基準 | REST | GraphQL | gRPC |
|---|---|---|---|
| 外部公開 | ✓ | △ | ✗ |
| 画面ごとに違うデータ | △ | ✓ | △ |
| 超高速・低レイテンシ | △ | △ | ✓ |
| ストリーミング | △ | ✓ | ✓ |
| ブラウザから直接 | ✓ | ✓ | ✗ |
| キャッシュ | ✓ | △ | ✗ |
INFO
「全部GraphQLに移行すべき」は誤りです。RESTとGraphQLとgRPCは競合ではなく、それぞれが得意な領域があります。テックブリッジでは、パブリックAPIはREST、モバイル向けBFFにGraphQL、マイクロサービス間はgRPCという3層構造が理想です。
サクラの判断
「モバイルアプリ向けのBFF(Backend for Frontend)としてGraphQLを追加しましょう。既存のREST APIは廃止せず、外部パートナー向けに維持する」
CTOは頷いた。「なぜ両方を維持するのか説明できる人間が設計できる。それがサクラらしい」
技術の選択は「何が最新か」ではなく「何が問題を解決するか」で決まる。
次章では、APIエコシステムを育て続ける戦略を学ぶ。