Skip to content

API利用法

💎 なぜシリアライザーを使用するのか?

Section titled “💎 なぜシリアライザーを使用するのか?”

シリアライザーは、データベースに保存されているデータと、クライアント(Webブラウザやモバイルアプリなど)に提供するJSONデータの形式を分離するために使われます。

  1. データの整形とフィルタリング

    • データベースのテーブルには、認証情報や内部管理用のデータなど、APIのレスポンスとして公開すべきではない情報が含まれていることがあります。シリアライザーを使うことで、必要な属性だけを選択してJSONに含めることができ、不必要なデータや機密情報の漏洩を防ぐことができます。
  2. 関連データの組み込み

    • ブログの投稿(Post)と、その投稿に紐づくコメント(Comment)のような関連するデータがある場合、シリアライザーはこれらの関連情報を一つのJSONレスポンスに含めることを可能にします。これにより、クライアントは複数のAPIリクエストを送信することなく、必要なすべてのデータを一度に取得できます。

⚠️ 「N+1問題」はシリアライザーの中でも起きる

Section titled “⚠️ 「N+1問題」はシリアライザーの中でも起きる”

「関連データの組み込み」は便利ですが、ここが最大のボトルネックになります。

重要な理解: シリアライザー内でhas_many :commentsと書くだけでは、シリアライザーが各レコードを処理するたびにSQLが発行されるN+1問題が発生します。

問題点: コントローラーでPost.allだけをフェッチして、シリアライザーに渡すと、投稿が100件あれば100回以上の追加クエリが飛びます。

# ❌ 危険: N+1問題が発生
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :content
has_many :comments # 各投稿ごとにSQLが発行される
end
# コントローラー
class PostsController < ApplicationController
def index
@posts = Post.all # SQL: SELECT * FROM posts (1回)
render json: @posts
# シリアライザーが各投稿のコメントを取得する際に
# SQL: SELECT * FROM comments WHERE post_id = ? (100回)
# 合計: 101回のSQLが発行される
end
end

改善案: コントローラー側でPost.includes(:comments).allとEager Loadingを忘れずに行うか、シリアライザー側でN+1を防ぐ仕組みを意識する必要があります。

# ✅ 安全: Eager Loadingを使用
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :content
has_many :comments
end
# コントローラー
class PostsController < ApplicationController
def index
@posts = Post.includes(:comments).all # SQL: 2回のみ
# 1. SELECT * FROM posts
# 2. SELECT * FROM comments WHERE post_id IN (1, 2, 3, ..., 100)
render json: @posts
# シリアライザーは既に読み込まれたデータを使用するため、追加のSQLは発行されない
end
end

推奨されるアプローチ:

  • コントローラー側でEager Loading: includespreloadeager_loadを使用
  • シリアライザー側の意識: 関連データを使用する場合は、必ずコントローラー側で事前に読み込む
  • Bullet Gem: 開発中にN+1問題を自動検知

このアプローチにより、シリアライザー内でもN+1問題を防げます。

  1. パフォーマンスの向上
    • 余分なデータを含まない、軽量なJSONレスポンスを提供することで、ネットワークの負荷を減らし、APIのレスポンス速度を向上させることができます。

💡 Active Model Serializers の利用方法

Section titled “💡 Active Model Serializers の利用方法”

Active Model Serializers を利用したJSONレスポンス形式の定義方法について、おさらいします。

⚠️ active_model_serializersの現状と選択肢

Section titled “⚠️ active_model_serializersの現状と選択肢”

AMSは長らく愛されてきましたが、実は現在、メンテナンスやパフォーマンスの観点から他の選択肢が選ばれることも増えています。

重要な理解: AMSは開発が停滞気味で、大規模なJSON生成では処理が遅いという課題があります。

問題点: 高速さを求める現場ではNetflix/fast_jsonapi(現在はコミュニティ版のblueprinterやjsonapi-serializer)が好まれます。

改善案: もしこれから新規で大規模なAPIを作るなら、AMS以外のモダンで高速なライブラリがあることも知っておくと、設計の引き出しが増えます。

ライブラリ特徴パフォーマンスメンテナンス状況
ActiveModel::Serializersシンプルで使いやすい中程度停滞気味
JbuilderRails標準、テンプレートベース中程度活発
blueprinter柔軟で高速高速活発
jsonapi-serializerJSON:API仕様準拠、非常に高速非常に高速活発

推奨される選択:

  • 小規模なAPI: Jbuilder(Rails標準、シンプル)
  • 中規模なAPI: blueprinter(柔軟で高速)
  • 大規模なAPI: jsonapi-serializer(最高のパフォーマンス)

プロジェクトの要件に応じて、適切なライブラリを選択しましょう。

  1. Gem の追加

    • まず、Gemfileactive_model_serializers を追加し、ターミナルで bundle install を実行します。
    # Gemfile

gem ‘active_model_serializers’

