対象: Rails 8.x(8.1系)/ Ruby 3.4 以降 最終更新: 2026年10月
目次
- 基本思想
- プロジェクト構成
- モデル(Active Record)
- コントローラー
- ルーティング
- ビュー / フロントエンド
- ビジネスロジックの置き場所
- データベースとマイグレーション
- パフォーマンス
- バックグラウンドジョブ
- セキュリティ
- テスト
- 設定・秘密情報の管理
- コード品質・ツール
- デプロイ・運用
- アップグレード戦略
- チェックリスト
1. 基本思想
- Convention over Configuration(設定より規約) 命名規則・ディレクトリ構成・REST などの Rails の規約に従う。規約から外れるほど、学習コストと保守コストが上がる。
- DRY(Don’t Repeat Yourself) ただし早すぎる抽象化は避ける。「3回出てきたら共通化」程度が目安。
- 「Rails Way」をまず使い切る 独自アーキテクチャや大量の gem を入れる前に、Rails 標準機能(concerns、コールバック、Active Job、Solid 系など)で解決できないか検討する。
- 小さく、読みやすく メソッドは短く、クラスは単一責務に。命名で意図が伝わるコードを書く。
2. プロジェクト構成
app/
├── controllers/
│ └── concerns/
├── models/
│ └── concerns/
├── views/
├── jobs/
├── mailers/
├── helpers/
├── javascript/ # Stimulus コントローラーなど
├── services/ # (任意) 複数モデルにまたがる処理
├── forms/ # (任意) フォームオブジェクト
├── queries/ # (任意) 複雑なクエリオブジェクト
└── policies/ # (任意) 認可ロジック
app/配下に作ったディレクトリは自動でオートロード対象になる(Zeitwerk)。ファイル名とクラス名を必ず一致させる。app/services/orders/checkout.rb→Orders::Checkout
lib/には Rails に依存しない汎用コードを置く。Rails 7.1 以降はconfig.autoload_lib(ignore: %w[assets tasks])で管理できる。- ディレクトリを増やしすぎない。必要になってから作る。
3. モデル(Active Record)
3.1 クラス内の記述順序を統一する
class User < ApplicationRecord
# 1. include / extend
include Searchable
# 2. 定数
ROLES = %w[admin member guest].freeze
# 3. enum / 属性定義
enum :status, { active: 0, suspended: 1 }, default: :active
# 4. 関連
belongs_to :organization
has_many :posts, dependent: :destroy
# 5. バリデーション
validates :email, presence: true, uniqueness: { case_sensitive: false }
# 6. コールバック
before_validation :normalize_email
# 7. スコープ
scope :recent, -> { order(created_at: :desc) }
# 8. クラスメソッド
def self.find_by_login(login) = find_by(email: login.downcase)
# 9. インスタンスメソッド
def display_name = name.presence || email
private
# 10. private メソッド
def normalize_email
self.email = email&.strip&.downcase
end
end3.2 バリデーション
- アプリ側のバリデーションと DB 制約を両方設ける。
validates :email, uniqueness: trueだけでは競合状態で重複が入りうるため、ユニークインデックスも張る。 presenceには DB 側のnull: falseを対応させる。- 正規化は
normalizes(Rails 7.1+)を使うとシンプル。
normalizes :email, with: ->(email) { email.strip.downcase }3.3 コールバックは控えめに
- コールバックは「そのモデル自身のデータ整合性」に関わる処理に限定する(正規化、デフォルト値の設定など)。
- メール送信・外部API呼び出し・他モデルの更新などの副作用はコールバックに書かない。明示的にサービスやコントローラーから呼ぶ。
- どうしても必要な場合は
after_commitを使い、トランザクション確定後に実行する。
3.4 enum
- 必ずハッシュで値を明示する(配列だと順序変更で値がずれる)。
- Rails 7 以降のキーワード引数形式
enum :status, {...}を使う。 validate: true(Rails 7.1+)で不正値を例外ではなくバリデーションエラーにできる。
3.5 スコープとクエリ
- 再利用するクエリ条件はスコープにまとめる。
- 引数が多い、複雑な条件分岐がある場合はクラスメソッドかクエリオブジェクトにする。
default_scopeは原則使わない(予期しない挙動の原因になる)。
3.6 Fat Model を避ける
- モデルが肥大化したら、Concern・値オブジェクト・サービスオブジェクトなどに分割する。
- Concern は「複数モデルで共有される振る舞い」か「明確に名前をつけられる責務の切り出し」に使う。単なるファイル分割目的では使わない。
3.7 関連付け
dependent:オプションを必ず検討する(:destroy,:delete_all,:nullify,:restrict_with_error)。belongs_toは Rails 5 以降デフォルトで必須。任意の場合のみoptional: trueを付ける。- 使われなくなった関連は
deprecated: true(Rails 8.1+)で段階的に廃止できる。
4. コントローラー
4.1 Skinny Controller
コントローラーの責務は「リクエストを受け取り、適切な処理を呼び出し、レスポンスを返す」ことだけ。ビジネスロジックはモデルやサービスへ。
class PostsController < ApplicationController
before_action :set_post, only: %i[show edit update destroy]
def index
@posts = Post.published.includes(:author).order(created_at: :desc)
end
def create
@post = Current.user.posts.build(post_params)
if @post.save
redirect_to @post, notice: "投稿を作成しました"
else
render :new, status: :unprocessable_entity
end
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.expect(post: [:title, :body, :published])
end
end4.2 ポイント
- Strong Parameters を必ず使う。Rails 8 以降は
params.expectが推奨(不正な形式のパラメータで 400 を返し、500 エラーを防げる)。 - アクションは基本の 7 つ(
index show new create edit update destroy)に限定する。それ以外が必要なら新しいコントローラーを作ることを検討する(後述)。 - バリデーション失敗時は
status: :unprocessable_entity(422)を返す。Turbo で正しく動作させるために必須。 - インスタンス変数はアクションごとに 1〜2 個程度に抑える。
- 認可チェックは
before_actionやポリシーで一元化する。 - リクエスト単位のグローバル状態(現在のユーザーなど)は
Current(ActiveSupport::CurrentAttributes)を使う。
5. ルーティング
5.1 RESTful に設計する
# Good: カスタムアクションの代わりにリソースを切り出す
resources :posts do
resource :publication, only: %i[create destroy] # POST/DELETE /posts/:post_id/publication
resources :comments, only: %i[create destroy], shallow: true
end
# Bad: 独自アクションを増やす
resources :posts do
member do
post :publish
post :unpublish
end
end- 「動詞」ではなく「名詞(リソース)」で考える。
publish→Publicationリソースのcreate。 - ネストは 1 階層までが目安。深くなる場合は
shallow: trueを使う。 only:/except:で不要なルートを生成しない。bin/rails routesで定期的にルートを確認する。
6. ビュー / フロントエンド
6.1 ビューにロジックを書かない
- 複雑な条件分岐や計算はヘルパー、モデル、プレゼンター(デコレーター)、ViewComponent などへ移す。
- ビューからのクエリ発行(N+1 の温床)を避ける。必要なデータはコントローラーで用意する。
6.2 パーシャルを活用する
<%# コレクションレンダリング(ループより高速) %>
<%= render @posts %>
<%# ローカル変数を明示的に渡す %>
<%= render "shared/card", title: @post.title %>- パーシャル内ではインスタンス変数を参照せず、ローカル変数で受け取る。
- Rails 7.1+ では strict locals(
<%# locals: (title:, body: nil) -%>)でパーシャルの引数を明示できる。
6.3 Hotwire(Turbo + Stimulus)
- Rails 標準のフロントエンドは Hotwire。SPA フレームワークを導入する前に、Turbo Drive / Frames / Streams で要件を満たせないか検討する。
- JavaScript は Stimulus コントローラーとして小さく分割する。
- アセットは Propshaft(Rails 8 のデフォルト)+ importmap が基本。ビルドが必要な場合は jsbundling-rails / cssbundling-rails。
6.4 国際化(I18n)
- 文字列はハードコードせず、
config/locales/*.ymlに定義してt(".title")で呼び出す(遅延参照)。 - モデル名・属性名も
activerecord.models/activerecord.attributesに定義する。
7. ビジネスロジックの置き場所
| 置き場所 | 向いている処理 |
|---|---|
| モデル | 単一モデルに閉じたドメインロジック、バリデーション |
| Concern | 複数モデルで共有される振る舞い |
| サービスオブジェクト | 複数モデルにまたがる処理、外部API連携 |
| フォームオブジェクト | 複数モデルを1つのフォームで扱う場合、DBに保存しないフォーム |
| クエリオブジェクト | 複雑な検索条件・集計 |
| 値オブジェクト | 金額、住所など、属性の組に振る舞いを持たせる場合 |
| ジョブ | 時間のかかる処理、非同期処理 |
サービスオブジェクトの例
# app/services/orders/checkout.rb
module Orders
class Checkout
def self.call(...) = new(...).call
def initialize(order:, payment_method:)
@order = order
@payment_method = payment_method
end
def call
ActiveRecord::Base.transaction do
@order.lock!
@order.update!(status: :paid, paid_at: Time.current)
@order.line_items.each(&:reserve_stock!)
end
OrderMailer.with(order: @order).confirmation.deliver_later
@order
end
end
end- 1 クラス 1 責務。公開メソッドは
call1 つに統一するとチームで扱いやすい。 - 名前は「動詞+目的語」など処理内容がわかるものにする。
- 「とりあえず全部サービスへ」は避ける。モデルで自然に書けるならモデルに書く。
8. データベースとマイグレーション
- マイグレーションは一度本番に適用したら編集しない。修正は新しいマイグレーションで行う。
changeメソッドでロールバック可能に書く。不可逆な操作はup/downを明示する。- 外部キーには
foreign_key: trueで外部キー制約を、検索に使うカラムにはインデックスを張る。 null: falseとdefault:を適切に設定する。- マイグレーション内でアプリのモデルクラスを使わない(将来モデルが変わると壊れる)。データ移行はデータ移行用のタスクやジョブに分離する。
- 大きなテーブルへのカラム追加・インデックス作成はロックに注意する。PostgreSQL では
algorithm: :concurrently+disable_ddl_transaction!を使う。strong_migrationsgem で危険な操作を検出できる。 db/schema.rb(またはstructure.sql)は必ずバージョン管理に含める。
class AddPublishedAtToPosts < ActiveRecord::Migration[8.1]
disable_ddl_transaction!
def change
add_column :posts, :published_at, :datetime
add_index :posts, :published_at, algorithm: :concurrently
end
end9. パフォーマンス
9.1 N+1 クエリを防ぐ
# Bad
Post.all.each { |post| puts post.author.name }
# Good
Post.includes(:author).each { |post| puts post.author.name }includes/preload/eager_loadを使い分ける。- 開発環境では
strict_loadingやbulletgem /prosopitegem で検出する。
# config/environments/development.rb
config.active_record.strict_loading_by_default = true # 必要に応じて9.2 クエリの効率化
- 必要なカラムだけ取得:
select/pluck - 存在確認は
exists?(present?やany?でレコードを全件ロードしない) - 件数は
count(SQL)とsize(ロード済みなら配列長)を使い分ける - 大量データは
find_each/in_batchesでバッチ処理 - 一括操作は
insert_all/upsert_all/update_all(ただしバリデーション・コールバックは走らない点に注意) - 集計値を頻繁に参照するなら
counter_cache
9.3 キャッシュ
- フラグメントキャッシュ・ロシアンドールキャッシュ(
cache @post do ... end、touch: true) - 低レベルキャッシュ:
Rails.cache.fetch(key, expires_in: 1.hour) { ... } - Rails 8 では Solid Cache(DB バックエンド)がデフォルトで、Redis なしでも運用できる。
9.4 計測してから最適化する
rack-mini-profiler、APM(New Relic、Datadog、Skylight など)、ログのクエリ時間を確認してボトルネックを特定する。- 本番では YJIT を有効にする(Rails 7.2 以降はデフォルトで有効化される設定)。
10. バックグラウンドジョブ
- 時間のかかる処理(メール送信、外部API、画像処理、集計)は Active Job で非同期化する。
- Rails 8 のデフォルトバックエンドは Solid Queue。Redis なしで運用でき、多くのアプリではこれで十分。
- ジョブは冪等に設計する(リトライされても安全に)。
- 引数には ActiveRecord オブジェクトそのもの(GlobalID でシリアライズされる)か ID を渡し、大きなデータは渡さない。
retry_on/discard_onでエラー時の挙動を明示する。- 長時間ジョブは Active Job Continuations(Rails 8.1+)でステップ単位で再開可能にできる。
- メールは
deliver_nowではなくdeliver_laterを使う。
class SyncInventoryJob < ApplicationJob
queue_as :default
retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 5
discard_on ActiveRecord::RecordNotFound
def perform(product)
InventoryClient.sync(product)
end
end11. セキュリティ
-
Strong Parameters でマスアサインメントを防ぐ。
-
SQL インジェクション対策: 文字列補間でクエリを組まない。
# Bad User.where("name = '#{params[:name]}'") # Good User.where(name: params[:name]) User.where("name LIKE ?", "%#{User.sanitize_sql_like(params[:q])}%") -
XSS 対策: ERB はデフォルトでエスケープされる。
html_safe/rawは極力使わない。ユーザー入力の HTML はsanitizeを通す。 -
CSRF 対策:
protect_from_forgeryを無効化しない(API 専用の場合はトークン認証で代替)。 -
認証: Rails 8 の
bin/rails generate authenticationで基本的な認証を生成できる。パスワードはhas_secure_password(bcrypt)で保存。 -
認可: リソースへのアクセスは必ず所有者・権限でスコープする。
# Bad: 他人の投稿も取れる @post = Post.find(params[:id]) # Good @post = Current.user.posts.find(params[:id]) -
レート制限: Rails 7.2+ の
rate_limitでログインなどへの総当たりを防ぐ。rate_limit to: 10, within: 3.minutes, only: :create -
config.force_ssl = true(本番)、Content Security Policy の設定。 -
機密カラムは Active Record Encryption(
encrypts :ssn)で暗号化する。 -
ログに機密情報を出さない:
config.filter_parametersに追加する。 -
Brakeman(静的解析)と bundler-audit(gem の脆弱性チェック)を CI で実行する。Rails 8 の新規アプリには Brakeman が標準で入っている。
-
Rails 本体と gem を定期的にアップデートし、セキュリティリリースは速やかに適用する。
12. テスト
- Minitest(Rails 標準)か RSpec をチームで統一する。
- テストの種類と比重
- モデル / ユニットテスト: ロジックの大部分をここでカバー
- リクエスト(Integration)テスト: コントローラーの振る舞い、認可、レスポンス
- システムテスト: 主要なユーザーフローに絞る(遅いため)
- ジョブ・メーラーのテスト
- テストデータは fixtures(Rails 標準)か FactoryBot。必要最小限のデータで書く。
- 外部 API は WebMock / VCR などでスタブする。
- 時間依存のテストは
travel_toを使う。 - 並列実行(
parallelize(workers: :number_of_processors))でテストを高速化する。 - カバレッジは SimpleCov で計測しつつ、数値より「重要なパスがテストされているか」を重視する。
- CI で必ずテストを実行し、失敗したらマージしない。Rails 8.1 ではローカル CI(
bin/ci、config/ci.rb)も用意されている。
class PostTest < ActiveSupport::TestCase
test "タイトルがないと無効" do
post = Post.new(title: "")
assert_not post.valid?
assert_includes post.errors[:title], "を入力してください"
end
end13. 設定・秘密情報の管理
-
API キーやパスワードは Rails credentials(
bin/rails credentials:edit)か環境変数で管理し、コードやリポジトリに直接書かない。- 環境ごとに
--environment productionで分ける。 config/master.keyはコミットしない。
- 環境ごとに
-
アプリ固有の設定値は
config/xやconfig_forでまとめる。# config/application.rb config.x.payment = config_for(:payment) # config/payment.yml -
環境ごとの差異は
config/environments/*.rbに閉じ込め、コード中でRails.env.production?の分岐を多用しない。
14. コード品質・ツール
| ツール | 用途 |
|---|---|
| RuboCop(rubocop-rails-omakase) | コードスタイル統一。Rails 7.2+ の新規アプリに標準搭載 |
| Brakeman | セキュリティ静的解析 |
| bundler-audit | 依存 gem の脆弱性チェック |
| bullet / prosopite | N+1 クエリ検出 |
| strong_migrations | 危険なマイグレーションの検出 |
| annotaterb | モデルにスキーマ情報をコメントとして付与 |
| debug | 標準デバッガ(binding.break / debugger) |
- コードレビューでは「Rails の規約から外れていないか」「テストがあるか」「N+1 がないか」「認可漏れがないか」を確認する。
- Gemfile の gem は必要最小限に。メンテナンスされていない gem は避ける。
15. デプロイ・運用
- Rails 8 は Kamal 2 + Thruster で Docker ベースのデプロイが標準。Heroku・Render・Fly.io などの PaaS も選択肢。
- Rails 8 の Solid Queue / Solid Cache / Solid Cable により、Redis なしのシンプルな構成から始められる。
- アプリはステートレスに保ち、ファイルは Active Storage 経由で S3 などの外部ストレージに保存する。
- ヘルスチェックエンドポイント
/up(Rails 7.1+ 標準)をロードバランサーに設定する。 - ログは構造化し(Rails 8.1 の構造化イベント報告
Rails.eventも活用できる)、外部のログ基盤に集約する。 - エラートラッキング(Sentry、Honeybadger、AppSignal など)を導入する。Rails 標準の
Rails.errorAPI を使うとサービス非依存に書ける。 - DB マイグレーションはデプロイ時に自動実行し、後方互換性のある変更(カラム追加 → コード変更 → 古いカラム削除)を段階的に行う。
- DB のバックアップと復元手順を定期的に確認する。
16. アップグレード戦略
- Rails のサポートポリシーは短い。サポート中のバージョンを使い続けることが最大のセキュリティ対策。
- マイナーバージョンごとに段階的に上げる(例: 7.1 → 7.2 → 8.0 → 8.1)。一気に飛ばさない。
- 手順:
- テストが通る状態を確認
- 非推奨(deprecation)警告をすべて解消
Gemfileの Rails バージョンを上げてbundle update railsbin/rails app:updateで設定ファイルの差分を確認new_framework_defaults_X_Y.rbの設定を一つずつ有効化config.load_defaultsを新バージョンに更新
- Ruby のバージョンも合わせて計画的に上げる。
17. チェックリスト
実装時
- Rails の規約(命名、REST、ディレクトリ構成)に従っているか
- コントローラーは薄く、ロジックは適切な場所にあるか
- Strong Parameters(
params.expect)を使っているか - 認可チェック(所有者スコープ)が漏れていないか
- N+1 クエリが発生していないか
- DB 制約(NOT NULL、ユニーク、外部キー)とインデックスがあるか
- 重い処理はジョブで非同期化しているか
- 文字列は I18n 化されているか
- テストを書いたか
リリース前
- CI(テスト、RuboCop、Brakeman、bundler-audit)がすべて通っているか
- マイグレーションは本番データ量で安全か(ロック、所要時間)
- 秘密情報がコードやログに含まれていないか
- エラー監視・ログで問題を検知できるか