API利用法
💎 なぜシリアライザーを使用するのか?
Section titled “💎 なぜシリアライザーを使用するのか?”シリアライザーは、データベースに保存されているデータと、クライアント(Webブラウザやモバイルアプリなど)に提供するJSONデータの形式を分離するために使われます。
-
データの整形とフィルタリング
- データベースのテーブルには、認証情報や内部管理用のデータなど、APIのレスポンスとして公開すべきではない情報が含まれていることがあります。シリアライザーを使うことで、必要な属性だけを選択してJSONに含めることができ、不必要なデータや機密情報の漏洩を防ぐことができます。
-
関連データの組み込み
- ブログの投稿(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が発行される endend改善案:
コントローラー側でPost.includes(:comments).allとEager Loadingを忘れずに行うか、シリアライザー側でN+1を防ぐ仕組みを意識する必要があります。
# ✅ 安全: Eager Loadingを使用class PostSerializer < ActiveModel::Serializer attributes :id, :title, :content has_many :commentsend
# コントローラー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は発行されない endend推奨されるアプローチ:
- コントローラー側でEager Loading:
includes、preload、eager_loadを使用 - シリアライザー側の意識: 関連データを使用する場合は、必ずコントローラー側で事前に読み込む
- Bullet Gem: 開発中にN+1問題を自動検知
このアプローチにより、シリアライザー内でもN+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 | シンプルで使いやすい | 中程度 | 停滞気味 |
| Jbuilder | Rails標準、テンプレートベース | 中程度 | 活発 |
| blueprinter | 柔軟で高速 | 高速 | 活発 |
| jsonapi-serializer | JSON:API仕様準拠、非常に高速 | 非常に高速 | 活発 |
推奨される選択:
- 小規模なAPI: Jbuilder(Rails標準、シンプル)
- 中規模なAPI: blueprinter(柔軟で高速)
- 大規模なAPI: jsonapi-serializer(最高のパフォーマンス)
プロジェクトの要件に応じて、適切なライブラリを選択しましょう。
-
Gem の追加
- まず、
Gemfileにactive_model_serializersを追加し、ターミナルでbundle installを実行します。
# Gemfile - まず、
gem ‘active_model_serializers’
2. **シリアライザーの生成**- `rails generate serializer` コマンドを使って、モデルに対応するシリアライザーを作成します。
```bashrails generate serializer post-
シリアライザーの定義
app/serializers/post_serializer.rbファイルを開き、レスポンスに含めたい属性を定義します。
app/serializers/post_serializer.rb class PostSerializer < ActiveModel::Serializerattributes :id, :title, :content, :created_atend
⚠️ フロントエンド(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_atend
# 結果:# {# "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の流儀に反する改善案: シリアライザーの設定で「キーをキャメルケースに変換して出力する」機能を有効にしましょう。これにより、フロントエンドエンジニアが違和感なくコードを書けるようになります。
# ✅ 良い例1: ActiveModel::SerializersでcamelCaseに変換ActiveModel::Serializer.config.key_transform = :camel_lower
class PostSerializer < ActiveModel::Serializer attributes :id, :title, :content, :created_atend
# 結果:# {# "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.jbuilderjson.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に変換
このアプローチにより、フロントエンドエンジニアが違和感なくコードを書けます。
-
コントローラーでの利用
- コントローラーで
@postsオブジェクトをJSONとしてレンダリングするだけで、定義したシリアライザーが自動的に適用されます。
class PostsController < ApplicationControllerdef index@posts = Post.allrender json: @postsendend - コントローラーで
⚠️ コンテキストに応じた「出力の切り替え」
Section titled “⚠️ コンテキストに応じた「出力の切り替え」”「一覧画面」と「詳細画面」で、返すデータの量を変えたい場合があります。
重要な理解:
全てを一つのPostSerializerで賄おうとすると、一覧画面なのに重い本文データまで全て返してしまい、パフォーマンスを損ないます。
問題点: 一覧画面では軽量なデータ(ID、タイトル、作成日など)のみが必要なのに、詳細な本文データまで返してしまうと、ネットワーク負荷が増大します。
# ❌ 問題: 一つのシリアライザーで全てを賄うclass PostSerializer < ActiveModel::Serializer attributes :id, :title, :content, :created_at, :updated_at has_many :comments belongs_to :userend
# 一覧画面でも詳細画面でも同じシリアライザーを使用# 一覧画面で本文(content)まで返すのは無駄改善案:
PostIndexSerializer(軽量版)とPostDetailSerializer(詳細版)のように、用途別にシリアライザーを分ける、あるいはscopeを使って出力を制御する手法を身につけましょう。
# ✅ 良い例1: 用途別にシリアライザーを分ける# app/serializers/post_index_serializer.rb(軽量版)class PostIndexSerializer < ActiveModel::Serializer attributes :id, :title, :created_at belongs_to :user, serializer: UserSummarySerializerend
# app/serializers/post_detail_serializer.rb(詳細版)class PostDetailSerializer < ActiveModel::Serializer attributes :id, :title, :content, :created_at, :updated_at has_many :comments belongs_to :userend
# コントローラー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 endend
# ✅ 良い例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 endend
# コントローラー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) endend推奨されるアプローチ:
- 用途別シリアライザー: 一覧用と詳細用を分ける(推奨)
- scope/view: 一つのシリアライザーで複数のビューを定義
- 条件分岐: シリアライザー内で条件分岐を使用(ただし、複雑になりやすい)
このアプローチにより、各画面に必要なデータだけを返し、パフォーマンスを最適化できます。
このように、シリアライザーはAPI開発におけるデータ管理をシンプルかつ効率的にし、セキュリティとパフォーマンスの向上に貢献します。
ただし、実務では以下の点に注意が必要です:
- N+1問題: シリアライザー内でも発生するため、コントローラー側でEager Loadingを忘れずに行う
- ライブラリの選択: プロジェクトの要件に応じて、適切なシリアライザーライブラリを選択する
- 命名規則: フロントエンド(React)に合わせてcamelCaseに変換する
- コンテキストに応じた出力: 一覧画面と詳細画面で返すデータ量を変える
これらの実務的な知識を理解することで、効率的で保守性の高いAPIを構築できます。