2. **シリアライザーの生成**
- `rails generate serializer` コマンドを使って、モデルに対応するシリアライザーを作成します。
```bash
rails generate serializer post
  1. シリアライザーの定義

    • app/serializers/post_serializer.rb ファイルを開き、レスポンスに含めたい属性を定義します。
    app/serializers/post_serializer.rb
    class PostSerializer < ActiveModel::Serializer
    attributes :id, :title, :content, :created_at
    end

⚠️ フロントエンド(React)に合わせた「命名規則」

Section titled “⚠️ フロントエンド(React)に合わせた「命名規則」”

Railsはsnake_caseですが、JavaScript(React)はcamelCaseが標準です。

重要な理解: @post.created_atをそのまま返すと、React側でpost.created_atと書くことになり、JSの流儀に反します。

問題点: フロントエンドエンジニアが違和感を感じ、コードの可読性が下がります。

# ❌ 問題: snake_caseのまま返される
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :content, :created_at
end
# 結果:
# {
# "id": 1,
# "title": "Hello",
# "content": "...",
# "created_at": "2024-01-01T00:00:00.000Z" # snake_case
# }
# React側で使用する際
const post = response.data;
console.log(post.created_at); // JSの流儀に反する

改善案: シリアライザーの設定で「キーをキャメルケースに変換して出力する」機能を有効にしましょう。これにより、フロントエンドエンジニアが違和感なくコードを書けるようになります。

config/initializers/active_model_serializers.rb
# ✅ 良い例1: ActiveModel::SerializersでcamelCaseに変換
ActiveModel::Serializer.config.key_transform = :camel_lower
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :content, :created_at
end
# 結果:
# {
# "id": 1,
# "title": "Hello",
# "content": "...",
# "createdAt": "2024-01-01T00:00:00.000Z" # camelCase
# }
# ✅ 良い例2: blueprinterでcamelCaseに変換
class PostBlueprint < Blueprinter::Base
identifier :id
fields :title, :content
field :created_at, name: :createdAt # 明示的にcamelCaseに変換
end
# ✅ 良い例3: JbuilderでcamelCaseに変換
# app/views/api/v1/posts/index.json.jbuilder
json.posts @posts do |post|
json.id post.id
json.title post.title
json.content post.content
json.createdAt post.created_at # camelCaseに変換
end

推奨されるアプローチ:

  • ActiveModel::Serializers: key_transform = :camel_lowerを設定
  • blueprinter: nameオプションで明示的に変換
  • Jbuilder: 手動でcamelCaseに変換

このアプローチにより、フロントエンドエンジニアが違和感なくコードを書けます。

  1. コントローラーでの利用

    • コントローラーで @posts オブジェクトをJSONとしてレンダリングするだけで、定義したシリアライザーが自動的に適用されます。
    class PostsController < ApplicationController
    def index
    @posts = Post.all
    render json: @posts
    end
    end

⚠️ コンテキストに応じた「出力の切り替え」

Section titled “⚠️ コンテキストに応じた「出力の切り替え」”

「一覧画面」と「詳細画面」で、返すデータの量を変えたい場合があります。

重要な理解: 全てを一つのPostSerializerで賄おうとすると、一覧画面なのに重い本文データまで全て返してしまい、パフォーマンスを損ないます。

問題点: 一覧画面では軽量なデータ(ID、タイトル、作成日など)のみが必要なのに、詳細な本文データまで返してしまうと、ネットワーク負荷が増大します。

# ❌ 問題: 一つのシリアライザーで全てを賄う
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :content, :created_at, :updated_at
has_many :comments
belongs_to :user
end
# 一覧画面でも詳細画面でも同じシリアライザーを使用
# 一覧画面で本文(content)まで返すのは無駄

改善案: PostIndexSerializer(軽量版)とPostDetailSerializer(詳細版)のように、用途別にシリアライザーを分ける、あるいはscopeを使って出力を制御する手法を身につけましょう。

# ✅ 良い例1: 用途別にシリアライザーを分ける
# app/serializers/post_index_serializer.rb(軽量版)
class PostIndexSerializer < ActiveModel::Serializer
attributes :id, :title, :created_at
belongs_to :user, serializer: UserSummarySerializer
end
# app/serializers/post_detail_serializer.rb(詳細版)
class PostDetailSerializer < ActiveModel::Serializer
attributes :id, :title, :content, :created_at, :updated_at
has_many :comments
belongs_to :user
end
# コントローラー
class PostsController < ApplicationController
def index
@posts = Post.includes(:user).all
render json: @posts, each_serializer: PostIndexSerializer
end
def show
@post = Post.includes(:user, :comments).find(params[:id])
render json: @post, serializer: PostDetailSerializer
end
end
# ✅ 良い例2: scopeを使って出力を制御(blueprinter)
class PostBlueprint < Blueprinter::Base
identifier :id
fields :title
view :index do
fields :created_at
association :user, blueprint: UserSummaryBlueprint
end
view :detail do
fields :content, :created_at, :updated_at
association :user, blueprint: UserBlueprint
association :comments, blueprint: CommentBlueprint
end
end
# コントローラー
class PostsController < ApplicationController
def index
@posts = Post.includes(:user).all
render json: PostBlueprint.render(@posts, view: :index)
end
def show
@post = Post.includes(:user, :comments).find(params[:id])
render json: PostBlueprint.render(@post, view: :detail)
end
end

推奨されるアプローチ:

  • 用途別シリアライザー: 一覧用と詳細用を分ける(推奨)
  • scope/view: 一つのシリアライザーで複数のビューを定義
  • 条件分岐: シリアライザー内で条件分岐を使用(ただし、複雑になりやすい)

このアプローチにより、各画面に必要なデータだけを返し、パフォーマンスを最適化できます。

このように、シリアライザーはAPI開発におけるデータ管理をシンプルかつ効率的にし、セキュリティとパフォーマンスの向上に貢献します。

ただし、実務では以下の点に注意が必要です:

  • N+1問題: シリアライザー内でも発生するため、コントローラー側でEager Loadingを忘れずに行う
  • ライブラリの選択: プロジェクトの要件に応じて、適切なシリアライザーライブラリを選択する
  • 命名規則: フロントエンド(React)に合わせてcamelCaseに変換する
  • コンテキストに応じた出力: 一覧画面と詳細画面で返すデータ量を変える

これらの実務的な知識を理解することで、効率的で保守性の高いAPIを構築できます。