1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

第12章|今さら学ぶ「モデル詳解」

1
Last updated at Posted at 2026-02-21

第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次情報リンク)


まとめ

  • ✅ モデルはデータの永続化・バリデーション・アソシエーション・スコープなど業務ルールを管理する層
  • ✅ 「モデル = テーブル」ではない。ActiveModel::Model を使えばDBなしでもバリデーション等が使える
  • ✅ バリデーションは「手荷物検査」。DBに保存する前にデータの正しさをチェックする
  • ✅ validates(s付き)は組み込み用、validate(sなし)はカスタム用
  • ✅ Dirtyトラッキングで属性の変更を検知し、コールバック内で活用できる
  • ✅ モデルが肥大化したら Fat Model の兆候。Service ObjectやForm Objectに分割する(→ 第25章)

📚 シリーズ目次:「今さら学ぶ」シリーズ — はじめに

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?