Ruby on Rails architecture patterns and conventions based on official Rails guides, the Rails Doctrine, and core team recommendations. Covers Active Record, controllers, routing, concerns, caching, background jobs, and Rails 8+ defaults.
From eccnpx claudepluginhub tatematsu-k/ai-development-skills --plugin eccThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Based on: Rails Guides, Rails Doctrine, DHH, and Rails core team members.
| Ruby | Database |
|---|---|
Model: BookClub (singular CamelCase) | Table: book_clubs (plural snake_case) |
Foreign key: author_id | Column: author_id |
Primary key: id (bigint) | Auto-created |
Join table: authors_books | Alphabetical order |
Reserved columns: type (STI), lock_version (optimistic locking), *_count (counter cache), created_at/updated_at (timestamps)
常に change メソッドを使用(auto-reversible):
class CreateProducts < ActiveRecord::Migration[8.1]
def change
create_table :products do |t|
t.string :name, null: false
t.references :author, foreign_key: true
t.timestamps
end
add_index :products, :part_number, unique: true
end
end
Rules:
foreign_key: true を追加するvalidates :name, presence: true
validates :email, uniqueness: { case_sensitive: false }
validates :bio, length: { maximum: 500 }
validates :age, numericality: { only_integer: true }
validates :size, inclusion: { in: %w[S M L] }
validates :end_date, comparison: { greater_than: :start_date }
Critical: モデルの uniqueness バリデーションだけでは不十分。必ずDBレベルの一意インデックスも追加する(レースコンディション対策)。
| 関係 | 宣言 | FK の場所 |
|---|---|---|
| 1対多 | has_many / belongs_to | 子テーブル |
| 1対1 | has_one / belongs_to | 子テーブル |
| 多対多(JoinModel付き) | has_many :through | Joinテーブル |
| 多対多(シンプル) | has_and_belongs_to_many | Joinテーブル |
| ポリモーフィック | belongs_to :imageable, polymorphic: true | 子テーブル |
Rules:
has_many :through を使う(has_and_belongs_to_many ではなく)dependent: を指定する(孤立レコード防止)belongs_to は常に単数、has_many は常に複数# BAD — N+1
books = Book.limit(10)
books.each { |b| b.author.name } # 11 queries
# GOOD — eager loading
books = Book.includes(:author).limit(10) # 2 queries
# GOOD — strict loading (Rails 6.1+, Eileen Uchitelle)
user = User.strict_loading.first
user.address # raises StrictLoadingViolationError
推奨: config.active_record.strict_loading_by_default = true を開発環境で有効にする
大量データの処理:
Customer.find_each(batch_size: 5000) { |c| process(c) } # NOT Model.all.each
効率的なデータ取得:
User.pluck(:email) # Array を返す(AR オブジェクトなし)
User.where(...).exists? # boolean のみ
「非CRUDアクションをコントローラに追加しない — 新しいコントローラを作れ」
# BAD — カスタムアクション
class InboxesController < ApplicationController
def pendings; end
def archive; end
end
# GOOD — 新しいコントローラ (DHH way)
class Inboxes::PendingsController < ApplicationController
def index; end
end
class Inboxes::ArchivesController < ApplicationController
def create; end
end
すべてのコントローラは標準CRUD(index, show, new, edit, create, update, destroy)のみ。
# Rails 8 推奨: expect(構造が一致しなければ 400 を返す)
def person_params
params.expect(person: [:name, :age])
end
ビジネスロジックはモデル(+ Concerns)に配置。コントローラはリクエスト処理に専念。
DHH: 「Concerns で太ったモデルをダイエットさせる」
# app/models/concerns/taggable.rb
module Taggable
extend ActiveSupport::Concern
included do
has_many :taggings, as: :taggable, dependent: :destroy
has_many :tags, through: :taggings
end
def tag_names
tags.pluck(:name)
end
end
# app/models/post.rb
class Post < ApplicationRecord
include Taggable, Searchable, Visible
end
DHH は Service Objects に反対: 「J2EE ルネサンスフェア」と呼び、Anemic Domain Model アンチパターンだと批判。代わりに Concerns、モデル、多数の小さな REST コントローラを推奨。
resources :photos # 7 RESTful routes
resources :photos, only: [:index, :show] # 限定
resource :profile # singular (no index)
resources :articles do
resources :comments, shallow: true
end
# Collection: /articles/:article_id/comments
# Member: /comments/:id
Rule: ネストは1レベルまで。それ以上は shallow nesting を使う。
concern :commentable do
resources :comments
end
resources :articles, concerns: :commentable
resources :photos, concerns: :commentable
ライフサイクル順序 (create): before_validation → after_validation → before_save → before_create → after_create → after_save → after_commit
Rules:
private にするafter_commit を使う(after_save ではない)throw(:abort) でチェーンを停止update/save を呼ばない(無限ループの原因)Rails 8+ per-transaction callbacks:
Article.transaction do |txn|
article.update(published: true)
txn.after_commit { PublishMailer.with(article: article).deliver_later }
end
ファイル名は定数名と一致させる:
users_helper.rb → UsersHelperhtml_parser.rb → HtmlParser(アクロニム設定が必要なら inflect.acronym "HTML")Rules:
require を使わない(autoloading と reloading が壊れる)config.to_prepare を使う)bin/rails zeitwerk:check で命名規約を検証する| 従来 | Rails 8 Default |
|---|---|
| Sidekiq + Redis | Solid Queue (DB-backed) |
| Redis / Memcached | Solid Cache (DB-backed) |
| Redis (Action Cable) | Solid Cable (DB-backed) |
37signals で日次 2000万ジョブを処理した実績あり。
# Kamal 2: SSH経由のDockerデプロイ、Kubernetes不要
# Kamal Proxy: ゼロダウンタイム、自動SSL (Let's Encrypt)
# Thruster: Nginx置き換え、X-Sendfile、アセットキャッシュ、gzip圧縮
bin/rails generate authentication
# User, Session モデル、メーラー、コントローラを生成
# has_secure_password、ログインスロットリング、パスワードリセット含む
# 基本的な認証には Devise 不要
DHH: 「HTML Over The Wire」— SPA不要
| コンポーネント | 役割 |
|---|---|
| Turbo Drive | ページ遷移をAjax化 |
| Turbo Frames | ページの部分更新 |
| Turbo Streams | WebSocket経由のリアルタイム更新 |
| Stimulus | 最小限のJavaScript振る舞い |
byroot の警告: 「Rails アプリは IO-bound」という思い込みは危険。YJIT の効果が示すように、多くのアプリは想定以上に CPU-bound。
<% cache product do %>touch: trueRails.cache.fetch(key, expires_in: 12.hours) { expensive_call }stale?(@product) で HTTP 304Anti-pattern:
# BAD: AR オブジェクトをキャッシュ
Rails.cache.fetch("users") { User.all.to_a }
# GOOD: IDをキャッシュし再クエリ
ids = Rails.cache.fetch("user_ids") { User.pluck(:id) }
User.where(id: ids)
<%= csrf_meta_tags %> を含める# BAD
Model.where("name = #{params[:name]}")
# GOOD
Model.where("name = ?", params[:name])
Model.where(name: params[:name])
# 常に current_user でスコープ
@project = @current_user.projects.find(params[:id])
rate_limit to: 10, within: 3.minutes, only: :create
reset_session を呼ぶ(セッション固定攻撃防止)user_id のみ保存(複雑なオブジェクトは不可)\A と \z(文字列境界)を使う。^ と $(行境界)は使わないrails new my_api --api
ActionController::API を継承(軽量なミドルウェアスタック)stale? で Conditional GET を活用rack-cors gem で設定| Anti-Pattern | Rails Way |
|---|---|
| Service Objects | Concerns + Models |
| マイクロサービス | Majestic Monolith |
| React/Vue SPA | Hotwire (Turbo + Stimulus) |
| Webpack/Vite | Import Maps + Propshaft |
| Sidekiq + Redis | Solid Queue |
| Devise (基本認証) | bin/rails generate authentication |
| 深いルートネスト | Shallow nesting (1レベルまで) |
| Fat Controller | Many thin REST-only controllers |
| DDD / Clean Architecture | Active Record + Concerns |
| GC チューニング(計測なし) | Vernier でプロファイリング後に判断 |
Sources: