第12章|今さら学ぶ「モデル詳解」
📚 シリーズ目次はこちら → 「今さら学ぶ」シリーズ — はじめに
🗺️ KnowledgeNoteの設計を確認 → 設計マップ
この章でわかること
- MVCにおけるモデルの本当の役割 — 「テーブルの窓口」ではない
- モデルの責務の範囲 — 何を書くべきで何を書くべきでないか
- バリデーション — データの品質を守る検問所
- ActiveRecord と ActiveModel — テーブルがあるモデルとないモデル
- Dirtyトラッキング — 「変更されたかどうか」を検知する仕組み
🏠 たとえ話で掴む「モデル」
第9章でモデルを「倉庫の管理者」にたとえました。今回はもう少し具体的に、 会社の人事部 として考えます。
人事部の仕事は、社員のデータ管理だけではありません。「採用ルールを守っているか確認する(バリデーション)」「雇用形態に応じた手続きを自動で行う(コールバック)」「関係会社や部署との関係を管理する(アソシエーション)」——こうしたビジネスのルールをまとめて担っています。
| 人事部の仕事 | モデルの仕事 |
|---|---|
| 社員データの保管・取り出し | DB へのデータの読み書き |
| 採用要件を満たしているか確認 | バリデーション |
| 入社・退社時の手続きを自動実行 | コールバック |
| 部署や関係会社との関係管理 | アソシエーション |
| 「営業部の全員を取得する」などの検索 | スコープ・クエリ |
大事なのは、人事部は 採用の最終判断や経営戦略には関与しない ということ。あくまで定まったルールの範囲内で動き、複雑な判断が必要なときは別の部署(サービスクラス等)に委ねます。
モデルとは何か — 技術的な定義
MVCにおけるモデルの役割
モデル(Model) は、MVCアーキテクチャの中で データと業務ルールを管理する層 です。
コントローラ(第14章)がリクエストの交通整理をし、ビュー(第13章)が画面の描画を担当するのに対して、モデルはアプリの核心部分——「データをどう保存するか」「データにどんな制約があるか」「データ同士がどう関係するか」——を一手に担います。
Railsのモデルは ApplicationRecord を継承したクラスで、 Active Record パターン という設計に基づいています。Active Recordとは、テーブルの1行を1つのオブジェクトに対応させ、そのオブジェクト自身がDBへの読み書きメソッドを持つという仕組みです(→ 第15章で詳しく扱います)。
「モデル ≠ テーブル」— よくある誤解
スクールで学んだ段階では「モデル = テーブルと対応するファイル」という理解になりがちです。確かに多くの場合そうなのですが、正確には モデルは業務のルールを持つクラス であって、テーブルを持つかどうかは本質ではありません。
# ① テーブルと紐づくモデル(一般的なActiveRecordモデル)
class Article < ApplicationRecord
validates :title, presence: true
belongs_to :user
end
# ② テーブルを持たないモデル(ActiveModel::Model)
class ContactForm
include ActiveModel::Model
attr_accessor :name, :email, :message
validates :name, presence: true
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :message, presence: true
end
② のように、DBにテーブルがなくてもバリデーションやフォームヘルパーとの連携が必要な場面でモデルを作ることがあります。
モデルが担う責務の範囲
モデルが担うべき責務は、次のように整理できます。
| 責務 | 具体例 | 解説する章 |
|---|---|---|
| テーブル定義 | マイグレーション(db/migrate/) |
第15章 |
| データの永続化 |
save, create, update, destroy
|
第15章 |
| バリデーション |
presence: true, カスタムバリデーション |
この章 |
| アソシエーション |
has_many, belongs_to
|
第15章・第18章 |
| スコープ・クエリ |
scope :published, .where
|
第16章 |
| コールバック |
before_save, after_create
|
第16章 |
| ビジネスロジック(単純なもの) | 公開可否の判定、文字列の変換 | この章・第25章 |
💡 マイグレーションはモデルファイルの外にある
テーブルの構造(どのカラムを持つか)はdb/migrate/配下のマイグレーションファイルで定義します。app/models/article.rbにカラムの一覧は書きません。Railsがテーブル定義を読み取って、自動的にモデルと対応付けてくれます(→ 第15章で詳しく扱います)。
一方、 モデルに書くべきでないもの もあります。
# ❌ ビューの表示ロジックはモデルに書かない
def formatted_title_with_html
"<h1 class='text-2xl'>#{title}</h1>".html_safe
end
# ❌ メール送信など副作用の大きい処理はモデルに書かない
def save_and_send_welcome_email
save
UserMailer.welcome(self).deliver_later
end
# ✅ データに関する純粋なロジックはモデルに書いてよい
def published?
published == true && published_at <= Time.current
end
def summary
body.truncate(100)
end
モデルに複雑なロジックが増えてきたら「Fat Model(太ったモデル)」の兆候です(→ 第25章で詳しく扱います)。
✈️ バリデーション — データの品質を守る
たとえ話で掴む「バリデーション」
バリデーションを一言でいうと、 空港の手荷物検査 です。
飛行機に乗る前、全員が手荷物検査を受けます。刃物や危険物がないか、サイズ制限を超えていないかをチェックされる。検査に通らなければ搭乗できません。
Webアプリも同じです。ユーザーがフォームから送ったデータをDBに保存する前に、 「このデータは大丈夫か?」とチェック するのがバリデーションです。
| 空港の検査 | バリデーション |
|---|---|
| 「パスポートを持っていますか?」 |
presence: true(必須チェック) |
| 「同じパスポート番号の人がいませんか?」 |
uniqueness: true(一意チェック) |
| 「荷物のサイズは規定内ですか?」 |
length: { maximum: 100 }(文字数チェック) |
| 「メールアドレスの形式は正しいですか?」 |
format: { with: /正規表現/ }(形式チェック) |
| 独自の持ち込み禁止リスト | カスタムバリデーション |
なぜバリデーションが必要なのか
データベースに不正なデータが入ると、アプリ全体に影響が波及します。タイトルが空の記事、形式の壊れたメールアドレス、重複したアカウント——こうしたデータが混入すると、表示の崩れ、検索の不具合、メール送信の失敗など、さまざまな問題が起こります。
バリデーション(Validation) は、ActiveRecordが提供する データの検証機構 です。モデルの save や update が呼ばれたとき、DBへの書き込みの前に自動で検証を実行し、不正なデータをブロックします。
検証に失敗すると save は false を返し、model.errors にエラー内容が格納されます。コントローラはこの戻り値で成功・失敗を分岐し、ビューがエラーメッセージを表示する流れです。
バリデーションは save、create、update などの DBに書き込むメソッド が呼ばれたときに自動実行されます。save(validate: false) で明示的にスキップすることもできますが、通常は使いません。
よく使うバリデーション
presence — 「空っぽはダメ」
class Article < ApplicationRecord
validates :title, presence: true
validates :body, presence: true
end
# 使用例
article = Article.new(title: "", body: "")
article.valid? # => false
article.errors.full_messages
# => ["Titleを入力してください", "Bodyを入力してください"]
uniqueness — 「重複はダメ」
class User < ApplicationRecord
validates :email_address, uniqueness: true
# ↑ 同じメールアドレスで2回登録できない
end
⚠️
uniquenessバリデーションはRuby側でのチェックです。同時にリクエストが来た場合にすり抜ける可能性があるため、DBの ユニークインデックス も併せて設定するのが安全です(→ 第17章で扱います)。
length — 「長すぎ / 短すぎはダメ」
class Article < ApplicationRecord
validates :title, length: { maximum: 100 }
# ↑ タイトルは100文字まで
validates :body, length: { minimum: 10, message: "は10文字以上で入力してください" }
# ↑ 本文は10文字以上。エラーメッセージをカスタマイズ
end
format — 「形式が違う」
class User < ApplicationRecord
validates :email_address, format: { with: URI::MailTo::EMAIL_REGEXP,
message: "の形式が正しくありません" }
end
numericality — 「数値でないとダメ」
class Like < ApplicationRecord
validates :likeable_id, numericality: { only_integer: true, greater_than: 0 }
end
バリデーション一覧
| バリデーション | チェック内容 | 使用例 |
|---|---|---|
presence |
空でないか | タイトル、メールアドレス |
uniqueness |
重複していないか | メールアドレス、タグ名 |
length |
文字数の範囲内か | タイトル(max 100)、パスワード(min 8) |
format |
正規表現に一致するか | メールアドレス、URL |
numericality |
数値か | 価格、数量 |
inclusion |
指定リストに含まれるか | ステータス(draft/published) |
confirmation |
確認用フィールドと一致するか | パスワード確認 |
カスタムバリデーション
標準のバリデーションでは足りない場合、 カスタムバリデーション を作れます。
class Article < ApplicationRecord
validates :title, presence: true
validate :title_not_contain_banned_words # ← validates ではなく validate(sなし)
private
def title_not_contain_banned_words
banned_words = ["スパム", "広告", "宣伝"]
banned_words.each do |word|
if title&.include?(word)
errors.add(:title, "に「#{word}」を含めることはできません")
end
end
end
end
validates(s付き)は組み込みバリデーション、validate(sなし)はカスタムバリデーションで使います。紛らわしいですが、この違いは重要です。
📝 フォーム送信からエラー表示までの流れ
バリデーションがコントローラやビューとどう連携するか、全体の流れを追います。
① ユーザーがフォームに入力して「投稿」ボタンを押す
↓
② ブラウザがPOSTリクエストを送る
POST /articles { article: { title: "", body: "test" } }
↓
③ コントローラが受け取る(Strong Parametersで項目を制限 → [第14章](https://qiita.com/harapeco-mgn/items/9c033b20f56250d0edb0)で詳しく扱います)
↓
④ モデルが保存を試みる
@article.save → バリデーション実行
→ title が空なので presence バリデーションに引っかかる
→ @article.errors に「Titleを入力してください」が追加される
→ save は false を返す
↓
⑤ コントローラが失敗時の処理を実行
render :new, status: :unprocessable_entity
→ @article(エラー情報付き)をビューに渡す
↓
⑥ ビューがエラーメッセージを表示する
ポイントは render :new で再表示されたとき、 ユーザーが入力したデータが残っている ことです。@article にフォームのデータが保持されているので、form_with(model: @article) が自動的にフォームに値を埋め戻してくれます。フォームの詳細は(→ 第13章で詳しく扱います)。
🔄 ActiveRecord と ActiveModel
ActiveRecord — テーブルと紐づくモデル
ApplicationRecord を継承したモデルは、全て ActiveRecord::Base の機能を持ちます。users テーブルがあれば User モデルが対応し、find・save・where などのメソッドでDBを操作できます(→ 第15章で詳しく扱います)。
ActiveModel::Model — テーブルを持たないモデル
DBにテーブルは不要だが、バリデーションやフォームヘルパーとの連携が必要な場面で使います。代表例は「お問い合わせフォーム」です。
# app/models/contact_form.rb
class ContactForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :name, :string
attribute :email, :string
attribute :message, :string
validates :name, presence: true
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :message, presence: true, length: { minimum: 10 }
end
# app/controllers/contacts_controller.rb
class ContactsController < ApplicationController
def new
@contact = ContactForm.new
end
def create
@contact = ContactForm.new(contact_params)
if @contact.valid?
ContactMailer.inquiry(@contact).deliver_later
redirect_to root_path, notice: "送信しました"
else
render :new, status: :unprocessable_entity
end
end
private
def contact_params
params.expect(contact_form: [:name, :email, :message])
end
end
コントローラから見ると、ActiveModel::Model を使ったモデルも ActiveRecord::Base を継承したモデルも、 ほぼ同じ書き方で扱えます。form_with(model: @contact) が動き、@contact.valid? でバリデーションが走り、@contact.errors にエラーが入る——インターフェースが統一されているからです。
使い分けの判断基準
| ActiveRecord(テーブルあり) | ActiveModel::Model(テーブルなし) | |
|---|---|---|
| DBへの保存が必要か | ✅ 必要 | ❌ 不要(メール送信、検索フォーム等) |
| バリデーションが必要か | ✅ 使える | ✅ 使える |
| form_with との連携 | ✅ 使える | ✅ 使える |
| 典型例 | User, Article, Comment | ContactForm, SearchForm |
🔍 Dirtyトラッキング — 変更を検知する仕組み
Dirtyトラッキング (Dirty tracking)とは、モデルの属性が「変更されたかどうか」を検知する ActiveRecord の機能です。コールバックの中で「特定の属性が変更されたときだけ処理を実行したい」という場面で役立ちます。
「Dirty(汚れた)」という名前は、「変更が加えられた(未保存の変更がある)状態」を「汚れている」と表現する慣習からきています。
article = Article.find(1)
# 変更前の状態
article.title # => "元のタイトル"
article.title_changed? # => false
article.changed? # => false(何も変更されていない)
# 属性を変更する
article.title = "新しいタイトル"
# 変更後の状態
article.title_changed? # => true(変更されている!)
article.changed? # => true
article.title_was # => "元のタイトル"(変更前の値)
article.title_change # => ["元のタイトル", "新しいタイトル"](変更前→後のペア)
article.changes # => { "title" => ["元のタイトル", "新しいタイトル"] }
# 保存すると変更フラグがリセットされる
article.save
article.title_changed? # => false
コールバックでの活用
class Article < ApplicationRecord
before_save :update_published_at, if: :published_changed?
private
def update_published_at
# published フラグが変更されたときだけ実行
if published?
self.published_at = Time.current
else
self.published_at = nil
end
end
end
if: :published_changed? の部分が Dirtyトラッキングの活用例です。「公開フラグが変わったときだけ、公開日時を更新する」という処理を、コールバックに余分な条件判定を持ち込まずシンプルに書けます。
保存後の変更を確認する
article.save
# 保存が完了した後も、直前の変更は _previously_ で参照できる
article.title_previously_changed? # => true
article.title_previous_change # => ["元のタイトル", "新しいタイトル"]
🛠️ KnowledgeNoteでの具体例
KnowledgeNoteのモデルに定義されたバリデーションの実例です。
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
validates :name, presence: true, length: { maximum: 50 }
validates :email_address, presence: true,
uniqueness: true,
format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }
# メールアドレスの前後の空白と大文字を自動で除去する
normalizes :email_address, with: ->(e) { e.strip.downcase }
end
# app/models/article.rb
class Article < ApplicationRecord
belongs_to :user
validates :title, presence: true, length: { maximum: 100 }
validates :body, presence: true, length: { minimum: 10 }
# 公開フラグが変化したときだけ公開日時を更新する
before_save :set_published_at, if: :published_changed?
validate :body_not_contain_only_spaces
private
def set_published_at
self.published_at = published? ? Time.current : nil
end
def body_not_contain_only_spaces
if body.present? && body.strip.empty?
errors.add(:body, "は空白のみでは投稿できません")
end
end
end
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :user
belongs_to :article
belongs_to :parent, class_name: "Comment", optional: true
validates :body, presence: true, length: { maximum: 1000 }
end
各モデルが「自分のデータの品質」を自分で管理しています。コントローラはバリデーションの結果を save の戻り値で受け取り、成功か失敗かで処理を分岐するだけです。
💼 面接で聞かれたら?
Q:MVCのModelの役割を説明してください。
「モデルはデータの永続化と業務ルールを管理する層です。DBへの読み書きだけでなく、バリデーション(データの検証)、アソシエーション(テーブル間の関係)、スコープ(よく使うクエリの定義)などを担います。コントローラはビジネスロジックを持たず、モデルに処理を委ねる設計が原則です。」
深掘りされたら:
- 「Fat Modelとは?」→ モデルに業務ロジックが集中して肥大化した状態。コントローラから薄くする際にモデルに詰め込みすぎると起こる。Service ObjectやForm Objectなどに分割するのが対策(→ 第25章)。
Q:バリデーションとは何ですか?
「バリデーションは、DBに保存する前にデータの正しさを検証する仕組みです。タイトルが空でないか(presence)、メールが重複していないか(uniqueness)などをモデルに定義します。検証に失敗すると
saveがfalseを返し、model.errorsにエラー内容が格納されます。」深掘りされたら:
- 「uniquenessバリデーションだけでは不十分な理由は?」→ Ruby側のチェックなので、同時リクエストではすり抜ける可能性がある。DBにユニークインデックスを設定してDB側でも保証するのが安全(→ 第17章)。
- 「
validatesとvalidateの違いは?」→validates(s付き)は組み込みバリデーション用。validate(sなし)はカスタムバリデーションのメソッドを指定するとき。
🔗 もっと深く知りたい人へ(1次情報リンク)
- Rails ガイド:Active Record バリデーション — 全バリデーションヘルパーの一覧
- Rails ガイド:Active Record の基礎 — Active Recordパターンの説明
- Rails API:ActiveModel::Dirty — Dirtyトラッキングの全メソッド
- Rails API:ActiveModel::Model — テーブルなしモデルの実装
まとめ
- ✅ モデルはデータの永続化・バリデーション・アソシエーション・スコープなど業務ルールを管理する層
- ✅ 「モデル = テーブル」ではない。
ActiveModel::Modelを使えばDBなしでもバリデーション等が使える - ✅ バリデーションは「手荷物検査」。DBに保存する前にデータの正しさをチェックする
- ✅
validates(s付き)は組み込み用、validate(sなし)はカスタム用 - ✅ Dirtyトラッキングで属性の変更を検知し、コールバック内で活用できる
- ✅ モデルが肥大化したら Fat Model の兆候。Service ObjectやForm Objectに分割する(→ 第25章)
📚 シリーズ目次:「今さら学ぶ」シリーズ — はじめに